98

I am building a permissions UI, I have a list of permissions with a select list next to each permission. The permissions are represented by an observable array of objects which are bound to a select list:

<div data-bind="foreach: permissions">
     <div class="permission_row">
          <span data-bind="text: name"></span>
          <select data-bind="value: level, event:{ change: $parent.permissionChanged}">
                   <option value="0"></option>
                   <option value="1">R</option>
                   <option value="2">RW</option>
           </select>
      </div>
 </div>

Now the problem is this: the change event gets raised when the UI is just populating for the first time. I call my ajax function, get the permissions list and then the event get raised for each of the permission items. This is really not the behavior I want. I want it to be raised only when a user really picks out a new value for the permission in the select list, how can I do that?

peterh
  • 11,875
  • 18
  • 85
  • 108
guy schaller
  • 4,710
  • 4
  • 32
  • 54

11 Answers11

126

Actually you want to find whether the event is triggered by user or program , and its obvious that event will trigger while initialization.

The knockout approach of adding subscription won't help in all cases, why because in most of the model will be implemented like this

  1. init the model with undefined data , just structure (actual KO initilization)
  2. update the model with initial data (logical init like load JSON , get data etc)
  3. User interaction and updates

The actual step that we want to capture is changes in 3, but in second step subscription will get call , So a better way is to add to event change like

 <select data-bind="value: level, event:{ change: $parent.permissionChanged}">

and detected the event in permissionChanged function

this.permissionChanged = function (obj, event) {

  if (event.originalEvent) { //user changed

  } else { // program changed

  }

}
Sarath
  • 9,030
  • 11
  • 51
  • 84
  • 5
    I think this should be marked as the correct answer. I had the exact scenario as described in the question, and I tested passing strings as the Key to the `select` element. However, even when the initial value of the select matched one of the options, it still triggered the `change` event. When I implemented this simple `if` block in the handler, everything worked as expected. Try this method first. Thanks, @Sarath! – Pflugs Oct 23 '15 at 14:35
  • I like the concept of differentiating between user changed and program changed. This is helpful in everyday cases, especially with knockout where observables are most likely to change value without any user interaction. – rodiwa Jan 07 '16 at 07:39
  • Agree with @Pflugs, differentiating between the event types worked for me and this should be the accepted answer. – Emma Middlebrook Feb 08 '16 at 16:39
  • A big thank you for this answer. I was tearing my hair out! – DavidHyogo Mar 09 '17 at 08:25
  • Getting error as: Unable to process binding "value: function (){return level }" Message: level is not defined Any idea why? – Twinkal Aug 06 '17 at 08:06
  • 1
    def should be an accepted answer... event type property could be used to differentiate triggering of the premissionChanged – caniaskyouaquestion Nov 10 '17 at 09:55
  • 1
    Also used this method to detect whether a user was changing a select rather than another trigger. In my case updating the options values via the "options" binding triggered the "change" event. – nath Dec 01 '17 at 16:15
  • This is not the right answer. As @Michael Best points out, the correct Knockout way to do things is with observables and subscribers. The issue of setting an observable first time and not have it fire any subscribers or event handlers is trivial. It does not require a contrived, and ultimately incorrect, solution like this. – Soulriser Oct 14 '18 at 21:25
32

This is just a guess, but I think it's happening because level is a number. In that case, the value binding will trigger a change event to update level with the string value. You can fix this, therefore, by making sure level is a string to start with.

Additionally, the more "Knockout" way of doing this is to not use event handlers, but to use observables and subscriptions. Make level an observable and then add a subscription to it, which will get run whenever level changes.

Michael Best
  • 16,623
  • 1
  • 37
  • 70
  • after 2 hours of digging the cause my form triggering the 'change' event after page load, you guessing saved me. – Samih A Jan 13 '15 at 14:35
  • Your answer gave me an idea ... I purposefully changed the type coming from the url, so my subscribe function would trigger on page load (I wanted it to process a url var, not just when my dropdown triggered a change). Is there a less hacky way of doing this? – Jiffy Apr 03 '17 at 01:44
4

Here is a solution that may help with this strange behaviour. I couldn't find a better solution than place a button to manually trigger the change event.

EDIT: Maybe a custom binding like this could help:

ko.bindingHandlers.changeSelectValue = {

   init: function(element,valueAccessor){

        $(element).change(function(){

            var value = $(element).val();

            if($(element).is(":focus")){

                  //Do whatever you want with the new value
            }

        });

    }
  };

And in your select data-bind attribute add:

changeSelectValue: yourSelectValue
jamesmortensen
  • 33,636
  • 11
  • 99
  • 120
Ingro
  • 2,841
  • 5
  • 26
  • 42
  • oh... you mean like a save button?.. not good for me.. i really just need this to work :) – guy schaller Jun 18 '12 at 11:13
  • Edit answer with a better solution (hopefully). – Ingro Jun 19 '12 at 13:19
  • Hey Ingro, this got flagged (incorrectly) as not an answer. I reworded your initial sentence so flaggers won't accidentally think you're posting a question as an answer. Hope this helps! :) – jamesmortensen Nov 07 '13 at 02:27
  • @jmort253: sorry, that was my fault. It's better not to answer using "I have the same problem too", as this seems to look like you're asking something else. Instead, directly provide a solution to an answer and explain how it works. – Qantas 94 Heavy Nov 07 '13 at 11:57
  • 2
    Yeah sorry, my mistake. It was one of the first answer I posted on stackoverflow and didn't know all the rules. Thanks for the edit! – Ingro Nov 07 '13 at 14:11
3

I use this custom binding (based on this fiddle by RP Niemeyer, see his answer to this question), which makes sure the numeric value is properly converted from string to number (as suggested by the solution of Michael Best):

Javascript:

ko.bindingHandlers.valueAsNumber = {
    init: function (element, valueAccessor, allBindingsAccessor) {
        var observable = valueAccessor(),
            interceptor = ko.computed({
                read: function () {
                    var val = ko.utils.unwrapObservable(observable);
                    return (observable() ? observable().toString() : observable());
                },
                write: function (newValue) {
                    observable(newValue ? parseInt(newValue, 10) : newValue);
                },
                owner: this
            });
        ko.applyBindingsToNode(element, { value: interceptor });
    }
};

Example HTML:

<select data-bind="valueAsNumber: level, event:{ change: $parent.permissionChanged }">
    <option value="0"></option>
    <option value="1">R</option>
    <option value="2">RW</option>
</select>
Community
  • 1
  • 1
mhu
  • 17,720
  • 10
  • 62
  • 93
2

If you use an observable instead of a primitive value, the select will not raise change events on initial binding. You can continue to bind to the change event, rather than subscribing directly to the observable.

1

Quick and dirty, utilizing a simple flag:

var bindingsApplied = false;

var ViewModel = function() {
    // ...

    this.permissionChanged = function() {
        // ignore, if flag not set
        if (!flag) return;

        // ...
    };
};

ko.applyBindings(new ViewModel());
bindingsApplied = true; // done with the initial population, set flag to true

If this doesn't work, try wrapping the last line in a setTimeout() - events are async, so maybe the last one is still pending when applyBindings() already returned.

Niko
  • 26,516
  • 9
  • 93
  • 110
  • hi thanks for the reply, this won't work for me because i call the ko.applyBindings when the dom is ready, but my permissions a are being populated to my model at a later stage... so the flag would be true but the problem would still happen. – guy schaller Jun 18 '12 at 07:52
0

I had a similar problem and I just modified the event handler to check the type of the variable. The type is only set after the user selects a value, not when the page is first loaded.

self.permissionChanged = function (l) {
    if (typeof l != 'undefined') {
        ...
    }
}

This seems to work for me.

fireydude
  • 1,181
  • 10
  • 23
0

Try this one:

self.GetHierarchyNodeList = function (data, index, event)
{
    debugger;
    if (event.type != "change") {
        return;
    }        
} 

event.type == "change"
event.type == "load" 
biruk1230
  • 3,042
  • 4
  • 16
  • 29
-1

If you are working using Knockout, use the key functionality of observable functionality knockout.
Use ko.computed() method and do and trigger ajax call within that function.

vasu
  • 1
-1

use this:

this.permissionChanged = function (obj, event) {

    if (event.type != "load") {

    } 
}
AGuyCalledGerald
  • 7,882
  • 17
  • 73
  • 120
-2

Create js component

define([
    'Magento_Ui/js/form/element/select',
    'mage/translate'
], function (AbstractField, $t) {
    'use strict';

    return AbstractField.extend({
        defaults: {
            imports: {
                update: 'checkout.steps.shipping-step.shippingAddress.shipping-address-fieldset.country_id:value'
            },
            modules: {
                vat_id: '${ $.parentName }.vat_id'


            }
        },

        /**
         * Initializes UISelect component.
         *
         * @returns {UISelect} Chainable.
         */
        initialize: function () {
            this._super();
            this.vat_id().visible(false);
            return this;
        },
        update: function (value) {
            if(value == 'GB'){
                this.vat_id().visible(true);
            }else{
                this.vat_id().visible(false);

            }
        }
        
        
            
         
      
    });
});