Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

How to know when a component has finished updating DOM? #1475

Closed
PrinceAli321 opened this issue Jul 31, 2014 · 26 comments
Closed

How to know when a component has finished updating DOM? #1475

PrinceAli321 opened this issue Jul 31, 2014 · 26 comments

Comments

@PrinceAli321
Copy link

Hi Guys,

I need to reflow my page after some KO components have finished rendering in the DOM to support this Foundation plugin: http://foundation.zurb.com/docs/components/equalizer.html

I know we have afterRender for the template and foreach bindings but i can't find anything similar for the new components functionality.

How can i go about executing a callback when my components are loaded (either individually or after all have been loaded)? Is there something i've missed?

@chafnan
Copy link

chafnan commented Aug 3, 2014

Could you provide an example?

@pbouzakis
Copy link

+1

I was trying to create a ko plugin for supporting insertion points.

<some-component>Insert me.</some-component>

I could easily memoize this content using the preprocessNode hook, but I have no hook of when I can insert this into the rendered template.

@PrinceAli321
Copy link
Author

Here's an example of where we might want to know if a component has finished rendering:

http://codepen.io/anon/pen/EGgbk?editors=100

When you refresh that page there is a brief flicker while everything loads. Ideally there'd be a way to hide all of the components until everything has finished loading.

@dmr
Copy link

dmr commented Aug 6, 2014

I built some code around this problem because I wanted to test this: Here is a mocha test that demonstrates how it works:

define([
    'knockout',
    'jquery',
    'chai',
], function(
    ko,
    $,
    chai
) {
    'use strict';

    var attempts = 400;

    function test_bindingDone(element) {
        var dfd = $.Deferred();
        function doTest(attempt) {
            if (attempt >= attempts) {
                dfd.reject('timeout');
                return;
            }
            // console.info('attempt', attempt, element.childElementCount);
            var bindingDone = element.childElementCount > 0;
            if (bindingDone) {
                dfd.resolve(element);
                return;
            }
            setTimeout(doTest.bind({}, attempt+1), 1);
        }
        doTest(1);
        return dfd.promise();
    }

    function bindComponentToTestNode(params) {
        var sandbox = document.getElementById('sandbox');
        ko.cleanNode(sandbox);
        sandbox.innerHTML = '<!-- ko component: {name: name, params: params} --><!-- /ko -->';
        var viewModel = {
            name: params.name,
            params: params.params
        };
        ko.applyBindings(viewModel, sandbox);
        if (params.display_sandbox) {
            sandbox.style.display = 'block';
        }
        if (!ko.components.isRegistered(viewModel.name)) {
            throw new Error('Component not registered');
        }
        return test_bindingDone(sandbox)
        .then(function(sandbox) {
            return sandbox.children;
        }, function() {
            sandbox.style.display = 'block';
        });
    }

    var assert = chai.assert;

    describe('sandbox bind', function() {
        it('writes to sandbox and returns element with access to viewModel instance', function(done) {

            function MyViewModel(params) {
                this.name = params.name;
            }

            ko.components.register('test-component', {
                viewModel: MyViewModel,
                template: '<div data-bind="text: name"></div>'
            });

            bindComponentToTestNode({
                name: 'test-component',
                params: {
                    name: 'bla'
                }
            })
            .then(function(return_value) {
                var sandbox = document.getElementById('sandbox');

                // parameters we passed for initialization
                var sandbox_data = ko.dataFor(sandbox);
                assert.equal(sandbox_data.name, 'test-component');

                var first_component_element = sandbox.children[0];
                var component_vm = ko.dataFor(first_component_element);

                // test return value
                assert.equal(return_value, sandbox.children);

                // var proto = Object.getPrototypeOf(component_vm);
                var proto = component_vm.__proto__.constructor;

                assert.equal(proto, MyViewModel);

                assert.equal(
                    first_component_element.outerHTML,
                    '<div data-bind="text: name">bla</div>'
                );

                done();
            });
        });
    });
});

Now I can write tests and "measure" how long it takes. The method test_bindingDone will help you but it's hacky I guess :)

@SteveSanderson
Copy link
Contributor

I agree that something like this would be valuable. We should consider it for KO 3.3+.

@dantrain
Copy link

+1

This could be super useful for page transitions and the like if you are using components for entire pages/panels of your UI.

Maybe something along these lines?

ko.components.register('my-component', {
    viewModel: {
        createViewModel: function(params, componentInfo) {
            $(componentInfo.element).hide();

            return new MyViewModel(params);
        }
    },
    afterBind: function(componentInfo) {
        $(componentInfo.element).slideDown();
    },
    template: ...
});

@mbest mbest added this to the 3.3.0 milestone Aug 26, 2014
@Munawwar
Copy link

Munawwar commented Sep 8, 2014

+1 for this issue. And +1 for @dantrain's idea.
Syntax like:

<my-component data-bind="afterRender: someFunction"></my-component>

would probably mean that a parent view model is listening to 'afterRender' of this child component.

But I think it is useful for a component to know the 'afterRender' of itself. So calling componentViewModel.afterRender() or something is a good solution I guess.

@Excelsior-Charles
Copy link

How about something in the view-model itself that if present gets fired. Similar to Durandal.js compositionComplete lifecycle events so that if the function exists in your viewmodel, it gets called once the component has been bound to the DOM.

Perhaps you can pass in the element to this function so appropriate code would look like such.

viewModel: function(params) {
    // Data: value is either null, 'like', or 'dislike'
    this.chosenValue = params.value;

    // Behaviors
    this.like = function() { this.chosenValue('like'); }.bind(this);
    this.dislike = function() { this.chosenValue('dislike'); }.bind(this);

    this.compositionComplete = function(element) {
        $(element).doSomeJqueryFunction();
    };
}

@Misterhex
Copy link

For anyone who need a workaround with current version v3.2.0, I found this link that helps on stackoverflow.

@richotaylor
Copy link

+1 for some way to get notified when a component and its child components have been loaded. The afterRender mechanism would work but it is fired before sub components are loaded. I just found this question after posting on stack overflow. See that question for more information (if curious).

http://stackoverflow.com/questions/27157350/knockoutjs-afterrender-callback-when-all-nested-components-have-been-rendered

#1533 Also seems to be related.

The existing workaround don't work for child components unfortunately.

@W3Max
Copy link

W3Max commented Feb 11, 2015

+1 for this issue and for @richotaylor comments about child components

@jods4
Copy link

jods4 commented Feb 11, 2015

I solved this problem using an early build of 3.3 and turning all my components synchronous. Other benefits: better perf and less flashing content.

This does not prevent you from dynamically loading component templates, I just do it myself and pass a string to the component declaration. It's only a different way to organize the codebase (and it also makes me think that including require logic inside KO components was a bit of a mistake).

@W3Max
Copy link

W3Max commented Feb 16, 2015

@jods4 Could you share a code example of how you do it? I would really appreciate it!

@jods4
Copy link

jods4 commented Feb 17, 2015

Well, in my project I'm using Typescript with requirejs as AMD loader, but in essence it works like this (Sorry if my js syntax for require is not 100% correct, TS makes this so much easier):

Step 1: be sure to have a KO 3.3 build, otherwise this won't work.

Step 2: define your component.
Key points here: I use requirejs to fetch the template as a string and use the option synchronous: true.

define(['knockout', 'text!template'], function(ko, template) {
  ko.components.register('my-component', {
    viewModel: MyVM,
    template: template,
    synchronous: true
  });
});

Step 3: make sure your component is loaded before any view tries to use it.
You can do as you like, I do so by adding a fake require dependency on any viewmodel that needs a component in its view.

define(['MyComponent'], function() {
  /* Define whatever you want that needs MyComponent to be loaded */
});

@W3Max
Copy link

W3Max commented Feb 19, 2015

@jods4 Thanks! I will look into synchronous: true

@jods4
Copy link

jods4 commented Feb 23, 2015

@W3Max Note that as of today 3.3 is officially released! Look at @rniemeyer blog post for more info: http://www.knockmeout.net/2015/02/knockout-3-3-released.html

@mbest mbest modified the milestones: 3.4.0, Post 3.4.0 Jul 29, 2015
@lzl124631x
Copy link

@jods4 @W3Max I want to note that the first time load still might be aync even if you mark the component with synchronous: true.

If you want to change the policy for a particular component, you can specify synchronous: true on that component’s configuration. Then it might load asynchronously on first use, followed by synchronously on all subsequent uses. If you do this, then you need to account for this changeable behavior in any code that waits for components to load. However, if your component can always be loaded and initialized synchronously, then enabling this option will ensure consistently synchronous behavior. This might be important if you’re using a component within a foreach binding and want to use the afterAdd or afterRender options to do post-processing.

It has been a long way without afterBind... And I'll tell you how I need it when I see you again

@jods4
Copy link

jods4 commented Apr 1, 2016

@lzl124631x This is pretty old stuff but what I did was that I bundled all the templates with my app (using require.js + !text plugin). I required them in the file that created the custom component and passed them as string to KO. -> guaranteed fast sync load.

@lzl124631x
Copy link

@jods4 Shaking hands. I'm using require.js, !text plugin and r.js for AMD, bundling and minifying. All the js and html files are bundled into one js file in my case. So, as you've said, this can ensure me it's a sync load? I'm not sure about that. Have you tested it?

(Just out of curiosity, are you still using require.js? I'm using it but found that it's very difficult to set the paths configs when your project getting bigger and bigger. Things got messed up. I tend to make a shift to webpack which seems more popular now. Have you made the shift?)

@jods4
Copy link

jods4 commented Apr 1, 2016

@lzl124631x I went as far as not letting KO load the template through require but passing it directly as a string.
So my module imports the template into a string as a dependency, then creates the KO component and feeds it the template as a string.
This way I am sure composition is synchronous, even the first time.

Although we are moving away from KO -- which is great but doesn't seem to evolve towards newer web standards -- to Aurelia for newer projects, we still rely on AMD/requirejs at the moment. It is stable and has worked great for us for several years. Webpack, browserify and co. look cool but it seems that there is still a lot of friction and moving parts, which we can't afford here at work. We are considering making the move, but haven't done so... yet. 😉

@gabidi
Copy link

gabidi commented Apr 1, 2016

@gabidi
Copy link

gabidi commented Apr 1, 2016

It allows you to load HTML after the Dom ready event

@jacobslusser
Copy link

I've been using RequireJS and Knockout on large production sites for a couple years now and I've found that the technique suggested by @jods4 is the way to go. Knockout should not have put require support in the box because it just leads to confusion and uncertainty about when a component is loaded.

I got tired though of constantly having to add a dependency in every view model to 'text!<view>' to get the view loaded and add the code suggested by @jods4 for registering the component....

So I put it all into a RequireJS plugin. The relevant plugin snippet is below, but the complete Gist can be found at: https://gist.github.com/jacobslusser/2d9210606a8ab64f3a4df7747ee40404:

// A simple RequireJS plugin that will load a view and view model based on naming convention.
// Once loaded the view is added to the DOM so it can be used with KO templates and the view
// and view model are registered as a KO component.
define('component', ['jquery', 'knockout'], function ($, ko) {
    return {
        load: function (name, parentRequire, onload, config) {
            parentRequire([name, 'text!' + name + '.html'], function (viewModel, view) {

                // Add the view to the DOM
                var template = '<script type="text/template" id="' + name + '">' + view + '</script>';
                $('body').append(template);

                // Register as a KO component
                ko.components.register(name, {
                    viewModel: viewModel,
                    template: { element: name },
                    synchronous: true
                });

                onload(viewModel);
            }, function (err) {
                onload.error(err);
            });
        }
    };
});

NOTE: This plugin assumes that <view>.html and <viewmodel>.js are in the same folder. If they are not, or are in a subfolder, adjust your plugin accordingly.

Put this in it's own file, or as I do, just in my main RequireJS config file and use it like this:

// The component is required using the 'component!' plugin we created. This will load
// 'page1.html' and 'page1.js' and return 'page1.js' to us.
define(['knockout', 'component!page1'], function (ko, Page1) {
    'use strict';

    function App() {

        var args = { name: 'Jacob' };

        // Example of using the component binding with the automatically registered component
        this.sampleComponent = ko.observable({
            name: 'page1',
            params: args
        });

        // Templates can also be used and are probably preferable because we have easy access
        // to the view model if we want to programmatically call anything on the view model.
        // i.e. sampleTemplate().data.yourFunction()
        this.sampleTemplate = ko.observable({
            name: 'page1',
            data: new Page1(args)
        });
    }
    
    ko.applyBindings(new App());
});

When the plugin loads the template it also adds it to the DOM and registers the KO component using the template: { element: template } syntax rather than the string template: template syntax. This has the added benefit that you can use the 'component!' plugin to require a template. So it works for both components and templates! See the example above.

This has had a huge productivity boost in our large RequireJS/Knockout applications. The result is that we typically use the component binding when we don't need to interact with the view model and the params we can pass it through the component binding are simple. If, however, we want to have a reference to the view model and interact with it by calling functions on the view model or something else, we use the template binding which avoids all the nastiness associated with communicating between parent/child view models and offers support for things like the afterRender function.

It's the best of both worlds; it's syntactically easier to follow; and has better performance because the view and view model are required in parallel rather than in serial as it is when you require '<viewmodel>' and then 'text!<view>' (if you're not already bundling your modules).

A more complete example can be found in the afformentioned Gist: https://gist.github.com/jacobslusser/2d9210606a8ab64f3a4df7747ee40404.

Still lovin' Knockout after all these years...

@str
Copy link

str commented Jun 20, 2017

+1 Isn't this possible yet?

@brianmhunt
Copy link
Member

knockout/tko (ko 4 candidate) latest master branch has this.

More specifically, the applyBindings family of functions now return a Promise that resolves when sub-children (including asynchronous ones) are bound.

The API isn't set or documented yet, but the bones have been set up.

@mbest
Copy link
Member

mbest commented Nov 25, 2017

Fixed with #2319.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests