Home

Awesome

Aurelia BindTable provides cool Aurelia bindings to RethinkDB

Forked from BindTable and tweaked to work for Aurelia ;)

The bindings are realtime using socket.io.

BindTable was inspired by Build Realtime Apps

Installation

Install aurelia-rethink-bindtable

  npm i aurelia-rethink-bindtable --save

Install RethinkDB

  npm i rethinkdb --save

Now you're ready to use RethinkDB bindings in your Aurelia application:

Distributed modules

The /dist folder contains built code for amd, commonjs, es6 and system. Choose the one that best fits your module system.

npm and ES6

By default the ES6 distribution is linked to the main entry of the package.json file.

import {Bindable} from 'aurelia-rethink-bindtable';

JSPM (SystemJs) and Amd

For JSPM the amd distribution is used by default (see jspm section of package.json)

  "jspm": {
    "main": "dist/amd/index",
    "format": "amd",
    "directories": {
      "lib": "dist/amd"
    ...    
    }
  }

Custom distribution loading

Commonjs example:

var Bindable = require('aurelia-rethink-bindtable/dist/commonjs').Bindable;

RethinkDB console

You can play around with the RethinkDB tables using the built in web console. To start DB server:

rethinkdb

open http://localhost:8080/#tables

Add a table and fill in the data. Then test it.

Running tests

Install Karma CLI

$ npm install -g jspm karma-cli

$ karma start
WARN [karma]: Port 9876 in use
INFO [karma]: Karma v0.12.31 server started at http://localhost:9877/
INFO [launcher]: Starting browser Chrome
INFO [Chrome 41.0.2272 (Mac OS X 10.10.2)]: Connected on socket cZNHR1B6WAacnOS_3bE5 with id 9608952
..

Trouble shooting

WARN [preprocess]: Can not load "babel", it is not registered!
  Perhaps you are missing some plugin?

Install missing plugin:

npm install karma-babel-preprocessor

Binding ViewModels

See Client API

Let's configure a View-Model Questions that binds to the RethinkDB table 'question' via bindtable over socket.io

The main classes to import are:

import {Bindable, BindTable} from 'aurelia-rethink-bindtable';
import io from 'socket.io-client';
import Filters from './filters';

@inject(Filters)
export class Questions extends Bindable {
  tableName = 'questions';

  constructor(filters) {
    super({socket: io('localhost'), logging: true});
    this.filters   = filters;
  }

  filter() {
    this.table.bind(this.filters.easy, this.rowLimit);
  }
}

We assume we have a Filters class with filter functions such as easy, which can be injected as a singleton.

export default class QuestionFilters {
  easy(item) {
    return item.level === 'easy';
  }

  // more filter methods...
}

Better to inject Bindable than use inheritance!

import {Bindable} from 'aurelia-rethink-bindtable';
import io from 'socket.io-client';
import Filters from './filters';

@inject(Bindable, Filters)
export class Questions {
  tableName = 'questions';

  constructor(bindable, filters) {
    super({socket: io('localhost'), logging: true});
    this.filters   = filters;
    this.bound = bindable;
  }

  filter() {
    this.table.bind(this.filters.easy, this.rowLimit);
  }

  get rows() {
    return this.bound.rows;
  }

  get table() {
    return this.bound.table;
  }
}

You could combine this with an ES6 compatible mixin approach or use a custom @bindable([table name], [socket addr]) class decorator ;) Decorators guide

We provide such a class decorator @bindable. You tell @bindable the name of the table, such as questions and optionally the server host (default: localhost). @bindable will add the tableName and socketHost on the class itself (ie. as static properties).

Here we use the Aurelia @inject decorator. You could use any similar approach, such as needlepoint @dependencies

import {bindable} from 'aurelia-rethink-bindtable';

@bindable('questions', 'www.mydomain.com')
@inject(Bindable)
export class Questions {
  constructor(bindable) {
    this.bound = bindable.configure({logging: true, socketHost: Questions.socketHost});
  }

  selectRow(row) {
    this.selectedRow = row;
  }

  deleteSelected() {
    this.table.delete(this.selectedRow);
  }
}

The bindable decorator creates two getter methods rows and table which delegate to the this.bound properties of the same name, (ie this.bound.rows and this.bound.table) by convention.

You can use rows with repeat.for (in Aurelia) to dynamically display the row data of the table.

<template>
    <ul repeat.for="row of rows">
      <li click.bind="selectRow(row)">${row.id}</li>
      <li click.bind="selectRow(row)">${row.name}</li>
    </ul>
    <button click.bind="deleteSelected()">Delete</button>
</template>

Bindable

The Bindable class will create an instance variable rows (you can bind to) and a delete(record) function to delete a record (row) from the table. The rows will be filtered dynamically by the filter() method.

class Bindable {
  // ...
  get tableName() {
    throw "tableName not defined";
  }

  get rowLimit() {
    return 100;
  }

  activate() {
    this.table = this.bindTable.table(this.tableName);

    this.rows = table.rows;
    this.delete = table.delete;

    this.filter();
  }
}

Now bind to the variable rows. You can also use the instance variable table to directly interact with table methods such as adding or upserting rows etc.

this.table.delete(record)
this.table.add(record)
this.table.update(record) // upsert: ie. insert or update
this.table.findById(id)

You can enable logging by passing logging: true to the BindTable constructor.

BindTable.create({socket: socket, logging: true});

Development and contributions

Would be awesome to make this a plugin (if it makes sense), perhaps by making Bindable a registered singleton for injection so we use composition over inheritance!

See Skeleton plugin and making our first plugin

Would also be nice to create server side API generators for the entity/socket code (see below).

npm i - to install dependencies

gulp build - to build distribution

See /build/tasks for all gulp tasks supported ;)

npm link to link this module while developing. Add more tests in spec and run using karma via gulp test.

Server side code

You need to setup your server to listen to specific socket messages and emit messages back!

You can experiment with the new server/entity-listener class which you can use as follows:

import connect from 'aurelia-rethink-bindtable/server';
const io = require('socket.io')(server);

connect({
  orderBy: 'createdAt',
  io: io
}).forTables('questions', 'answers');

You can also use EntityListener and EntityBinders classes directly or extends as you want etc. (see code in src/server) EntityListener essentially wraps the code below (currently still untested!). which is the server io listen code for the question table. This code was taken from the original BindTable example

Also see Socket Server API for more details on using socket.io on the server.

io.on('connection', function(socket){
  socket.on('question:findById', function(id, cb){
    r.table('question')
      .get(id)
      .run(cb);
  });

  socket.on('question:add', function(record, cb){
    record = _.pick(record, 'name', 'question');
    record.createdAt = new Date();

    r.table('question')
      .insert(record)
      .run(function(err, result){
        if(err){
          cb(err);
        }
        else{
          record.id = result.generated_keys[0];
          cb(null, record);
        }
      });
  });

  socket.on('question:update', function(record, cb){
    record = _.pick(record, 'id', 'name', 'question');
    r.table('question')
      .get(record.id)
      .update(record)
      .run(cb);
  });

  socket.on('question:delete', function(id, cb){
    r.table('question')
      .get(id)
      .delete()
      .run(cb);
  });

  socket.on('question:changes:start', function(data){

    let limit, filter;
    limit = data.limit || 100;
    filter = data.filter || {};
    r.table('question')
      .orderBy({index: r.desc('createdAt')})
      .filter(filter)
      .limit(limit)
      .changes()
      .run({cursor: true}, handleChange);

    function handleChange(err, cursor){
      if(err){
        console.log(err);
      }
      else{

        if(cursor){
          cursor.each(function(err, record){
            if(err){
              console.log(err);
            }
            else{
              socket.emit('question:changes', record);
            }
          });
        }

      }
      socket.on('question:changes:stop', stopCursor);

      socket.on('disconnect', stopCursor);

      function stopCursor () {
        if(cursor){
          cursor.close();
        }
        socket.removeListener('question:changes:stop', stopCursor);
        socket.removeListener('disconnect', stopCursor);
      }
    }
  });
});