Make your Backbone Application dance

Post on 23-Aug-2014

462 views 0 download

description

Building Backbone applications quickly became a de-facto standard, though we can’t really define it as framework but rather just as foundation to create other frameworks. If you like Backbone, you’ll fall in love with Marionette, the best application library to create large scale Javascript applications. Nicholas will explain the framework, show examples and talk about some projects like cronycle.com that have been delivered to production using Marionette.

Transcript of Make your Backbone Application dance

ake your Backbone Application Dance

May 15th, 2014 — Verona

Nicholas Valbusa@squallstar

I AM

Nicholas Valbusasquallstar - squallstar.it

AT

Lead JS Developercronycle.com

Our challenges:

- Expand your timeline tweets into stories

- Organize and filter your stories into collections

"Cronycle is a content curation tool based on Twitter tweets and RSS feeds"

- Make it real-time, responsive, awesome

- Live search

10ppl in London

- Rails+Marionette Webclient

Paid service, at the end of this speech (Yipee!!!)

- Rails API, Elasticsearch, Redis queue

- iOS Native app

Always choose the right tool for the job

• Solid foundation/core classes

• Flexible and simple

• Great pub/sub events architecture

• Awesome Model/Collection implementationfor REST resources

• Good MV*-style code separation (p.s. it’s a MVP)

• It is not like Angular.JS

THE GOOD PARTS

• It's incredibly easy to end up in a bad place

• No Application main class (some people use its router)

• Doesn’t include automatic and good ways to bind models to their views

• No “native” way to organise the pieces of your webapp into modules

• No "native" way to organize layouts (header/footer/sidebar/content/etc..)

• It is not sponsored/maintained by Google

THE BAD PARTS

let me read that again,

• “Awesome Model/Collection implementationfor REST resources”

var Library = Backbone.Collection.extend({ model: Book, url: “v1/books” });

for all the rest, there’s…

Marionette.jsmarionettejs.com

Very similar to Backbone, just goes a few more steps.

“A composite application library for Backbone that aims to simplify the construction of large scale

JavaScript applications” !

— that sits on top of Backbone

An event-driven collection of common design and implementation patterns.

Basically… Marionette brings an application architecture to Backbone

Key features

• Organised as hellApplications are built in modules, and with event-driven architecture

• No zombies Built-in memory management and zombie-killing in views, regions and layouts

• Flexible Just code the way you prefer, and picks only the features you need.

• Takes care of the rendering process

TAKES CARE OF THE RENDERING

PROCESS

Depends on:

Backbone & Underscore

Backbone.BabySitter & Backbone.Wreqr (both included)

just 31kb !

PrefaceThe base structure I'm using was adopted from BackboneRails

App skeleton& boot

<html> <head> <title>JSDay2014 - Marionette</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <script src="libs/jquery.js"></script> <script src="libs/underscore.js"></script> <script src="libs/backbone.js"></script> <script src="libs/backbone.marionette.js"></script> <script src="boot.js"></script> </head> <body>

<div id="header-region"></div> <div id="main-region"></div> <div id="footer-region"></div>

<script> App.start({ environment: 'staging', foo: 'bar' }); </script> ! </body> </html>

index.html

App = new Backbone.Marionette.Application(); !App.on("initialize:before", function (options) { // do something. come on! }); !App.on("initialize:after", function (options) { if (Backbone.history){ Backbone.history.start(); } });

javascript / boot.js

— that’s it!

Regions

App.addRegions({ headerRegion: "#header-region", mainRegion: "#main-region" footerRegion: "#footer-region" });

MyCustomHeaderRegion = Marionette.Region.extend({ el: "#header-region" }); !MyApp.addRegions({ headerRegion: MyCustomHeaderRegion });

You can also define custom classes for your regions:

“Regions provide consistent methods to manage, show and close views in your applications and layouts”

header-region

main-region

footer-region

Showing a view in a region

var myView = new MyView(); !// renders and displays the view App.mainRegion.show(myView); !// closes the current view App.mainRegion.close();

— simple as that.

header-regionmain-region

footer-region

MyView

App.mainRegion.currentView

If you replace the current view with a new view by calling show, it will automatically close the previous view

// Show the first view. var myView = new MyView(); MyApp.mainRegion.show(myView);

no more zombies!

// Replace the view with another. The // `close` method is called for you var anotherView = new AnotherView(); MyApp.mainRegion.show(anotherView);

Marionette Modules

AMD/Require vs Marionette Modules

Take advantage of Marionette's built in module-loader

App.module("MyModule", function (MyModule, App, Backbone, Marionette, $, _) { ! // do stuff here ... ! var myData = "this is private data"; ! MyModule.someData = "public data"; !}); !var theSameModule = MyApp.module("MyModule");

Split your sections/features into modules

Always keep your app organised —

while it grows

Organise each module

App.BlogApp (Marionette Module)BlogApp.Router (Marionette.AppRouter)BlogApp.Posts (Submodule)

Posts.ControllerPosts.View

What about Backbone models/collections?

App.Entities (Marionette module)Entities.Article (Backbone.Model)Entities.Articles (Backbone.Collection)

yeah, about those models...

The magic of Backbone models

App.module("Entities", function (Entities, App, Backbone, Marionette, $, _) { ! Entities.Post = Backbone.Model.extend(); ! Entities.Posts = Backbone.Collection.extend({ model: Entities.Post, url: “path/to/posts.json“ }); !});

entities / posts.js

App.module("BlogApp", function (BlogApp, App, Bk, Mr, $, _) { ! BlogApp.Router = Backbone.Marionette.AppRouter.extend({ appRoutes: { "posts" : "showArticles", "posts/:id" : "showArticle" } }); ! var API = { showArticles: function () { BlogApp.Posts.Controller.Show(); }, showArticle: function (id) { BlogApp.Posts.Controller.Show(id); } }; ! App.addInitializer(function () { new BlogApp.Router({ controller: API }); }); !});

apps / blog / app.js

App.module("BlogApp.Posts", function (Posts, App, Bk, Mr, $, _) { ! Posts.Controller = { Show: function () { ! var layout = new Posts.View({ collection: new App.Entities.Posts }); ! App.mainRegion.show(layout); } }; !});

apps / blog / posts / controller.js

App.module("BlogApp.Posts", function (Posts, App, Backbone, M, $, _) { ! Posts.View = Backbone.Marionette.View.extend({ tagName: "section", className: "posts" }); !});

apps / blog / posts / view.js

Here comes the magic!

Let’s have a look at Marionette Views

Marionette.ItemViewRenders a single item

(Backbone.Model)

Backbone.Model

ItemView

Marionette.CollectionViewRenders the items of a Backbone.Collection

Doesn’t need a template

CollectionView

ItemView Backbone.Model

Backbone.Collection

ItemView Backbone.Model

Marionette.CompositeView

Renders the items of a Backbone.Collection within a wrapper

Extends from Marionette.CollectionView !

Also: Represents both a branch and a tree structure

Therefore: can also render a model if needed

CollectionView

ItemViewBackbone.Model

Backbone.Collection

ItemViewBackbone.Model

ItemViewBackbone.Model

CompositeViewTemplate

+ Backbone.Collection + optional Backbone.Model

Backbone.Model

ItemView

.some-selector

Backbone.Model

ItemView

Before going further, choose your template engine

Underscore templates works out of the box

<script type="template" id="post-template"> <h2> <%- title %> </h2> </script>

you can also override Marionette Renderer:

Backbone.Marionette.Renderer.render = function (template, data) { ! tpl = _.template($( "script.wtf-is-this-" + template ).html()); if (!tpl) throw("Template " + template + " not found!"); ! return tpl(data); !};

<script type="text/template" class="wtf-is-this-post-template"> <h2> <%- title %> </h2> </script>

config/marionette/renderer.js

Using Rails? Go with Jade + JST

gem 'tilt-jade'

Compiles jade templates into js functions for use as clientside templates

Jade is just amazing

.post-content header(class='ng-wtf') h1= title span by #{author} ! if youAreUsingJade p You are amazing ! .body= description

Backbone.Marionette.Renderer.render = (tpl, data) -> path = JST["apps/" + tpl] throw "Template #{tpl} not found!" unless path path data

CoffeeScript...

back to our applet's implement these views

App.module("BlogApp.Posts", function (Posts, App, Bk, Mr, $, _) { ! Posts.PostView = Backbone.Marionette.ItemView.extend({ tagName: "article", className: "post", template: “#post-template" }); ! Posts.View = Backbone.Marionette.CollectionView.extend({ tagName: "section", className: "posts", itemView: Posts.PostView, ! initialize: function (options) { options.collection.fetch(); } }); !});

apps / blog / posts/ view.js

let’s make it better

<script type="text/template" id="post-template"> <a href="#"><%- title %></a> </script> !!<script type="text/template" id="posts-template"> <h1>My nice blog</h1> <ul></ul> </script>

Posts.View = Backbone.Marionette.CompositeView.extend({ tagName: "section", className: "posts", template: “#posts-template", itemView: Posts.PostView, itemViewContainer: "ul", ! initialize: function (options) { options.collection.fetch(); } });

just a few changes to the CollectionView

Posts.PostView = Backbone.Marionette.ItemView.extend({ tagName: "li", className: "post", template: “#post-template", events: { "click a" : "showSinglePost" }, showSinglePost: function (event) { event.preventDefault(); Backbone.history.navigate("posts/" + this.model.get('id')); } });

and some more to the ItemView

Serializing the data

Marionette calls model.toJSON() by default

Posts.PostView = Backbone.Marionette.ItemView.extend({ ... ! // overrides the default behaviour serializeData: function () { return _.extend(this.model.toJSON(), { "foo" : "bar" }); } });

can be overridden by defining serializeData()

Template helpers

<script id="my-template" type="template"> I think that <%= showMessage() %> </script>

Posts.PostView = Backbone.Marionette.ItemView.extend({ ... ! templateHelpers: { showMessage: function () { return this.title + " rocks!"; } }, ! ... });

Modal/Collection events

Backbone.Marionette.CompositeView.extend({ ! modelEvents: { // eq to view.listenTo(view.model, "change:name", view.nameChanged, view) "change:name": "nameChanged" }, ! collectionEvents: { // eq to view.listenTo(view.collection, "add", view.itemAdded, view) "add": "itemAdded" }, ! // ... event handler methods nameChanged: function () { /* ... */ }, itemAdded: function () { /* ... */ } !});

App global requests

// define your request App.reqres.setHandler("show:post", function (id) { Backbone.history.navigate("posts/" + id, true); });

AKA let your modules talk with each other

// use it App.request("show:post", 3);

Marionette in the real world

— 5 minutes of Marionette applied to Cronycle —

header-region with ItemView (User, Backbone.Model)

main-region with CollectionView (Backbone.Collection)

CompositeView (Model + Collection)

ItemView (Model)

ItemView (Model)

left-sidebar-region with CompositeView

Modal windows, just an overlay region

Modal region

App.module("Modals", function (Modals, App, Backbone, Marionette, $, _) { ! Modals.Region = Marionette.Region["extends"]({ el: "#modal-region", open: function(view) { $.magnificPopup.open(view); }, close: function() { $.magnificPopup.instance.close(); } }); !!!!!!!!!!!!!!!!});

Modals.start = function () { App.addRegions({modalRegion: Modals.Region}); };

App.reqres.setHandler("alert", function (title) { view = new MyModalWindow({ title: title }); return App.modalRegion.show(view); });

// This is how you use it App.request("alert", "Roflcopter!");

Fetching for new articles

1. define a comparator on your collection

Fetching for new articles

var Entities.Posts = Backbone.Collection.extends({ model: Entities.Post, url: "/posts", ! comparator: function (model) { return -parseInt(model.get('published_at'), 10); } });

2. define a custom fetch method

Fetching for new articles

var Entities.Posts = Backbone.Collection.extends({ ! fetchNewPosts: function (callback) { this.fetch({ url: "posts/?min_ts=#{@first().get('published_at')}", update: true, add: true, remove: false } !});

Fetching for new articles

3. override the appendHtml method on your CompositeView

var YourItemView = Backbone.Marionette.CompositeView.extends({ ! ui: { linksContainer: ".posts-container" }, ! appendHtml: function (collectionView, itemView, index) { if (index == 0){ this.ui.linksContainer.prepend(itemView.$el); } else { childAtIndex = this.ui.linksContainer.find("> article:eq(" + index + ")"); ! if (childAtIndex.length) { childAtIndex.before(itemView.$el); } else { this.ui.linksContainer.append(itemView.$el); } } } !});

put a test on it

https://github.com/bradphelan/jasminerice

+ jasmine-rice for Rails users

If you like your goat...

describe("Navigating to the posts route", function () { ! it("should display some articles", function () { ! Backbone.history.navigate("/posts", true); ! expect(App.mainRegion.$el.find('article.post').length).toBeGreaterThan(0); ! expect(App.mainRegion.currentView.collection.at(0).get('title')).toBe('foo'); ! }); });

github.com/squallstar/jsday2014-marionettejs

The project we just created:

(the dummy blog, not cronycle!)

Nicholas Valbusa@squallstar - squallstar.it

THANKS! Q/A?

https://joind.in/11286