ViewModel: Extend onRendered function

Hey guys,
I’m having a little problem on extending the onRendered function of my ViewModel. For example, I’ve extended my model like this:

Template.videos_card.onCreated(function() {
this.vm = new ViewModel(CardViewModel(this.data));

this.vm.extend({

    changeListener: function () {

      //this is a new function, not defined in the original object
    },

    onRendered: function() {

        //this method is also defined in the original object, I want to extend
       it but it should still execute the onRendered of the original object
    }

});



});

Now I want extend the “onRendered” method of the original one, meaning I want to execute the original onRendered function and also the extended onRendered. How could I do this?

This is convoluted way to go about things but let’s roll with it…

  • You can define multiple onRendered functions on the template. In your case you can move the new onRendered out of the view model.
Template.videos_card.onCreated(function() {
  this.vm = new ViewModel(CardViewModel(this.data));
});

Template.videos_card.onRendered(function() { 
  // Do your stuff
} );
  • You can wrap the original onRendered function:
Template.videos_card.onCreated(function() {
  this.vm = new ViewModel(CardViewModel(this.data));
  var originalOnRendered = this.vm.onRendered;
  this.vm.extend({
      onRendered: function() {
        originalOnRendered();
        // Do your stuff
      }
  });
});
1 Like

Thank you for the answer, but the second example is breaking the function, I’m just getting $ is not defined, this is the part of the original onRendered method:

onRendered: function () {
        var instance = this.templateInstance;
        instance.$(".card").css("opacity", 1);
        instance.$(".card").addClass("animated slideInUp fadeIn");
}
 if (this.isPicture()) {
///....
}

Seems like templateInstance isn’t defined anymore if I use the manual definition way like it is posted in the docs. If I comment it out, the next error is that this.isPicture() isn’t defined.

The error throws if I call originalRender() within the extended version.

this.vm = new ViewModel(CardViewModel(this.data));

var originalRendered = this.vm.onRendered;

//Edit: This one here is working - defined in the extended ViewModel:

    original: originalRendered,
    onRendered: function() {

        this.original();
        this.changeListener()();
        StatusBar.backgroundColorByHexString("#000000");
        window.addEventListener('orientationchange', this.changeListener());

    }
onRendered: function (instance) {
        instance.$(".card").css("opacity", 1);
        instance.$(".card").addClass("animated slideInUp fadeIn");
}

I don’t know where you’re using this.isPicture() but it’s probably outside the context of a view model.

It is part of the original ViewModel, if I do

Template.videos_card.onCreated(function() {
  this.vm = new ViewModel(CardViewModel(this.data));
  console.log(this.vm);
});

…the function isPicture() is there. Seems like im running again into a “this” issue, or? Because with the “original” function within the ViewModel I change’s originalRendered context and then your second way is also working.

Where are you using this.isPicture() that is giving you the error? Show the code.

In my original ViewModel, here:

 CardViewModel = function (data) {
    isPicture: function () {
        if (data.pictureId) return true; else return false;
    },

    onRendered: function () {
        var instance = Template.instance(); //normally, this.templateInstance works...
        instance.$(".card").css("opacity", 1);
        instance.$(".card").addClass("animated slideInUp fadeIn");


        if (this.isPicture()) {
          ......
            });

        }

   
    },
});

Then I add this model to my normal “card”

Template.card.viewmodel(CardViewModel); 

I also have a “video_card” which should extend the original ViewModel:

Template.videos_card.onCreated(function() { 
   this.vm = new ViewModel(CardViewModel(this.data));
   var originalRendered = this.vm.onRendered;
  

  this.vm.extend({
  
    onRendered: function() {

        originalRendered(); //doesn't work
        StatusBar.backgroundColorByHexString("#000000");
 

    }
 });
});

After that, I’m binding it to the Template:

Template.videos_card.onRendered( function() {


this.vm.extend(this.data); //I need it because of the with context, otherwise my document properties are not available
this.vm.bind(this);
this.vm.onRendered();


});

I call the template like this:

   {{#with posting}}
       {{>videos_card}}
   {{/with}}

Posting is a MongoDB document.

yep, you’ve got a problem with this. For option #2 just provide the context:

Template.videos_card.onCreated(function() {
  this.vm = new ViewModel(CardViewModel(this.data));
  var originalOnRendered = this.vm.onRendered;
  this.vm.extend({
      onRendered: function() {
        originalOnRendered.call(this);
        // Do your stuff
      }
  });
});

Thank you, I really run very often into this issue with ViewModel :joy:. I think I will create some examples for myself to use ViewModel the correct way.

With the (very limited) information I have about what you’re doing, I would do something like:

Template.videos_card.viewmodel(function(data){
  var card = CardViewModel(data);
  card.changeListener = function() {
    // something
  };
  return card;
});

Template.videos_card.onRendered(function(){
  var viewmodel = this.viewmodel;
  // Do something else
});

Edit: If you need control over the order in which onRendered functions are called then you have to wrap the original onRendered:

Template.videos_card.viewmodel(function(data){
  var card = CardViewModel(data);
  card.changeListener = function() {
    // something
  };
  originalOnRendered = card.onRendered;
  card.onRendered = function(templateInstance) {
    originalOnRendered.call(this);
    // Do something else
  };
  return card;
});

Hey,
thanks, that really helped me. I’m just remembering that I run also in that “this” issue on my transformation / helper classes. Think we’ve discussed it here ViewModel 2 - A new level of simplicity

Is this problem more complex? I’m just wondering how I can “force” the context of “this”, so that it doesn’t get “overwritten” by ViewModel. Sorry for that beginner questions, but I moved from PHP to JS and never had problems with data contexts :smiley:

I don’t know if this is even a problem. ViewModel is designed so this refers to the current view model under normal situations. That solves a lot of context/scope issues. That said, it’s still JavaScript so you can run into problems with this but that’s not a ViewModel issue, it’s just the way JS works.

In your case you were calling a function (A) that used this from inside another function (B). In that case the context for A is the function B unless you bind it to a different context.

@manuel Now I’ve the case, that some properties of my MongoDB document could be added while the ViewModel has been already generated. If I call my single card again with

{{#with posting}}
{{>videos_card}}
{{/with}}

and I add the property isRecommended:true to the MongoDB posting object, this.isRecommended() isn’t defined in my ViewModel videos_card. It works when I refresh the page or I add the property direct into my Blaze template ({{isRecommended}}). Is there an option to recreate the ViewModel when the data context get’s new properties?

If I understand you correctly, you’re trying to use a property that isn’t there (on the Mongo document). If that’s the case you need to be explicit about it and define the property on the view model for videos_card. That way you can still access the property with the default value of your choosing if the document doesn’t have that property.

Yeah, for the moment it isn’t there, but it is possible that it will be available when a user clicks on “Recommended”. So in that case, the Mongo object get’s updated and now has the property “isRecommended”. So the ViewModel should also get the property when the Mongo object has changed. If I define a default value isRecommended:false in my ViewModel, will it be overwritten when I use {{#with posting}}?

Yep

(and I need twenty characters)

You gave me a good reason for adding properties to the view model when they’re referenced by the markup (similar to what happens in Blaze).

Update to v2.5.0: Properties are automatically added to the view models when used in the markup.

1 Like

Hey @manuel ,
sorry to ask you again, but is it normal that some nested ViewModels aren’t working? Just for example, I’m having 2 templates:

Comments.html:

<div id="comments">
<h1>Comments</h>
<div class="all" data-bind="click:expandAll">
{{#each comments}}
 {{>commenItemt}}
{{/each}}
 </div>
</div>

CommentItem.html

<div class="card" data-bind="text:commentText"></div>

Both of them have ViewModels. Now I get all the time errors like “Property ‘XXXX’ not found on view model”. The problem seems to be, that my template “commentItem” get’s all properties of the parent ViewModel and looses it’s own. So in my example, “commentText” isn’t available anymore, but “expandAll” for a commentItem.

The really annoying thing is, that it works with the account of User 1, but not when I’ve logged in with the account of User 2. I can see all comments with both accounts, but on User 2 all properties of my ViewModel are lost without any error message, except the “property missing” one.

When I remove the ViewModel of “Comments”, it works fine on both accounts.

//Edit: Okay, this behavior happens if we don’t check if our subscriptions already loaded. So this code fixed the issue:

{{#if Template.subscriptionsReady}}
                      {{#each comments}}
                          {{>comment_item}}
                      {{/each}}
{{/if}}

Please make a repro because that should work without waiting for subscriptions. There must be more to this.

@manuel

Didn’t get exactly the same reproduction because my project uses also the package publish-composite to return multiple collections with one subscription, but in the repo above I get also an error like this. I’m having 20 entries in the collection “test”, all of them have a firstname and a lastname (checked via Mongo GUI).

This is what I get on my site:

It is John Doe Only for a test
It is John Doe Only for a test
It is John Doe Only for a test
It is John Doe Only for a test
It is John Doe Only for a test
[...]

I’m getting this error (only once per page reload):

binds.coffee:61 Property 'firstname' not found on view model:
binds.coffee:61 Property 'lastname' not found on view model:
binds.coffee:61 Property 'someText' not found on view model:

The error disappears if I change my “main” template to:

    {{#if Template.subscriptionsReady}}
        {{#each tests }}
            {{>model}}
        {{/each}}
    {{/if}}

I’ll later extend this repo, because on my real project it doesn’t show the binding values, in the example above it works. The only difference on my real project between my 2 users is, that the first one is already subscribed to it’s own comments (during startup), while the second one subscribes when he opens the comments page - and there it is failing without subscriptionsReady.