简体   繁体   中英

Knockoutjs valueHasMutated not working correctly

Hopefully this will be a quick one for a knockout guru....

I'm writing a couple of custom bindings to help me translate a UI using a custom translation engine in the project I'm working on.

One is to translate text, the other is to translate the 'placeholder' attribute on HTML5 input elements.

Both bindings are identical apart from the last statement, where one updates the text in the element and the other updates an attribute value.

The text one works perfectly, but the place holder one does not, and I'm stuck for an answer as to why.

The binding code is as follows:

Translated Text Binding

ko.bindingHandlers.translatedText = {

    init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {

        // Get our custom binding values
        var value = valueAccessor();
        var associatedObservable = value.observable;
        var translationToken = value.translationToken;

        // Set up an event handler that will respond to events telling it when our translations have finished loading
        // the custom binding will instantly update when a key matching it's translation ID is loaded into the
        // local session store
        window.addEventListener("TranslationsLoaded", (e) => {
            //associatedObservable(" "); // Force an update on our observable, so that the update routine below is triggered
            associatedObservable.valueHasMutated();
        }, false);

    },

    update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {

        // Get our custom binding values
        var value = valueAccessor();
        var associatedObservable = value.observable;
        var translationToken = value.translationToken;

        // Ask local storage if we have a token by that name
        var translatedText = utilityLib.getTranslatedString(translationToken);

        // Check if our translated text is defined, if it's not then substitute it for a fixed string that will
        // be seen in the UI (Whatever you put into the 'associatedObservable' at this point WILL appear in the element
        if (undefined === translatedText || translatedText === "" || translatedText === null) {
            if (sessionStorage["translations"] === undefined) {
                // No translations have loaded yet, so we blank the text
                translatedText = "";
            } else {
                // Translations have loaded, and the token is still not found
                translatedText = "No Translation ID";
            }
        }
        associatedObservable(translatedText);
        ko.utils.setTextContent(element, associatedObservable());
    }

} // End of translatedText binding

Translated Placeholder Binding

ko.bindingHandlers.translatedPlaceholder = {

    // This one works pretty much the same way as the translated text binding, except for the final part where
    // the translated text is inserted into the element.

    init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
        var value = valueAccessor();
        var associatedObservable = value.observable;
        var translationToken = value.translationToken;
        window.addEventListener("TranslationsLoaded", (e) => {
            debugger;
            associatedObservable.valueHasMutated();
        }, false);
    },

    update: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
        var value = valueAccessor();
        var associatedObservable = value.observable;
        var translationToken = value.translationToken;
        var translatedText = utilityLib.getTranslatedString(translationToken);
        debugger;
        if (undefined === translatedText || translatedText === "" || translatedText === null) {
            if (sessionStorage["translations"] === undefined) {
                translatedText = "";
            } else {
                translatedText = "No Translation ID";
            }
        }
        associatedObservable(translatedText);
        element.setAttribute("placeholder", translatedText);
    }

} // End of translatedPlaceholder binding

The idea is a simple one, if the binding runs and the translations are already present in sessionStorage, then we pick up the translated string and plug it in to the observable associated with the element.

If the translations have loaded, but the translation is not found "No Translation ID" is plugged into the observable bound to the element.

If the translations have NOT yet loaded, plug an empty string into the observable, then wait for the event 'TranslationsLoaded' to fire. When this event is raised, the observable bound to the element is mutated, causing an update to happen, which in turn re-checks the translations, which it then finds have loaded and so acts accordingly.

However.....

It doesn't matter how hard I try, the translated placeholder binding just will not fire it's update.

I can clearly see in the debugger that the event is recieved on both bindings, and the mutate function IS called.

On the translated text binding, I get the following sequence...

'init' -> 'update' -> 'event' -> 'mutate' -> 'update'

Which is exactly what I expect, and it occurs on every element+observable bound to that binding.

On the translated placeholder i get

'init' -> 'update' -> 'event' -> 'mutate'

but the final update never occurs.

As a result, the translated string for the placeholder is never looked up correctly, the text one with identical code works perfectly!!

For those who'll ask, i'm using the bindings like this:

<input type="text" class="form-control" data-bind="value: userName, translatedPlaceholder: { observable: namePlaceHolderText, translationToken: 'loginBoxNamePlaceholderText'}">

<span class="help-block" data-bind="translatedText: {observable: nameErrorText, translationToken: 'loginBoxUserNameEmptyValidationText'}"></span>

and inside the view model, the 'observable' parameters are just normal ko.observable variables holding strings.

Cheers Shawty

I believe you have event bubbling issues... try putting a 'return true' after your call to valueHasMutated like this:

init: (element: HTMLElement, valueAccessor: Function, allBindings: KnockoutAllBindingsAccessor, viewModel: any, bindingContext: KnockoutBindingContext) => {
    var value = valueAccessor();
    var associatedObservable = value.observable;
    var translationToken = value.translationToken;
    window.addEventListener("TranslationsLoaded", (e) => {
        associatedObservable.valueHasMutated();
        return true; // allow event to bubble
    }, false);
},

That being said, I am thinking all this manual eventing is against-the-grain for what knockout can do for you. You should be observing your data, binding to it, and letting knockout do all the internal eventing for you... that's what it does.

An example building on user3297291's fiddle:

ko.bindingHandlers.translatedPlaceholder = {
  init: function(element, valueAccessor) {
    var va = valueAccessor();
    var obs = ko.utils.unwrapObservable(va.obs);
    var placeholderStr = obs[va.key];
    console.log(placeholderStr);
    element.setAttribute("placeholder", placeholderStr);
  },
  update: function(element, valueAccessor) {
    var va = valueAccessor();
    var obs = ko.utils.unwrapObservable(va.obs);
        var placeholderStr = obs[va.key];
    console.log(placeholderStr);
    element.setAttribute("placeholder", placeholderStr);
  }
};

var vm = function() {
    var self = this;
    self.dictionary = ko.observable({
    "placeholder": "Initial State"
  });

  self.switchTranslations = function() {
    // Set the 'new' dictionary data:
    self.dictionary({
      "placeholder": "My Translated Placeholder"
    });
  };
}
ko.applyBindings(new vm());

Fiddle: https://jsfiddle.net/brettwgreen/5pmmd0va/

Trigger the update handler

From the Knockout documentation :

Knockout will call the update callback initially when the binding is applied to an element and track any dependencies (observables/computeds) that you access. When any of these dependencies change, the update callback will be called once again.

The key is that you have to access the observable within the update function to get updates. In your translatedText binding you do so:

ko.utils.setTextContent(element, associatedObservable());

But there is no such access of associatedObservable in the update function of translatedPlaceholder . You'll need to add it like this:

associatedObservable(translatedText);
associatedObservable();  // get notified of updates to associatedObservable
element.setAttribute("placeholder", translatedText);

A better approach

For your case, there really isn't a need for an update handler because you don't need to update the view based on changes to the viewmodel. Instead your updates just come from events, which can be set up in the init handler.

ko.bindingHandlers.translatedPlaceholder = {
    init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
        function loadTranslation() {
            var translationToken = valueAccessor(),
                translatedText = utilityLib.getTranslatedString(translationToken);
            element.setAttribute("placeholder", translatedText || "No Translation ID");

            window.removeEventListener("TranslationsLoaded", loadTranslation);
        }
        if (sessionStorage["translations"] === undefined)        
            window.addEventListener("TranslationsLoaded", loadTranslation, false);
        } else {
            loadTranslation();
        }
    }
}

Usage:

data-bind="value: userName, translatedPlaceholder: 'loginBoxNamePlaceholderText'"

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM