Exponentially increasing click actions firing on custom buttons in Knockout.js

I implemented a button with a custom binding handler that is functioning perfectly! However, I have observed that on the first click, the action is triggered once. But on subsequent clicks, it multiples - 4 times, then 8, and so on until I refresh the page.

While I have gone through some Stack Overflow posts about knockoutjs button click events firing multiple times, I believe my situation might have a unique twist.

To provide more context, here is a gist of my entire setup: https://gist.github.com/fischgeek/dcd6cad07bce920cbd03aa6d6dc1e125

In short, below is the custom binding handler extracted for reference; in case there's an obvious issue that has escaped my notice:

ko.bindingHandlers.actionButton = {
        init: function (element, valueAccessor, allBindingsAccessor, data, context) {
            var value = valueAccessor();
            if (typeof value === 'object') {
                throw (`${value.Title()} binding must be a string.`);
            }
            var options = allBindingsAccessor().abOptions || {};
            $(element).attr('type', 'submit');
            $(element).addClass('btn');
            $(element).append(`<span data-bind="text: ${value}.Title()"></span>&nbsp;`);
            $(element).append(`<span class="glyphicon" data-bind="css: ${value}.Glyph()"></span>`);
            data[value].Title(options.Title || data[value].Title());
            ko.applyBindingsToNode(element, { css: data[value].State(), click: data[value].WorkMethod() });
        },
        update: function (element, valueAccessor, allBindingsAccessor, data, context) {
            var value = valueAccessor();
            ko.applyBindingsToNode(element, { css: data[value].State(), click: data[value].WorkMethod() });
        }
    };

Here's how you can use this in your HTML code:

<button data-bind="actionButton: 'abSaveSchedule', abOptions: {Title: 'Save Schedule'}"></button>

Answer №1

When you first initialize the method, a button is added and bindings are applied to it. However, in the update method (which is referred to as the initial time and whenever any observables change), the bindings are applied to the element again.

Each time there is a click, the number of times the action is called increases depending on the number of observables updated and the number of existing bindings already applied to these observables (such as Title, Glyph, State).

It is possible that removing the update handler entirely could make everything function fine.

Answer №2

In rare cases like this, the use of Knockout's cleanNode function may be necessary...

Discussion on init and update

It is crucial to note that after invoking the init function, Knockout will always proceed with calling the update function. This implies that in your situation, the applyBindings method will be invoked twice on the same element, causing the WorkMethod to execute twice upon clicking.

The suggested approach, as indicated in this answer by Philip, recommends excluding the applyBindings call from the update function. While this resolves most issues, there remains a concern: automatic updates when Title, State, or WorkMethod undergo changes. If this is not problematic for your scenario, consider making these properties non-observable. But assuming they need to be updated...

Maintaining the current view state

Lack of "update support" in your binding arises due to:

In the init function, you de-reference Title, State, and WorkMethod before applying inner bindings. Consequently, your custom binding receives all update calls, necessitating re-application of bindings to inner elements. (Refer to if and with bindings' source code for insights)

For text and css bindings, appropriate handling involves passing a reference to the respective observable rather than the inner value. This enables default bindings to manage updates within their own update functions.

$(element).append(`<span data-bind="text: ${value}.Title"></span>&nbsp;`);
// ...
ko.applyBindingsToNode(element, {
  css: data[valueAccessor()].State
// ...

Similarly, when dealing with the click event binding, re-application becomes essential since it does not unwrap its valueAccessor... (Do you think this behavior should change?)

To prevent multiple onClick listeners, cleaning the node prior to re-applying bindings is imperative.

Demonstration with necessary enhancements

ko.bindingHandlers.actionButton = {
  init: function(element, valueAccessor, allBindingsAccessor, data, context) {
    var value = valueAccessor();

    if (typeof value === 'object') {
      throw (`${value.Title()} binding must be a string.`);
    }
    var options = allBindingsAccessor().abOptions || {};
    $(element).attr('type', 'submit');
    $(element).addClass('btn');

    // Preserve the observable while allowing for it to be set from the binding's options
    if (options.Title) {
      data[value].Title(options.Title);
    }
    
    $(element).append(`<span data-bind="text: ${value}.Title"></span>&nbsp;`);
    $(element).append(`<span class="glyphicon" data-bind="css: ${value}.Glyph"></span>`);

    
    
  }, update:  function(element, valueAccessor, allBindingsAccessor, data, context) {
    ko.cleanNode(element);
    ko.applyBindingsToNode(element, {
      css: data[valueAccessor()].State,
      click: data[valueAccessor()].WorkMethod()
    });
  }
};


var vm = {

  Title: ko.observable("title"),
  State: ko.observable(""),
  WorkMethod: ko.observable(
    function() {
      console.log("click");
    }
  ),
  Glyph: ko.observable("")
};

ko.applyBindings({
  "abSaveSchedule": vm,
  changeMethod: () => vm.WorkMethod(() => console.log("click2")),
  changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>

<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>

While functional, the existing structure may appear cluttered.

Refactored Approach

Here is a revised version accomplishing the same tasks but with enhanced readability:

ko.bindingHandlers.actionButton = {
  init: function(element, valueAccessor, allBindingsAccessor, data, context) {
    var propName = valueAccessor();
    var options = allBindingsAccessor().abOptions || {};
    var ctx = data[propName];

    if (options.Title) {
      data[propName].Title(options.Title);
    }

    ko.applyBindingsToNode(element, {
      attr: {
        "type": "submit",
        "class": "btn"
      },
      css: ctx.State,
      click: (...args) => ctx.WorkMethod()(...args),
      template: {
        data: ctx,
        nodes: $(`<span data-bind="text:Title"></span>
          <span class="glyphicon" data-bind="css: Glyph"></span>`)
      }
    });
    return {
      controlsDescendantBindings: true
    };
  }
};


var vm = {

  Title: ko.observable("title"),
  State: ko.observable(""),
  WorkMethod: ko.observable(
    function() {
      console.log("click");
    }
  ),
  Glyph: ko.observable("")
};

ko.applyBindings({
  "abSaveSchedule": vm,
  changeMethod: () => vm.WorkMethod(() => console.log("click2")),
  changeTitle: () => vm.Title("New title")
});
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
<button data-bind="actionButton: 'abSaveSchedule', abOptions: { Title: 'override title' }"></button>

<button data-bind="click: changeMethod">change work method</button>
<button data-bind="click: changeTitle">change title</button>

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

The onChange event does not work as expected for Select controls

I am facing an issue with my common useForm.tsx file when handling the onChange event for select controls. The error message I encounter is displayed below. Does anyone have any suggestions on how to resolve this? Error: Type '(e: ChangeEvent<HTM ...

Ways to decrease the size of this item while maintaining its child components?

Here is an object that I am working with: { "name": "A", "children": [ { "name": "B", "open": false, "registry": true, "children": [ { ...

NGC Error: Unable to locate the type definition file for 'rx/rx.all' - Please fix this issue

Recently, I've been working on some enhancements to the flex-layout project. While running ngc ./node_modules/.bin/ngc -p src/lib/tsconfig.json I encountered an issue... Error Cannot find type definition file for 'rx/rx.all'. It seems li ...

Creating descriptions for types in Vue.js using TypeScript

When running this code snippet, you might encounter the error message 'description' does not exist in PropValidator export default Vue.extend( { name: 'something', props: { 'backgro ...

Updating the status of the checkbox on a specific row using Typescript in AngularJS

My goal is to toggle the checkbox between checked and unchecked when clicking on any part of the row. Additionally, I want to change the color of the selected rows. Below is my current implementation: player.component.html: <!-- Displaying players in ...

Issue with migrating TypeOrm due to raw SQL statement

Is it possible to use a regular INSERT INTO statement with TypeOrm? I've tried various ways of formatting the string and quotes, but I'm running out of patience. await queryRunner.query('INSERT INTO "table"(column1,column2) VALUES ...

Error: Attempting to change a read-only property "value"

I am attempting to update the input value, but I keep receiving this error message: TypeError: "setting getter-only property "value" I have created a function in Angular to try and modify the value: modifyValue(searchCenter, centerId){ searchCenter.va ...

Creating a Union Type from a JavaScript Map in Typescript

I am struggling to create a union type based on the keys of a Map. Below is a simple example illustrating what I am attempting to achieve: const myMap = new Map ([ ['one', <IconOne/>], ['two', <IconTwo/>], ['three ...

Transforming TypeScript declaration files into Kotlin syntax

Has there been any progress on converting d.ts files to Kotlin? I came across a post mentioning that Kotlin developers were working on a converter, but I am unsure about the current status. I also found this project, which seems to be using an outdated c ...

What are the steps to set up NextJS 12.2 with SWC, Jest, Eslint, and Typescript for optimal configuration?

Having trouble resolving an error with Next/Babel in Jest files while using VSCode. Any suggestions on how to fix this? I am currently working with NextJS and SWC, and I have "extends": "next" set in my .eslintrc file. Error message: Parsing error - Can ...

Can the detectChanges() method in Angular cause any issues when used with the form control's valueChanges event?

Within my parent Component, I am working with a formGroup and updating its value using patchValue method. ngAfterViewInit() { this.sampleform.controls['a'].patchValue ...} I then pass this form to a child component in the parent component's ...

utilize a modal button in Angular to showcase images

I am working on a project where I want to display images upon clicking a button. How can I set up the openModal() method to achieve this functionality? The images will be fetched from the assets directory and will change depending on the choice made (B1, ...

Is it possible to deactivate input elements within a TypeScript file?

Is it possible to disable an HTML input element using a condition specified in a TS file? ...

How can I use the Required utility type in TypeScript for nested properties?

I'm exploring how to utilize the Required keyword to ensure that all members are not optional in TypeScript. I've achieved success with it so far, but I've run into an issue where it doesn't seem to work for nested members of an interfa ...

Can the Date class be expanded by overloading the constructor method?

In my dataset, there are dates in different formats that Typescript doesn't recognize. To address this issue, I developed a "safeDateParse" function to handle extended conversions and modified the Date.parse() method accordingly. /** Custom overload ...

Styling the pseudo element ::part() on an ion-modal can be customized based on certain conditions

Looking for a solution regarding an ion-modal with specific CSS settings? I previously had the following CSS: ion-modal::part(content) { width: 300px; height: 480px; } Now, I need to adjust the height based on conditions: if A, the height should be lo ...

What is the best way to manage data types using express middleware?

In my Node.js project, I am utilizing Typescript. When working with Express middleware, there is often a need to transform the Request object. Unfortunately, with Typescript, it can be challenging to track how exactly the Request object was transformed. If ...

Stuck on loading screen with Angular 2 Testing App

Currently working on creating a test app for Angular 2, but encountering an issue where my application is continuously stuck on the "Loading..." screen. Below are the various files involved: app.component.ts: import {Component} from '@angular/core& ...

Assign a value to a FormControl in Angular 6

I have 60 properties linked to 60 controls through the mat-tab in a form. When it comes to editing mode, I need to assign values to all the controls. One approach is as follows: this.form.controls['dept'].setValue(selected.id); This involves l ...

Guide on installing MathType plugins for CKEditor 5 in an Angular 8 environment

Encountering an issue while attempting to utilize MathType in CKEditor Error message at ./node_modules/@wiris/mathtype-ckeditor5/src/integration.js 257:98 Module parse failed: Unexpected token (257:98) A proper loader may be required to handle this file t ...