Namek Dev
a developer's log
NamekDev

Binding with rivets.js - details and tricks

May 24, 2017

This is the 2nd part of knowledge about rivets.js - a small JS library that binds data in web. While the first one was an introduction, this one treats about the details.

Part 1: My small secret web weapon to bind things - rivets.js

Version!

First of all, use the latest version. The main website is not always up to date so go straight to rivets.bundled.js on GitHub and use that.

In time of writing this, there is one bugfix that tackles the bug where $ variable would be always treated as it was jQuery and failing to run.

→ the bug: issue #646

→ the CHANGELOG

There is also an interesting fork made by blikblum.

Using components

Component in this library is a definition of template! Unlike the rest of library it doesn’t just bind things to existing DOM but creates the DOM based on your template.

Let’s have a scope with a value that will be passed to some component:

let scope = {
  myCmpData: {
    title: "some title"
  }
}

Then, the definition of a component:

rivets.components['my-component'] = {
  template: () => "
    <div>The title: {data.title}</div>
  ",

  initialize: (el, attrs) => {
    return { data: attrs.data }
  }
}

How do we use this component?

<div id="app">
  {myCmpData.title}
  <my-component data="myCmpData"></my-component>
</div>

The <my-component> element will be automatically translated into template: <div>The title: {data.title}</div>.

It’s also worth to mention the data variable here. It’s defined in component’s initialize(el, attrs) method and then used in the template. I have defined it here due to certain bug that will be discussed in the chapter “Nested scopes don’t refresh?”

Don’t store template in JavaScript code

Instead of defining:

template: () => "
  <div>The title: {data.title}</div>
",

you can generate the template in <template> tag (with PHP or whatever backend language):

<template id="my-component-template">
  <div style="border:1px solid steelblue">
    The title: {data.title}
  </div>
</template>

then, access the template by id:

template: () => getElementById("my-component-template").innerHTML,

The <template> tag is safe since it will be ignored by browser renderer. But if you’re still afraid that it may display in older browsers then put it into <head> of a page.

Live example

Straight to live example of this component: → rivets.js: component usage and update

If you have some questions about components you may want to read this topic: → Issue #691

Formatters!

Typically, formatter is used to… format data! For example, making sure that value is displayed in a certain way:

rivets.formatters.money = value => {
  let v1 = Number(value).toFixed(0)
  let v2 = Number(value).toFixed(2)
  return Number(v1) == Number(v2) ? v1 : v2
}

or:

rivets.formatters.capitalize = str => {
  let lower = str.toLocaleLowerCase()
  return lower.substr(0, 1).toLocaleUpperCase() + lower.substr(1)
}

I’ll show you few more.

Call functions only explicitly

If you pass some function as a value to binder it will be treated just as value. In older rivets versions, if some binder like rv-value received a value which was instanceof Function then it would be immediately called. Now, in rivets 0.9.0 it’s turned off by default but can be enabled back for legacy code:

rivets.configure({
  bindingAutoexecuteFunctions: true
})

If you do need to call such function you’ll need to use the call  formatter:

<div rv-value="vm.func | call"></div>

Older versions (below 0.9.0) needed a formatter which is now built-in:

rivets.formatters.call = function(fn) {
  return fn.apply(null, Array.prototype.slice.call(arguments, 1))
}

Note: this rule does not apply to rv-on-* binder, e.g. with rv-on-click="notify" the notify function will be called on each click. You don’t need the call formatter here!

→ rivets.js documentation: Call functions

Is a value one of list’s?

I wanted to check if value is any of specific values that are enlisted statically:

<div
  class="item-settings-row"
  rv-if="item.type | anyOf 'Shirt,Jacket,Hoodie'"
>
</div>

And here’s anyOf formatter that supports it:

rivets.formatters.anyOf = (value, arrayAsStr) => {
  return IndexOf(arrayAsStr.split(','), value, true) >= 0
}

where IndexOf is a special implementation of mine, the code itself should speak for itself:

// because IE8 doesn't support it natively
function IndexOf(array, element, ignoreTypeCheck = false) {
  for (var i = 0, n = array.length; i < n; ++i) {
    if (ignoreTypeCheck && element == array[i] || !ignoreTypeCheck && element === array[i]) {
      return i;
    }
  }

  return -1;
}

Get property of object

Since JavaScript expressions are not valid in binders you can’t simply go with:

rv-value="someObject['someProperty']"

Instead, you’ll need a formatter:

rivets.formatters.prop = (obj, propName) => obj[propName]

that will allow you to access properties:

rv-value="someObject | prop 'someProperty'"

Get indexed element

Same as with previous:

rivets.formatters.index = (arr, index) => arr[index]

With this someArray[15] would be accessed with:

rv-value="someArray | index 15"

Custom binders

If you’ve used AngularJS then you may be familiar with a concept of directives. In rivets you may treat binder as a modification of DOM elements without using aconcept of components.

A dropdown

Here’s one of my custom binders:

rivets.binders['bind-dropdown'] = {
  bind: function(el) {
    let adapter = rivets.adapters[(<any>this).observer.key.i]
    let model = this.model
    let keypath = (<any>this).observer.key.path

    this.onChangeCallback = function(evt) {
      adapter.set(model, keypath, evt.newValue)
    }

    let currentOptionEl = $d(el, '.dropdown-current-option')

    let defaultValue = adapter.get(model, keypath)
    if (!!defaultValue) {
      currentOptionEl.setAttribute('data-option-value', defaultValue)
    }
    else {
      defaultValue = currentOptionEl.getAttribute('data-option-value')
      adapter.set(model, keypath, defaultValue)
    }

    $(el).on('change', this.onChangeCallback)
  },

  unbind: function(el) {
    $.off(this.onChangeCallback)
  },

  routine: function(el, value) {
    if (value) {
      $(el).trigger('set_value', {newValue: value})
    }
  }
}

and usage:

<div class="dropdown color-selector with-names small" rv-bind-dropdown="model.badgeColor">
  <div class="dropdown-current-option"
     data-option-value="white">
    <div style="background:white" class="circled"></div><span>White</span>
  </div>
  <div class="dropdown-options">
    <div data-option-value="white"><div style="background:white" class="circled"></div><span>White</span></div>
    <div data-option-value="black"><div style="background:black"></div><span>Black</span></div>
    <div data-option-value="red"><div style="background:red"></div><span>Red</span></div>
    <div data-option-value="royalblue"><div style="background:royalblue"></div><span>Royalblue</span></div>
    <div data-option-value="navy"><div style="background:navy"></div><span>Navy</span></div>
    <div data-option-value="yellow"><div style="background:yellow" class="circled"></div><span>Yellow</span></div>
    <div data-option-value="green"><div style="background:green"></div><span>Green</span></div>
    <div data-option-value="purple"><div style="background:purple"></div><span>Purple</span></div>
    <div data-option-value="maroon"><div style="background:maroon"></div><span>Maroon</span></div>
    <div data-option-value="orange"><div style="background:orange"></div><span>Orange</span></div>
    <div data-option-value="gold"><div style="background:gold"></div><span>Gold</span></div>
    <div data-option-value="gray"><div style="background:gray"></div><span>Gray</span></div>
  </div>
</div>

Notice the model.badgeColor that is passed as an argument to binder.

rv-on-* with event cancellation

//  listen on event and immediately stop it's propagation
rivets.binders['on-*-stop'] = {
  "function": true,

  priority: rivets.binders['on-*'].priority + 1,

  unbind: function(el) {
    if (this.handler) {
      $.off(this.handler)
      this.handler = null
    }
  },

  routine: function(el, value) {
    let self = this
    if (this.handler) {
      $.off(this.handler)
    }
    $(el).on(self.args[0], self.handler = self.eventHandler(value))
  }
}

Usage is similiar to the original rv-on-*:

<div rv-on-click-stop="notify"></div>

Warn me about unknown binders

Here’s a trick I like to have on board. In my opinion this small modification should be built in.

Instead of allowing to define binding for custom attributes like rv-disabled for checkbox you should be only allow to define it with attr prefix - rv-attr-disabled. Why? Just to be sure that your custom bindings are not treated as attributes! Thus, we will overwrite custom rv-*  binder and introduce rv-attr-*.

// replace '*' binder with 'attr-*' for improved readability in HTML + finding wrong bindings
rivets.binders['*'] = function() {
  console.warn("Unknown binder: " + this.type);
}

rivets.binders['attr-*'] = function(el, value) {
  var attrToSet = this.type.substring(this.type.indexOf('-')+1)

  if (value || value === 0) {
    el.setAttribute(attrToSet, value);
  }
  else {
    el.removeAttribute(attrToSet);
  }
}

Now, we can easily bind every single attribute without looking at built-in binder list.

Nested scopes don’t refresh?

There is an issue about internal registration on correct scope. Thus, nested scope may break since they don’t always detect changes in model.

This fiddle shows the problem and this one workarounds it.

The problem seems to be about primitive values like numbers or strings. The easy workaround is to just put everything into an object so rivets can properly react to the changes in the scope.

So, instead of binding this:

rivets.bind(appEl, model)

bind this way:

rivets.bind(appEl, { vm: model })

so primitives will be accessed with rv-if="vm.someValue" `.

This was reported here: → GH issue #512: nested rv-if binders mess

and here, where it’s more investigated: → GH issue #486: rv-each loses reference to parent view model

and potentially fixed in: → commit in a rivets fork made by blikblum

Nuance about this keyword: class vs object

In C#, Java, C++ or other languages this keyword always references to the instance of a class when used in methods. JavaScript is different. Few years ago it didn’t have a mention of classes. JavaScript is all about functions and prototypes. Function can be called in a context and this is what this references to - call context.

To know more about calling a function in chosen context read the manual about the Function.prototype.apply() function. In fact, that’s what rv-on-* binder uses internally.

Let’s have a template which can call a function on click:

<div id="app">
  <div
    rv-on-click="shoot"
  >some text to click</div>
</div>

Now, let’s create a model (root scope) to bind with <div id="app">:

rivets.bind(
  document.getElementById("app"),
  model
)

Then, let’s deal with the model. If you want to define your model in a class then you can do this in several ways. Let’s start with TypeScript since it has some syntax that ES6 doesn’t:

class AppController {
  text: string = "some value"

  constructor() {
    // it's not needed but let's you compare it with next snippet easier
  }

  shoot = () => {
    console.log(this)
  }
}

model = new AppController()

This TypeScript snippet would be transpiled to ES6 like this:

class AppController {
  constructor() {
    this.text = "some value"

    this.shoot = () => {
      console.log(this)
    }
  }
}

model = new AppController()

Have you spotted what happend? shoot was idefined in constructor, not in class! The important part here is the arrow function. It automatically makes a closure that will treat this keyword to be context-independent. If we would specify this method with a function() { } syntax then this would be depending on context call. Thus, the arrow function syntax is immune to Function.prototype.apply()!

If you need ES5 syntax (without a class) then the other way to define the model / controller would be creating simply an object:

var model = {
  text: "some value",
  shoot: function() {
    // instead of calling `this` you need to reference `model` directly
  }
}

Full test can be found on this plunker: → Plunker: scoping and functions

But! There is an option to slightly fix things with this snippet:

rivets.configure({
  handler: function(context, ev, binding) {
    return this.call(binding.view.models, context, ev)
  }
})

You may try pasting it before bind() call in the plunker above and see what this refers to in each example.

Summary

Is that it? Well, there are features I didn’t reveal in the article but the official documentation does it well enough:

Other than that, this library is really neat. It’s not a framework so it’s easy to be used within legacy projects where views are generated by backend with some PHP or Ruby code. You may want to add some life with JavaScript into your project without taking the jQuery (heard) approach. rivets will at least give you some notion of declarativeness.

Daj Się Poznać, Get Noticed 2017, javascript, rivets.js, web
comments powered by Disqus