Thursday, April 9, 2015

UI-less Polymer Elements as the Model in MVVM


Last night's attempt at MVVM (Model-View-ViewModel) in Polymer did not quite work out the way I hoped.

I am trying to rework the very simple <hello-you> Polymer element used in a couple of Patterns in Polymer chapters:



I had hoped that the simplicity would lead to a quick solution on which I could build something more complex. I found myself stuck on the Model. I think the class is simple enough. To support the element as-is, the most basic of objects ought to suffice:
function Person(name) {
  this.name = name ? name : '';
}
What tripped me up was marking name observable. If a human introduces themselves, the Person object should have its name property updated and the View should be able to observe that change so that it can update itself with the new name.

Last night's solution was to shelve the idea of a Person object and use a plain old Object instead. That works fine in Polymer since Polymer can observe plain old objects out of the box—its properties are automatically observable. That solves my simple case, but what about a more complex object with a prototype and methods?

I can think of at least three approaches that might work for this. Tonight, I try what I hope is the easiest: making the Model a UI-less Polymer element:
Polymer({
  is: 'x-person',
  properties: {
    name: {
      type: String,
      value: '',
      observer: 'nameChanged'
    }
  },

  nameChanged: function(v, old) {
    console.log(v + ' ' + old)
  }
});
This should give me an <x-person> element that I can use inside of <hello-you> to store the name of the Person being greeted: <x-person name="Chris"/>. I declare the UI-less template as:
<polymer-element name="x-person">
  <script src="x_person.js"></script>
</polymer-element>
I import this into the <hello-you> element with a <link> import:
<link rel="import" href="../bower_components/polymer/polymer.html">
<link rel="import" href="x-person.html">
<polymer-element name="hello-you">
  <template>
    <!-- Template HTML here ... -->
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
With that, I should be able to use my <x-person> “Model” in the ViewModel (<hello-you>'s backing class) and the View (<hello-you>'s template).

The easiest (but possibly not the best for this pattern) way to bind <hello-you>'s <input> to the name property of <x-person> is in the template/view:
<polymer-element name="hello-you">
  <x-person name="{{name}}"></x-person>
  <template>
    <h2>Hello <span>{{name}}</span></h2>
    <p>
      <input value="{{name::input}}">
    </p>
    <!-- ... -->
  </template>
  <script src="hello_you.js"></script>
</polymer-element>
That is probably not quite right for MVVM since three items (<x-person>, the <h2> title, and the <input>) bind to a local name variable instead of a ViewModel property. I will worry about that later. For now, I have the ViewModel worrying about presentation (from last night) and a Model worrying about the “business” of naming.

Only this does not work.

When I update the <input>, the change observer that I logged in <x-person> is not seeing any changes. This turns out to be caused by placing <x-person> outside of <hello-you>'s <template> tag. The fix for this is easy enough, I move it inside of the <template>:
  <template>
    <x-person name="{{name}}"></x-person>
    <h2>Hello <span>{{name}}</span></h2>
    <p>
      <input value="{{name::input}}">
    </p>
    <!-- ... -->
  </template>
To complete tonight's efforts, I would like to drive the most basic of business logic in the Model: setting a default value. This is accomplished in Polymer 0.8 with a value property:
Polymer({
  is: 'x-person',
  properties: {
    name: {
      type: String,
      value: 'Bob',
      observer: 'nameChanged'
    }
  },
  // ...
});
But when I load up <hello-you>, I am not seeing “Bob”:



This is due to the lack of change notification from the Model. For a host element like <hello-you> to see changes to attributes of child elements, the child has to specify that the property generates notifications. This is done with the notify property:
Polymer({
  is: 'x-person',
  properties: {
    name: {
      type: String,
      value: 'Bob',
      notify: true,
      observer: 'nameChanged'
    }
  },
  // ...
});
Now, when I load my element, the name defaults to “Bob”:



I am unsure if this binding more properly belongs in the ViewModel than in the View as I have done here. For tonight, I claim a small victory in the form of an observable Model. It is clear that I need to continue my investigation into MVVM as I still do not have a complete handle on it.


Day #24

No comments:

Post a Comment