Adding Dynamic Content to your Joomla Website - Make your Website Dance
Make your Backbone Application dance
-
Upload
nicholas-valbusa -
Category
Internet
-
view
462 -
download
0
description
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