ViewModel 2 - A new level of simplicity

If you’re not familiar with the ViewModel library you can think of it as Angular without the ceremonies.

Version 2 is all around nicer and more polished. It’s just out of the oven so I expect a few wrinkles to pop up as people start using it but it should be ok. Topics of interest include:

Go to viewmodel.meteor.com for more information.

And of course it still takes half the code to get things done…

Here’s a greeting box with ViewModel:

<template name="greeting">

  <label>First Name</label>
  <input {{b "value: firstName"}} type="text">

  <label>Last Name</label>
  <input {{b "value: lastName"}} type="text">

  <input {{b "check: agree"}} type="checkbox" >
  <label {{b "text: 'My name is: ' + fullName"}}></label>

  <button {{b "enable: canGreet, click: greet"}}>Greet</button>
  <button {{b "click: clear"}}>Clear</button>

</template>
Template.greeting.viewmodel({
  firstName: '',
  lastName: '',
  agree: false,
  fullName: function() {
    return this.firstName() + ' ' + this.lastName();
  },
  greet: function() {
    alert( "Hello " + this.fullName() );
  },
  canGreet: function() {
    return this.firstName() && this.lastName() && this.agree();
  },
  clear: function() {
    this.reset();
  }
});

Here’s the same component with Blaze:

<template name="greeting">

  <label>First Name</label>
  <input id="firstName" type="text" value="{{firstName}}">

  <label>Last Name</label>
  <input id="lastName" type="text" value="{{lastName}}">

  <input id="agree" type="checkbox" checked={{agree}} >
  <label>My name is: {{fullName}}</label>

  <button id="greet" disabled="{{greetDisabled}}">Greet</button>
  <button id="clear">Clear</button>

</template>
Template.greeting.onCreated(function() {
  var state = new ReactiveDict("greeting");
  this.state = state;
  this.fullName = function() {
    return state.get("firstName") + ' ' + state.get("lastName");
  }
});

Template.greeting.helpers({
  firstName: function() {
    return Template.instance().state.get("firstName");
  },
  lastName: function() {
    return Template.instance().state.get("lastName");
  },
  fullName: function() {
    return Template.instance().fullName();
  },
  agree: function(){
    return Template.instance().state.get("agree");
  },
  greetDisabled: function () {
    var state = Template.instance().state;
    return !state.get("firstName") || !state.get("lastName") || !state.get("agree");
  }
});

Template.greeting.events({
  'input #firstName': function(event, template) {
    template.state.set("firstName", $("#firstName").val());
  },
  'input #lastName': function(event, template) {
    template.state.set("lastName", $("#lastName").val());
  },
  'change #agree': function(event, template) {
    template.state.set("agree", $("#agree").is(":checked"));
  },
  'click #clear': function(event, template) {
    template.state.set("firstName", '');
    template.state.set("lastName", '');
    template.state.set("agree", false);
  },
  'click #greet': function(event, template) {
    alert( "Hello " + template.fullName() );
  }
});

And the same component with React:

Greeting = React.createClass({
  dict: new ReactiveDict("greeting"),
  getInitialState: function() {
    return {
      firstName: '',
      lastName: '',
      agree: false
    }
  },
  componentDidMount: function() {
    this.setState({
      firstName: this.dict.get("firstName"),
      lastName: this.dict.get("lastName"),
      agree: this.dict.get("agree")
    });
  },
  updateFirstName: function (e) {
    var firstName = e.target.value;
    this.dict.set("firstName", firstName);
    this.setState({ firstName: firstName });
  },
  updateLastName: function (e) {
    var lastName = e.target.value;
    this.dict.set("lastName", lastName);
    this.setState({ lastName: lastName });
  },
  updateAgree: function (e) {
    var agree = e.target.checked;
    this.dict.set("agree", agree);
    this.setState({ agree: agree });
  },
  clear: function(e) {
    e.preventDefault();
    this.setState({
      firstName: '',
      lastName: '',
      agree: false
    });
  },
  fullName: function () {
    return (this.state.firstName || '') + ' ' + (this.state.lastName || '');
  },
  greet: function(e){
    e.preventDefault();
    alert( "Hello " + this.fullName() );
  },
  greetDisabled: function() {
    return !this.state.firstName || !this.state.lastName || !this.state.agree;
  },
  render() {
    return (
      <form className="ui form">

        <label>First Name</label>
        <input type="text" onChange={this.updateFirstName} value={this.state.firstName} />

        <label>Last Name</label>
        <input type="text" onChange={this.updateLastName} value={this.state.lastName} />

        <input type="checkbox" onChange={this.updateAgree} checked={this.state.agree}/>
        <label>My name is: {this.fullName()}</label>

        <button onClick={this.greet} disabled={this.greetDisabled()} >Greet</button>
        <button onClick={this.clear} >Clear</button>

      </form>
    );
  }
});
44 Likes

@manuel Thanks for the great work. I’m using v1 and it is a pleasure to work with :slight_smile:

1 Like

I think you are deserving of far more accolades than you are getting. This work gets lost in the shuffle, I fear, as everyone figures out which life raft they want to jump on with the announcements about Blaze, which is still the dominant Meteor front-end as we speak. As long as you are using Blaze, and don’t need SSR, viewmodel has always been the solution with least boilerplate and least ceremony of all the solutions.

Manuel’s commitment to a well-documented site, to responding to issues from the community, and to continuous improvement makes him an exceptional community member.

I think Telescope should use viewmodel - I think Blaze should encourage it - I think it should be the goto for Meteor for the future. I know I’m biased, coming from knockoutjs as my first reactive framework, but I’m pretty sure that by any measure of complexity of code you can think of, writing in viewmodel will lead to less head-scratching, less documentation searching (he’s got a great doc site BTW).

I lament the fact that SSR, and the ability to do React Native, pushes me to React for the moment. But Meteor+Blaze+ViewModel2 is definitely the least-friction onboarding for new developers. Simplicity first, popularity contests later.

Thanks for your work, Manuel!

20 Likes

Very very nice. I’m a big fan of ViewModel.

Some questions:

  • is this backwards compatible because I see you simplified the syntax?
  • did only the that syntax change, or did you add features?
  • did you make any other improvements?

a changelog would be great to answer questions like that.

1 Like

I agree with @deanius. This is great stuff, @manuel. I’ve overheard core devs speaking highly of your work :smile:

4 Likes

Manuel, at first glance I have to say I do like it. I’m gonna dive into it deeper, but real quick it’s all about the “2 way binding” thing right?

My conclusion with that is that it’s always been more glitz than actual punch. In reality, you don’t need the binding back. you don’t need your model populated on every keyup. You want to call one function at least to trigger saving its contents to mongo, right–so that one call can extract all the values from the form right then and save it. And of course you can optionally validate and perform various transforms from there. But the point is that in actuality there is very little difference between the model being bound on keyup and when you tell it so. It looks good in demos though! Am I wrong? What more abstractly/generally would you say are the biggest selling points to View Model?? Anything to help me wrap my head around it would be much appreciated.

1 Like

also what’s the input event?

'input #firstName': function(event, template) {
    template.state.set("firstName", $("#firstName").val());
  },

I feel like im having amnesia or something lol–there is no input event! …i assume its a shorthand for firing on all events–never heard of that, how long has that been in Blaze? Is that something somehow from jQuery too?

Great I am going to test if it will be compatible with our other Blaze packages. I am looking for an Angular replacement with data binding. Angular in Meteor is not compatible with other packages. Keep good working.

@deanius @rdickert @satya

Thank you for the kind words. I really appreciate it.

@satya

I added a what’s new section to the documentation.

is this backwards compatible because I see you simplified the syntax?

It is not backwards compatible. The library would have ended up a monster had I included the new features and still be backwards compatible. Another reason is that v1 required a monkey patch for Meteor. I got chills every time there was a new Meteor release. That’s gone now.

did only the that syntax change, or did you add features?

It has a bunch of new little things here and there which makes a much nicer experience for the developer. An example is much better errors. You now get something like:

The view model for template ‘person’ doesn’t have a ‘names’ property. See https://viewmodel.meteor.com/docs/viewmodels#defining for more information.

Another is how you don’t have to name your view models for them to persist across hot code pushes and save the state on the url.

did you make any other improvements?

It is much faster in terms of binding the elements and updating the view models. That’s because I did away with a ton of legacy stuff which nobody used (like being able to bind a view model to any element, not just a template).

@faceyspacey

input is a standard event: https://developer.mozilla.org/en-US/docs/Web/Events/input

Regarding 2 way binding. If you look at any React example which has input elements, you will find that they’re doing 2 way binding manually (since React doesn’t provide it). They listen for changes, manually update the state of the component, and do the 1-way bind on the template. Angular 2 got “rid” of two-way binding but ended up providing developers with a shortcut to do just that. The reason is simple, it is a common pattern to simplify the way you deal with element values.

I was going to define view models, how they work, why they make life so much easier, etc. but I think it’s better if you experience it yourself. Take a relatively small part of one of your websites (a template with blaze, template/controller with Angular, or component with React) and re-write it using ViewModel. Don’t pick something so small that there’s no code to back the view, and don’t pick something so big that it feels daunting. The ViewModel version will have less code and it will be simpler to read, write and reason about.

@charliecl

I haven’t tested v2 with other packages yet but it should work just fine with pretty much everything else (autoforms, easysearch, etc.) It makes life easier for javascript controls too. You can reference the html elements as part of the view models. For example:

<template name="example">
  <input type="text" {{b "ref: nameInput"}}>
</template>
Template.example.viewmodel({
  onRendered: function() {
    this.nameInput.makeFancy();
  }
})

Look Ma! No id on the html element or html references in my code!

4 Likes

Hi @manuel,

I really like your VM. But how do you write the new binding syntax in Jade?

I just tried guessing your example in the doc like:

  button('{{b "enable: canGreet, click: greet"}}') Greet

But it’s illegal .jade

Not to mention more quote pairs like:

  <label {{b "text: 'My name is: ' + fullName"}}></label>

Don’t want to lose the simplicity of Jade. Thanks. :smile:

2 Likes

After a bit of research, there’s actually a solution to replace the most used jade compiler: https://github.com/dalgard/meteor-jade

such that I could write the following .jade to reproduce the example in doc:

template(name="greeting")
  label First Name
  input(type="text" $b="value: firstName")

  label Last Name
  input(type="text" $b="value: lastName")

  input(type="checkbox" $b="check: agree")
  label($b="text: 'My name is: ' + fullName")

  button($b="enable: canGreet, click: greet") Greet
  button($b="click: clear") Clear

Perfect! :sunny:

3 Likes

To avoid conflict string to tag, I usually used single quote. Can I apply single quote in ref below?

<template name='example'>
  <input type='text' {{b 'ref: nameInput'}}>
</template>

Sure, its just javascript =)

Hello, Manuel, thank you for all the hard work!
So does v2 work with jade?

Sure, look at daveeel’s post up there

@avalanche1 I use it both with Jade and CoffeeScript and it works perfect.

2 Likes

Hmmm, and will it be compatible with ‘Blaze2’?

Looks interesting, but I don’t want to stick with any package which will be deprecated in future because it’s depends on Blaze1.

Unfortunately MDG has created this situation where no one knows what’s going to happen and everyone is looking for a life raft (as deanius said). Even worse is that there is no time frame for any of this. The transition to React could happen in a month or two or maybe a year from now. No one knows.

But since when did the lack of facts and figures stopped us from speculating? So let’s do just that!

Based on a complete lack of information I hereby predict that MDG will provide a way to transpile templates (and the code behind) into React components. This project will work beautifully at the beginning. The project will decline as Meteor continues to push React as the canonical way of working with Meteor. Within a year it will be clear that the project isn’t receiving any love because it’s just there for legacy purposes. Then while you could technically write your views with templates, it would be clear that React is the new Meteor way of doing things.

Getting more serious…

Where does that leave ViewModel?

ViewModel will continue working as long as there’s a way to transpile templates into React components. So ViewModel will continue working as long as MDG maintains Blaze 2 (or whatever the transpiler is called).

8 Likes

What I read is that ViewModel pre-defined many events for form processing. So what about new events such as touch screen or remote controller? An example is swipe event in gesture recognizer.

Do you have a simple echo input example like the one in Angular? That is input text in a text box and instant displayed as you input under the box. That is a typical hello example of Angular.