ViewModel 2 - A new level of simplicity

Hi @manuel , as my apps become more complex, I find that I’m repeating a lot for similar logics. So I’m thinking to generate those repeatitive parts. Sort of like a mini Meteor Kitchen. For each VM, I’m thinking to have one generated code (which won’t be hand touched and will regenerate as needed) + one customizable code.

So the question is, how to properly code VM to achieve that?
Idea is like:

myVM_gen.js

Template.myTemplate.viewmodel({
prop1: function(){

}

)

myVM_customize.js

Template.myTemplate.viewmodel({
prop1: // overwrite or extend the generated prop

})

That’s of course just my guess, probably won’t work for duplicated def. What’s your recommendation?

Unless I misunderstood, that’s exactly why mixin and share exist.

mixin & share were my first thought but seems not quite right …

For instance, I have 10 collections, and each of them has an Edit Form, Item Summary, Item Listing etc… with their respective subscriptions.

Take Edit Form as example, each collection shall has it’s own canSubmit logic against different fields of their own. Currently I hand coded the VMs but the majority of props don’t fit the share or mixin pattern.

I don’t mean to share or reuse (mixin) but to have a proper structure to be able to generate some generic code while be able to customized in another file.

Also those 10 collections might have 10x fields in total and other methods (as props in VM) , seems no good to put them all in ViewModel.mixin / share…

Do you mean something like this?

var vm1 = {

prop1:null,
prop2:null,

onRendered() { //code }

};


var vm2 = _.extend(vm1, {

onRendered() {
  // new come
},

title() {
   //new code
}
});

Template.vm1.viewmodel(vm1);
Template.vm2.viewmodel(vm2);
1 Like

I was thinking about this and you can simplify it further by moving the save button up to arrayItem. That way you get rid of the foo and bar view models…

if (Meteor.isClient) {

  localColl = new Mongo.Collection(null);
  localColl.insert({item:"foo", itemData:{title:"hello moon", alignment: "left"}});
  localColl.insert({item:"bar", itemData:{text:"What's your favorite bar?", chocolate:"Twix"}});
  localColl.insert({item:"foo", itemData:{title:"hello sun", alignment: "top"}});

  Template.demoVM.viewmodel({
    title: "hello world",
    collection() {
      return localColl.find();
    },
    submit(){
      alert("Submit!");
    },
    addFoo(){
      localColl.insert({
        item: 'foo',
        itemData: {
          title: '',
          alignment: ''
        }
      })
    },
    addBar(){
      localColl.insert({
        item: 'bar',
        itemData: {
          text: '',
          chocolate: ''
        }
      })
    }
  });
  
  Template.arrayItem.viewmodel({
    delete() {
      localColl.remove({ _id: this._id() });
    },
    save() {
      localColl.update(this._id(), { $set: { itemData: this.child(this.item()).data() }});
    }
  });
}
<body>
  {{> demoVM}}
</body>

<template name="demoVM">
    <input {{b "value: title"}} type="text">
    <label {{b "text: title"}}></label>
    <br />
    <button {{b "click: addFoo"}}>Add Foo</button>
    <button {{b "click: addBar"}}>Add Bar</button>
    <ul>
    {{#each collection}}
        {{> arrayItem}}
    {{/each}}
    </ul>
    <button {{b "click: submit"}}>submit</button>
</template>

<template name="arrayItem">
  <li>
    <hr />
    <button {{b "click: delete"}}>Del</button>
    <button {{b "click: save"}}>Save</button>
    {{>Template.dynamic template=item data=itemData }}
    <hr />
  </li>
</template>

<template name="foo">
    <div>
        <div>Foo Item:</div>
        <input {{b "value: title"}} type="text">
        <input {{b "value: alignment"}} type="text">
    </div>
</template>

<template name="bar">
    <div>
        <div>Bar Item:</div>
        <input {{b "value: text"}} type="text">
        <input {{b "value: chocolate"}} type="text">
    </div>
</template>
1 Like

That’s quite close. But your suggestion creates two VMs, right? Does the new code work for vm1?

Or the question to ask is: How do I make change or add new code to ONE constructed VM that applies to ONE Template instance?

Replacing could be done like::
ViewModel.fineOne('vm1').prop1 = function(){ ... }
Extending prop1 seems to be a general JS question. Referencing the existing prop1 in new code will cause Max stack size issue …

Since prop1 maybe already being used in template like {{prop1}}

Idea is like:
ViewModel.fineOne('vm1').prop1 = function(){ // do what original prop1 does // add extra action // return something different ... }

Hey, @manuel, ve just seen that you’ve made attr binding case sensitive! Use cases?

That’s for svg which is case sensitive.

Forgot name of the pattern but you need to grab the original function A and the second one B and wrap them in a new one C that calls A and B. You then use the load method to add C to the view model. That way A and C can have the same name.

Do you mean something like the following ?

ViewModel.findOne('vm1').a = function() { return 'Apple' }; ViewModel.findOne('vm1').b = function() { return 'Banana' }; c = function() { return ViewModel.findOne('vm1').a() + ViewModel.findOne('vm1').b() }; ViewModel.findOne('vm1').load({a: c});
Just tried on console. Loading okay. But
ViewModel.findOne('vm1').a()
has same error recursive: Maximum call stack size exceeded

@manuel thanks for “esa vaina” (translation - this thing) :slight_smile: You have made our lives so much easier with this. Pure view model objects that are easily bindable and unit tested. We love it, have been using it for quite some time, and you have cut our development times significantly.

1 Like

Ronen! Good to hear from you. I’m happy esta vaina is useful to you. Right now I’m working on one that uses React instead of Blaze. I’m sorry we couldn’t meet in Cabarete. Maybe next time =)

1 Like

Here’s the direction I thought you wanted to take:

  ViewModel.mixin({
    wrapper: {
      wrap(obj) {
        const toLoad = {};
        for(prop in obj) {
          if(this[prop]){
            prevFunc = this[prop].bind(this);
            newFunc = obj[prop].bind(this);
            toLoad[prop] = function(...args) {
              prevFunc(...args);
              newFunc(...args);
            }
          } else {
            toLoad[prop] = obj[prop];
          }
        }
        this.load(toLoad);
      }
    }
  });

  Template.A.viewmodel({
    mixin: 'wrapper',
    onCreated(){
      this.wrap({
        sayHello() {
          console.log("Hello from Another")
        }
      });
    },
    sayHello() {
      console.log("Hello from A");
    }
  });
<body>
{{> A}}
</body>

<template name="A">

</template>

So you can type in console

> ViewModel.findOne("A").sayHello()

and get:

Hello from A
Hello from Another
1 Like

Great direction! Shall try how to fit that in my code gen. Thanks.

Hey @manuel, Nice 1! Thats much better thanks :smiley:

New documentation site: viewmodel.org

2 Likes

I wonder if there is a way to use style binding with pseudo selector like :before?

Also it would be handy if there was a DRY way to add same method to multiple bindings, like this: {{b "focus, hover: doSmth" }}

Inline styles cannot be used to target pseudo-classes or pseudo-elements. The style binding is essentially an inline style. You need to apply the style directly on the element or use a style sheet.

As for 1 method used by multiple bindings, I can’t quite put my finger on it but it rubs me the wrong way, maybe because it’s adding to the API for little benefit, or maybe because I rarely (if ever) encounter that scenario.

In the React example ReactMeteorData isn’t being used, so that example can actually be a lot shorter.