26

afterRender works with template bindings, but after converting my templates to components, there does not seem to be any way to use afterRender. I have tried looking for an example of a component that uses afterRender, but cannot find anything.

user247702
  • 23,641
  • 15
  • 110
  • 157
Eric Kolotyluk
  • 1,958
  • 2
  • 21
  • 30
  • please provide source, maybe in a JSFiddle – Michael Crook Oct 07 '14 at 20:37
  • Looking at the source, it doesn't look like it does. It doesn't use the template engine to render the components, it just merely applies the component model to the component template. – Jeff Mercado Oct 07 '14 at 21:46
  • OK, I am not surprised, components are pretty new, as I had to upgrade to the latest release of Knockout to get them to work. I looks like I may have to understand how createViewModel(params, componentInfo) works. – Eric Kolotyluk Oct 09 '14 at 04:32
  • Sorry, didn't mean to downvote, don't know how that happened... – JakeJ Aug 28 '19 at 14:52

5 Answers5

40

I could not get the method working as per the above post. However i found a workaround on the git issue list and it doesn't require a custom KO Binding.

Add the below line in your component template html or string of code.

 <span data-bind="template: { afterRender: init }"></span>

Then create a init function in your module / viewModel:

 this.init = function() {
   Do cool DOM stuff here.
}

or depending on your viewModel structure:

viewModel: function(params) {
    return {
        init: function () {

        }
    };
},

Works like a charm. Example of it working is here

http://jsfiddle.net/gLcfxkv6/1/

Thread on knockout git here: https://github.com/knockout/knockout/issues/1533

Thanks to vamps on git for the workaround.

Piotr Stulinski
  • 9,241
  • 8
  • 31
  • 46
8

The secret here is http://knockoutjs.com/documentation/custom-bindings.html

ko.bindingHandlers.myCustomBinding = {
  init: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // This will be called when the binding is first applied to an element
    // Set up any initial state, event handlers, etc. here
    if (bindingContext.$data.init) bindingContext.$data.init(element, valueAccessor, allBindings, viewModel, bindingContext);
  },
  update: function(element, valueAccessor, allBindings, viewModel, bindingContext) {
    // This will be called once when the binding is first applied to an element,
    // and again whenever any observables/computeds that are accessed change
    // Update the DOM element based on the supplied values here.
    if (bindingContext.$data.update) bindingContext.$data.update(element, valueAccessor, allBindings, viewModel, bindingContext);
  }
};

so in my component template I do something like

<div class="row-fluid" data-bind="myCustomBinding: 'someValue'">

and on the component viewModel I just implement init and/or update, for example:

constructor.prototype.init = function(element, valueAccessor, allBindings, viewModel, bindingContext) {
  // All the buttons in the buttons group need the same name,
  // but they all need distinct ids. We use timestamps because
  // as components, the names and ids should be distinct across
  // multiple instances of each component.
  var timeStamp = new Date().getTime();
  $('input:radio').attr('name', timeStamp).button();
  $('input:radio:eq(0)').attr('id', timeStamp+1);
  $('input:radio:eq(1)').attr('id', timeStamp+2);
  $('input:radio:eq(2)').attr('id', timeStamp+3);

  // Initialize the number-picker
  $('input[name="number-picker"]').TouchSpin();
};

The Knockout documentation could be improved by pointing out this very useful case. Also, this is such a useful binding, there should be a standard bindings for 'init' and 'update', for example

<div data-bind="init: 'someValue'">
Eric Kolotyluk
  • 1,958
  • 2
  • 21
  • 30
  • Hi, i tried to use this but i was not able to get it working. I can see the code executing but it simply doesnt affect the dom elements... so i cannot get for example a telephone number picker to execute :( – Piotr Stulinski Nov 02 '14 at 17:23
6

We needed to access DOM elements in a component after switching between different components. We would have liked to use the non-existing "afterRender" binding on components.

We solved it with a Javascript setTimeout, letting KO do its rendering first, and in effect queueing our code after that.

HTML:

<div data-bind="component: compName"></div>

The code switching the component:

var compName = ko.observable();

//...

compName(switchToComponent);
setTimeout(function(){
    // this code is queued until after the component is rendered.
}, 0);
Arjan Einbu
  • 13,543
  • 2
  • 56
  • 59
3

As of knockout 3.5.1, you can add a koDescendantsComplete function to your viewModeland that will be triggered after the rendering is complete

var viewModel = {
    koDescendantsComplete: element => {
        console.log( 'Rendered!', element );
    }
}

See: https://github.com/knockout/knockout/blob/2db9f7f79939ed289621de72340ab048362ed76b/src/components/componentBinding.js#L73

  • This is (as of 3.5.1) by far the most suitable solution for components. As a bonus, the dom node the component is bound to is passed as a first argument. Allowing for convenient listener registration, etc. – Eelke van den Bos Jun 04 '21 at 01:02
1

Make a new binding, like so

ko.bindingHandlers.initBinding = {
init: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    debugger
    if (valueAccessor() && valueAccessor().afterRender && bindingContext.$data) {
        valueAccessor().afterRender(bindingContext.$data);
    }
},
update: function (element, valueAccessor, allBindings, viewModel, bindingContext) {
    }
};

export default ko.bindingHandlers.initBinding

You don't need the export if you're not using ES6(babel etc).

Then in your component html, you can do something like this

<div class="staff-directory" data-bind="initBinding: {afterRender: afterRender }">
    <p>Loaded</p>
</div>

And in your component model

class staffDirectory {

    constructor() {
        console.log('staff directory loaded');
    }

    afterRender() {
        console.log('afterRender called');
    }

}

export default staffDirectory;
Ryan Mann
  • 5,178
  • 32
  • 42