Henrique Schreiner
Henrique Schreiner

Reputation: 185

How to re-apply bindings after clear a node on Knockout.js?

I'm unable to apply bindings even after clearing node on Knockout. I'm trying to use components dynamically based on user actions (click a button, etc).

I created a UIComponent class to handle components, creating and removing them:

    (function (window) {

      define(['knockout'], function (ko) {

        /**
         * Custom component to the given DOM node
         * @type {{render: UIComponent.render}}
         */
        var UIComponent = (function () {

            return {

                /**
                 *  Render a component
                 * @param {object} component
                 * @param {object} element
                 */
                render: function (component, element, childComponents) {

                    var tagName = element.tagName && element.tagName.toLowerCase();

                    if (ko.components.isRegistered(tagName)) {
                        ko.components.unregister(tagName);
                    }

                    if (undefined !== childComponents) {

                        childComponents.forEach(function (child) {

                            if (ko.components.isRegistered(child.tagName)) {
                                ko.components.unregister(child.tagName);
                            }

                            ko.components.register(child.tagName, child.component);
                        });
                    }

                    ko.components.register(tagName, component);
                    ko.applyBindings();
                },

                /**
                 * Removes a component
                 * @param {object} component
                 * @param {object} element
                 */
                remove: function (component, element) {

                    ko.components.unregister(component.name);
                    ko.cleanNode(element);

                    // Remove any child elements from node
                    while (element.firstChild) {
                        element.removeChild(element.firstChild);
                    }
                }
            };

        })();

        window.UIComponent = UIComponent;

        return UIComponent;
    });
})(window);

I'm using RequireJS to load files for components dynamically, so I declare a component with an object containing a javascript file for viewModel and a template:

var QuoteForm = {
        viewModel: {
            require: '/scripts/components/sample-form.js'
        },
        template: {
            require: 'text!/templates/forms/available-products-quote-form.html'
        }
    };

And I use this component object on UIComponent class like this:

UIComponent.render(QuoteForm, $('quote-form')[0]);

This is the viewModel inside sample-form.js file:

define(['knockout'], function (ko) {

    var SampleFormModel = function (params) {

    };

    SampleFormModel.prototype = {

        /**
         * Dispose any resources used on component
         */
        dispose: function () {
            console.log('SampleFormModel disposed');
        }
    };

    return SampleFormModel;
});

To test, I'm using a simple setTimeout function to simulate user action:

UIComponent.render(QuoteForm, $('quote-form')[0]);

setTimeout(function () {
  UIComponent.remove(QuoteForm, $('quote-form')[0]);
}, 2000);

setTimeout(function () {
  UIComponent.render(QuoteForm, $('quote-form')[0]);
}, 4000);

When the first setTimeout executes, I got the SampleFormModel disposed message logged on console, but when the second setTimeout executes, I got an error from Knockout:

Uncaught Error: You cannot apply bindings multiple times to the same element.

Even when Knockout disposes the component, I can't apply the bindings on the same node again.

Upvotes: 0

Views: 1914

Answers (3)

Henrique Schreiner
Henrique Schreiner

Reputation: 185

Like Roy J said, I was applying the bindings to entire DOM. I just made some changes to UIComponent class just to register components to specific element:

(function (window) {

    define(['knockout'], function (ko) {

        /**
         * Custom component to the given DOM node
         * @type {{render: UIComponent.render}}
         */
        var UIComponent = (function () {

            return {

                /**
                 *  Render a component
                 * @param {object} component
                 * @param {object} element
                 */
                render: function (component, element, childComponents) {

                    var tagName = element.tagName && element.tagName.toLowerCase();

                    if (undefined !== childComponents) {

                        childComponents.forEach(function (child) {
                            ko.components.register(child.tagName, child.component);
                        });
                    }

                    ko.components.register(tagName, component);
                    ko.applyBindings(component, element);
                },

                /**
                 * Removes a component
                 * @param {object} component
                 * @param {object} element
                 */
                remove: function (component, element) {

                    ko.components.unregister(component.name);
                    ko.cleanNode(element);

                    // Remove any child elements from node
                    while (element.firstChild) {
                        element.removeChild(element.firstChild);
                    }
                }
            };

        })();

        window.UIComponent = UIComponent;

        return UIComponent;
    });

})(window);

Now it's working like a charm!

Upvotes: 3

Jonathan
Jonathan

Reputation: 245

I'm not very familiar yet with Knockout Components but I've been looking at the documentation.

There's this one tidbit here:

If you want all instances of your component to share the same viewmodel object instance (which is not usually desirable)...it’s necessary to specify viewModel: { instance: object }, and not just viewModel: object.

http://knockoutjs.com/documentation/component-registration.html#registering-components-as-a-viewmodeltemplate-pair

Looking at your code, you essentially have multiple instances of the HTML component (by rendering twice), but you are reusing the viewmodel. Have you tried this:

var QuoteForm = {
    viewModel: {
        instance: { require: '/scripts/components/sample-form.js' }
    },
    template: {
        require: 'text!/templates/forms/available-products-quote-form.html'
    }
};

Upvotes: 0

Anirudh Modi
Anirudh Modi

Reputation: 1829

You, need to clean your element before applying a binding on the same element, so, You can clean the element by using

ko.cleanNode(element)

This function will remove all the binding from the node on which you want to reapply the binding.

Upvotes: 1

Related Questions