_ _ _ ___ _ _ _ _ /_\ | |_ __ ___ __| |_ / _ \| |__ ___ ___| |___| |_ ___ _ _ ___| |_ / _ \| | ' \/ _ (_-< _| (_) | '_ (_-</ _ \ / -_) _/ -_)_| ' \/ -_) _| /_/ \_\_|_|_|_\___/__/\__|\___/|_.__/__/\___/_\___|\__\___(_)_||_\___|\__|
Hypermedia APIs and JavaScript Applications

Thomas Parslow

tom@almostobsolete.net
@almostobsolete
http://almostobsolete.net

Coming up!

A rich JavaScript application

The Dharmafly Team

Technologies





Architecture

The Hypermedia API

REST so over. Long live Hypermedia APIs.

http://blog.steveklabnik.com/posts/2012-02-23-rest-is-over

Hypermedia APIs

HTTP is Cool

The HTTP Uniform Interface

VerbSafeIdempotent
GETXX
PUTX
DELETEX
POST

Idempotent is a big word, but it just means that doing something lots is the same as doing it once. So automatic retries are fine!

Representation

Hyperlinks in JSON

{
  "href": "http://example.com/api/v1/users/john",
} 

Structure for JSON

{
  "href": "http://example.com/api/v1/users/john",
  "type": "user",
  "head": {
    ...
  },
  "body": {
   ...
  }
}
      

Related Links

{
  "href": "http://example.com/api/v1/users/john",
  "type": "user",
  "head": {
    ...
    "related": {
      "meetings": {
        "href": "http://example.com/api/v1/users/john/meetings"
      }
    }
  },
  "body": {
    ...
  }
}

Resource Representations

{
  "href": "http://example.com/api/v1/users/john",
  "type": "user",
  "head": {
    "allow": ["GET","PUT","PATCH","DELETE"],
    "created_at": "2011-03-09T00:33:00Z",
    "updated_at": "2011-03-09T00:33:00Z",
    "related": {
      "meetings": {
        "href": "http://example.com/api/v1/users/john/meetings"
      }
    }
  },
  "body": {
    "display_name": "John Doe",
    "phone_number": "123456789",
    "email": "john@example.com",
  }
}

Example: Interacting with a meeting

Example: Interacting with a meeting

Example: Interacting with a meeting

Example: Interacting with a meeting

Live Updates with WebSockets

{
 "type": "comment",
 "href": "http://example.com/api/v1/meetings/MYMEETING/activities/99",
 "head": {
  "related": {
   "parent": {
    "href": "http://example.com/api/v1/meetings/MEETINGID/activities"
   }
  }
 },
 "body": {
  "text": "Hello world!"
 }
} 

Partial Updates with PATCH and JSONPatch

Example resource:

{
  "type": "conference",
  "href": "http://example.com/api/v1/conferences/CONFERENCEID",
  "head": {
     ...
  },
  "body": {
    "lecture_mode": false,
    "recording": false,
    "locked": false
  }
} 

JSONPatch to start recording:

PATCH http://example.com/api/v1/conferences/CONFERENCEID
[
    {"replace": "/body/recording", "value": true},
] 

Demo here: The API

A Quick Intro to Backbone.JS

Backbone.JS

Backbone.JS Model Example

TodoModel = Backbone.Model.extend({
  defaults: {
    done: false,
    title: 'Todo item'
  }
});
 
var model = new TodoModel({title: 'Do stuff'});

model.on('change:title', function () {
  alert('Title is now: ' + model.get('title'));
});

model.set({title: 'Do other stuff'}); 

Backbone.JS View Example

var TodoView = Backbone.View.extend({
  events: {'click': 'updateDone'},
  initialize: function () {
    this.model.on('change:done', this.refreshFromModel, this);
    this.model.on('change:title', this.refreshFromModel, this);
    _.bindAll(this, 'refreshFromModel');
  },
  refreshFromModel: function () {
    this.$('input').attr('checked',this.model.get('done'));
    this.$el.attr('class', 'done-' + this.model.get('done'));
    this.$('b').text(this.model.get('title'));
  },
  render: function () {
    this.$el.html('<input type="checkbox"/><b></b>');
    this.refreshFromModel();
  },
  updateDone: function () {
    this.model.set('done', !this.model.get('done'));
  }
});

var view = new TodoView({model: model});
view.render();

$('#todo-items').append(view.el);

Backbone.JS Collection Example

var todoitems = new Backbone.Collection();

todoitems.on('add', function (added) {
  alert('Todo item added with title: ' + added.get('title'));
});

todoitems.add(model);
  

JavaScript Client App

Models in the App

Getting a resource model

# Create a client instance (usually once per app)
client = new resources.Client({
  base: "http://example.com/api/v1/"
});

# Get a resource from a known url. If the resource exists in the cache
# it will be returned.
meeting = client.getResource(
   'http://example.com/api/v1/meetings/MEETINGID'
);
  

Making sure a resource is loaded

# Make sure the resource has been fetched from the server
# (only fetch if it hasn't already been fetched)
meeting.maybeFetch().done(function () {
  # It's now safe to assume that the meeting is loaded
  alert(meeting.get('title'));
});

  

Subscribe to a resource's stream

# Subscribe to the WebSocket stream for this meeting
meeting.subscribe(); 

Resource Hyperlinks

# Follow a "related" hyperlink
comments = meeting.related('comments');

# Fetch each item in a collection. If there where inclusions
# instead of links this doesn't actually go to the server.
comments.fetchEach(function (comment) {
  alert('I have a fully loaded comment here:' + comment.get('text'));

  # Combines a call to related and to maybeFetch
  comment.fetchRelated('owner', function (user) {
    alert(user.get('name') + ' posted that comment');
  });
});

Views

Model Binding

Model Binding

# Actual version is in CoffeeScript
var MeetingView = views.BaseView.extend({
  template: 'app/templates/meeting/meeting.html',
  contextBindings: {
    title: 'title',
    starts_at: 'starts_at',
    owner_name: 'owner/full_name'
  },
  ...
});
  
<div class="meeting">
  <h1>{{title}} at {{starts_at}} by {{owner_name}}</h1>
  <ul class="comments">
</ul> 
</div> 

View Hierarchy

View Hierarchy

var MeetingView = views.BaseView.extend({
...
attachPoints: {
  comment: '.comments'
}
});

var Comment = views.BaseView.extend({
...
});

# In a controller somewhere
mettingview.appendSubView(new Comment({model: commentModel}));
<div class="meeting">
<h1>{{title}} at {{starts_at}} by {{owner_name}}</h1>
<ul class="comments">
</ul> 
</div> 

Router

Router

FromToHandlers Called
/ /meeting/MEETINGID enterMeeting(MEETINGID)
/meeting/MEETINGID /meeting/MEETINGID/presentation enterPresentation
/meeting/MEETINGID/presentation /meeting/MEETINGID/asset/ASSETID leavePresentation
enterAsset(ASSETID)
/meeting/MEETINGID/asset/ASSETID / leaveAsset
leaveMeeting

Router

FromToHandlers Called
/ /meeting/MEETINGID enterMeeting(MEETINGID)
/meeting/MEETINGID /meeting/MEETINGID/presentation enterPresentation
/meeting/MEETINGID/presentation /meeting/MEETINGID/asset/ASSETID leavePresentation
enterAsset(ASSETID)
/meeting/MEETINGID/asset/ASSETID / leaveAsset
leaveMeeting

Router

FromToHandlers Called
/ /meeting/MEETINGID enterMeeting(MEETINGID)
/meeting/MEETINGID /meeting/MEETINGID/presentation enterPresentation
/meeting/MEETINGID/presentation /meeting/MEETINGID/asset/ASSETID leavePresentation
enterAsset(ASSETID)
/meeting/MEETINGID/asset/ASSETID / leaveAsset
leaveMeeting

Router

FromToHandlers Called
/ /meeting/MEETINGID enterMeeting(MEETINGID)
/meeting/MEETINGID /meeting/MEETINGID/presentation enterPresentation
/meeting/MEETINGID/presentation /meeting/MEETINGID/asset/ASSETID leavePresentation
enterAsset(ASSETID)
/meeting/MEETINGID/asset/ASSETID / leaveAsset
leaveMeeting

Router

FromToHandlers Called
/ /meeting/MEETINGID enterMeeting(MEETINGID)
/meeting/MEETINGID /meeting/MEETINGID/presentation enterPresentation
/meeting/MEETINGID/presentation /meeting/MEETINGID/asset/ASSETID leavePresentation
enterAsset(ASSETID)
/meeting/MEETINGID/asset/ASSETID / leaveAsset
leaveMeeting

Router

root = new router.Route()
subroute = root.extend('/meeting/:id', {
enter: function (id) {
  var _this = this;
  # return a promise as this isn't finished until
  # the meeting view is loaded.
  return loadMeetingView(id).done(function (view) {
    _this.view = view; 
  });
},
leave: function () {
  this.view.destroy();
}
});

# full path would now be /meeting/:id/summary
subroute.extend('/summary', {
enter: function () {
  # We still have access to the view we set in the parent route
  this.view.showSummary();

  # this will be accessible to this route and all beneath it but
  # won't be visible to the parent route's handlers
  this.otherInfo = 'hello';

  return;# not returning a promise so this is done right away
},
leave: function () {
  this.view.hideSummary();
} 
});

Summary

Thanks For Listening!





Any questions?


http://almostobsolete.net/talks/hypermedia/



Thomas Parslow
tom@almostobsolete.net
@almostobsolete
http://almostobsolete.net