My small secret web weapon to bind things - rivets.js
Hey, I need something. Uumm. Yeah, I need… JavaScript template engine! No? Something more? A small library that would bind data to existing DOM, locally! Yes!
rivets.js - it’s not too popular but it works and it’s only 26 KB minified without gzipping (which would go to just 6 KB!). Let’s go through it’s features and see some snippets I’ve developed through few months.
Story short - what the library is
- developed with CoffeeScript
- small but rather feature complete + extensible
- not as big as AngularJS
- so it just binds things
- and it’s much more than simple templating systems like swig or nunjucks
- it’s reactive on data change
- flexible through custom formatters, binders, adapters and components
Community right now is rather small and original author doesn’t seem maintaining it anymore but there are other people who do the job. Somewhat passively, I’m one of them and I hope this post would be a useful contribution.
The engine + compare to AngularJS
- rivets formatters are like Angular filters - those get the value and transform it to something else.
- rivets binders are like Angular directives - those get element and optional value passed in.
- rivets components are like Angular 1.5 components or Angular directives used as a components.
- adapters probably don’t exist in Angular because…
- Angular usually takes JavaScript expressions while rivets uses much more simplier custom format for expressions
- so rivets adapters are a way to register custom operators for watching objects differently.
- Angular does dirty checking by marking objects while rivets overwrites setters in Objects to notify the engine about changes. The latter mechanism is sometimes called to be reactive.
Performance may not be great since there is no Virtual DOM or similiar fancy technique. However, there’s a reactiveness to data change which makes some little promise. Also, there’s rv-if which happens to be a mix between ng-if and ng-show.
Talk is cheap. Show me the code!
Let’s train on an example - a list of github repositories with custom tags:
Let’s say we have a collection of GitHub repositories (repos) where each one of them can be tagged (has a collection of tags).
This is our scope defined in TypeScript using class syntax:
interface IRepo {
name: string
author: string
tags: string[]
isExpanded: boolean
}
class PageController {
repos: IRepo[]
sortBy = 'name'
selectedFilter = ''
textFilter = ''
constructor() {
this.setRepos([])
}
setRepos = (repos) => {
this.repos = repos
this.refreshFiltering()
}
refreshFiltering = () => {
const filterFunc = ((filter) => {
if (filter == 'all')
return el => true
if (filter == 'tagged')
return el => el.tags && el.tags.length > 0
else if (filter == 'untagged')
return el => !el.tags || el.tags.length == 0
else
throw new Error(`unknown filter type: ${filter}`)
})(this.selectedFilter)
// you will find FILTER_AND_SORT in script.js file
this.filteredRepos = FILTER_AND_SORT(this.repos, filterFunc, this.textFilter, this.sortBy)
}
toggleRepo = (repo) => {
repo.isExpanded = !repo.isExpanded
this.refreshFiltering()
}
deleteTag = (repo, tag) => {
let tagIndex = repo.tags.indexOf(tag)
repo.tags.splice(tagIndex, 1)
this.refreshFiltering()
}
}
let scope = new PageController()
See the arrow functions? It’s important to have them due to definition of this keyword in call context made by rivets. After transpilation to JavaScript it would look like this:
class PageController {
constructor() {
this.setRepos = (repos) => {
this.repos = repos
this.refreshFiltering()
}
this.toggleRepo = (repo) => {
repo.isExpanded = !repo.isExpanded
this.refreshFiltering()
}
this.deleteTag = (repo ,tag) => {
let tagIndex = repo.tags.indexOf(tag)
repo.tags.splice(tagIndex, 1)
this.refreshFiltering()
}
this.refreshFiltering = () => {
const filterFunc = ((filter) => {
if (filter == 'all')
return el => true
if (filter == 'tagged')
return el => el.tags && el.tags.length > 0
else if (filter == 'untagged')
return el => !el.tags || el.tags.length == 0
else
throw new Error(`unknown filter type: ${filter}`)
})(this.selectedFilter)
// you will find FILTER_AND_SORT in script.js file
this.filteredRepos = FILTER_AND_SORT(this.repos, filterFunc, this.textFilter, this.sortBy)
}
this.sortBy = 'name'
this.selectedFilter = 'all'
this.textFilter = ''
this.setRepos([])
}
}
let scope = new PageController()
Another option would be to use simple object with properties:
let scope = {
sortBy: 'name',
selectedFilter: 'all',
textFilter: '',
repos: [],
filteredRepos: [],
setRepos: (repos) => {
scope.repos = repos
scope.refreshFiltering()
},
toggleRepo: (repo) => {
repo.isExpanded = !repo.isExpanded
scope.refreshFiltering()
},
deleteTag: (repo, tag) => {
let tagIndex = repo.tags.indexOf(tag)
repo.tags.splice(tagIndex, 1)
scope.refreshFiltering()
},
refreshFiltering: () => {
const filterFunc = ((filter) => {
if (filter == 'all')
return el => true
if (filter == 'tagged')
return el => el.tags && el.tags.length > 0
else if (filter == 'untagged')
return el => !el.tags || el.tags.length == 0
else
throw new Error(`unknown filter type: ${filter}`)
})(scope.selectedFilter)
// you will find FILTER_AND_SORT in script.js file
scope.filteredRepos = FILTER_AND_SORT(scope.repos, filterFunc, scope.textFilter, scope.sortBy)
}
}
There’s a nuance about the
this
keyword which may break all your code. Read the “nuance about this keyword” chapter in the next article about details to understand the issue.
Let’s get back to the example - fill repos with:
[
{ name: "github-star-tagger-chrome", author: "Namek", tags: ["chrome", "extension", "github", "stars"] },
{ name: "xstream", author: "staltz", tags: ["web", "reactive", "rxjs", "stream", "observables", "cyclejs"] },
{ name: "rivets", author: "mikeric", tags: ["web", "binding", "javascript"] }
]
Now, the** view**:
<div id="app">
<select
rv-value="sortBy"
rv-on-change="refreshFiltering"
>
<option value="">Do not sort</option>
<option value="name">Sort by Name</option>
<option value="tagCount" rv-if="selectedFilter | equals 'untagged' | not">Sort by Tag count</option>
</select>
<select
rv-value="selectedFilter"
rv-on-change="refreshFiltering"
>
<option value="all">Show all</option>
<option value="untagged">Untagged</option>
<option value="tagged">Tagged</option>
</select>
<div rv-if="filteredRepos | isEmpty">
No repositories found!
</div>
<div
class="repo"
rv-each-repo="filteredRepos"
rv-class-expanded="repo.isExpanded"
>
<div
class="repo-name"
rv-on-click="toggleRepo | args repo"
>
repo: { repo.author } / { repo.name }
</div>
<div
class="repo-btns"
rv-if="repo.isExpanded"
>
<span class="tag" rv-each-tag="repo.tags">
{ tag }
<button
class="btn-delete-tag"
rv-on-click="deleteTag | args repo tag"
>×</button>
</span>
</div>
</div>
</div>
Let’s highlight some things in the HTML above:
- every attribute prefixed with rv- is a binder
- (by default) data is bound in 2 ways:
rv-value
directive which is a two-way binding- one-way text binding using curly braces in element’s content, e.g.
{ tag }
- you can show/hide elements conditionally using built-in
rv-if
binder - you can loop collection using built-in
rv-each-*
binder - you can toggle classes using built-in
rv-class-*
binder - you can chain formatters, i.e. they launch one after another, e.g.
rv-if="filter | equals 'untagged' | not"
whereequals
is the first launched formatter (with value “untagged”) andnot
is the second (without additional value). The second one is called with an argument which is a result of previous formatter (theequals
) - you can register on events using
rv-on-*
binder, e.g.rv-on-change
,rv-on-click
However, few things are mysterious since they’re not built-in:
- formatters:
isEmpty
,equals
,not
- passing
args
to function call!
Let’s define them!
rivets.formatters['equals'] = (val, expectedVal) => val == expectedVal
rivets.formatters['not'] = val => !val
rivets.formatters['isEmpty'] = val => !val || val.length == 0
rivets.formatters.args = function(fn) {
let args = Array.prototype.slice.call(arguments, 1)
return () => fn.apply(null, args)
}
Finally, let’s bind the scope with element of id=“app” (defined in view):
rivets.bind(document.getElementById('app'), scope)
Full example
To see this example live open the plunker: → Example for rivets.js - presenting basic features
Also, you can take a look at bigger example which is a source of the small plunker: → Namek/github-star-tagger-chrome
Summary
There’s some learning curve due to it’s **syntax **which is really simple in terms of implementation but you can’t have JavaScript expressions as we used to have in AngularJS. That’s especially visible with chained formatters.
As this library is pretty small there is more to learn to have more power. Follow the next part:
→ Binding with rivets.js - details and tricks
References
- rivets.js
- CoffeeScript - a language rivets.js is written in
- example plunker
- github-star-tagger-chrome
- Binding with rivets.js - details and tricks - somewhat part 2 of this article