When building large multi-modular apps, embedding business logic within your Backbone views or models does not scale well. Ideally you would want to de-couple your business logic from the view logic. Solving this problem becomes really easy using Geppetto.
Prerequisites
To better understand the example in this blog post, it is important to have a good knowledge on the following frameworks:
Backbone & Marionette
Backbone ModelBinder
Please refer to my previous post on Backbone ModelBinder
RequireJS
Cary Landholt has a really good screen cast on requireJs
Experience the application first
We will be building a simple movie search app. We make use of the rotten tomatoes developer api for fetching the movie details.
Here is the direct link to the application: link
Some more information on the application
The application we are building is a simple search page with three views. A search view on the left for searching a movie, a result view on the right for displaying the search result and a container view which holds the search and the result views.
What functionality does each view provide?
Lets list out the functionality that each view is expected to provide.
Search View
Should accept the movie title as users input.
Should shout out the movie title using an event when the search button is clicked.
Result View
Should keep listening for data.
Display the data to the user whenever it gets it.
Container View
Acts as a dumb container that holds the search and the result view.
//search viewdefine(["jquery","underscore","backbone","marionette","geppetto","text!src/templates/SearchViewTemplate.html"],function($,_,Backbone,Marionette,Geppetto,SearchViewTemplate){varSearchView=Marionette.ItemView.extend({template:SearchViewTemplate,className:"well span4",bindings:{"title":'[name = "title"]'},events:{"click button.searchBtn":"searchClicked"},searchClicked:function(e){if(this.model.get("title")){this.context.dispatch("performSearchEvent"/*event name*/,{data:this.model}/*event payload*/);}else{//if title is not set, shake the text input//should have a required validator, but this would workthis.$('[name = "title"]').removeClass().addClass('animated shake');varthat=this;varwait=window.setTimeout(function(){that.$('[name = "title"]').removeClass()},1300);}},//local variable for model binder_modelBinder:undefined,initialize:function(){_.bindAll(this);//on view initialize, initialize _modelBinderthis._modelBinder=newBackbone.ModelBinder();//save the passed in context locally such that// we can dispatch or listen to events on this contextthis.context=this.options.context;},close:function(){//when view closes, unbind Model bindingsthis._modelBinder.unbind();},onRender:function(){this._modelBinder.bind(this.model/*the model to bind*/,this.el/*root element*/,this.bindings/*bindings*/);}});returnSearchView;})
//result viewdefine(["jquery","underscore","backbone","marionette","geppetto","text!src/templates/ResultViewTemplate.html"],function($,_,Backbone,Marionette,Geppetto,ResultViewTemplate){varResultView=Marionette.ItemView.extend({template:ResultViewTemplate,className:"well span6 clearfix",bindings:{"title":'[name = "title"]',"year":'[name = "year"]',"rated":'[name = "rated"]',"rating":'[name ="rating"]',"poster":{selector:'[name=poster]',elAttribute:'src'}},initialize:function(){_.bindAll(this);//on view initialize, initialize _modelBinderthis._modelBinder=newBackbone.ModelBinder();//save the passed in context locally such that// we can dispatch or listen to events on this contextthis.context=this.options.context;this.context.listen(this,"loadResultsSuccessEvent"/*event name*/,this.handleSearchResultsLoaded/*event listener*/);this.context.listen(this,"loadResultsErrorEvent"/*event name*/,this.handleSearchResultsLoadError/*event listener*/);},close:function(){//when view closes, unbind Model bindingsthis._modelBinder.unbind();},onRender:function(){this._modelBinder.bind(this.model/*the model to bind*/,this.el/*root element*/,this.bindings/*bindings*/);},handleSearchResultsLoaded:function(data){this.model.clear();this.model.set(data);},handleSearchResultsLoadError:function(){this.model.clear();alert('Opps...something went wrong, try searching again');}});returnResultView;})
//container viewdefine(["jquery","underscore","backbone","marionette","geppetto","src/controller/ApplicationContext","text!src/templates/ContainerTemplate.html","src/views/SearchView","src/views/ResultView"],function($,_,Backbone,Marionette,Geppetto,ApplicationContext,ContainerTemplate,SearchView,ResultView){//container view acts as plain layout viewvarContainerView=Marionette.ItemView.extend({//set templatetemplate:ContainerTemplate,className:"container myContainer",initialize:function(){_.bindAll(this);//create a Geppetto contextGeppetto.bindContext({view:this,context:ApplicationContext});},onRender:function(){//when view is container view is rendered//construct the search viewthis.constructSearchView();//construct the result viewthis.constructResultView();},constructSearchView:function(){//instantiate an search view//notice that we are passig the context from the//current container view to the search view constructorvarmySearchView=newSearchView({context:this.context,model:newBackbone.Model()});//render the viewmySearchView.render();//append it the current containerthis.$el.append(mySearchView.$el);},constructResultView:function(){//instantiate an result view//notice that we are passig the context from the//current container view to the result view constructorvarmyResultView=newResultView({context:this.context,model:newBackbone.Model()});//render the viewmyResultView.render()//append it the current containerthis.$el.append(myResultView.$el);}});returnContainerView;});
So, who is actually fetching the data ?
By looking at the code above, none of the views hold the business logic to fetch the data from the serve. The search view simply dispatches a performSearchEvent with the movie title as payload. The result view keeps listening for loadResultsSuccessEvent or loadResultsErrorEvent for displaying the data or error message & the container view simply creates these two views.
So who is actually querying the server ? Well, with Geppetto, you could define commands that lets you handle all the complex business logic.
The code snippet below shows the command for our example. The commands have an execute function which gets called when an event tied to the command is triggered.
//Search Movies Commanddefine(["jquery","underscore"],function($,_){varcommand=function(){};command.prototype.execute=function(){_.bindAll(this);varthat=this;varapikey="78ejsdd76tc6jsffmrxjddxu";varbaseUrl="http://api.rottentomatoes.com/api/public/v1.0";varmoviesSearchUrl=baseUrl+'/movies.json?apikey='+apikey;//get the movie titlevarquery=this.eventData.data.get("title");varpageLimit="&page_limit=1";//make an plain jquery ajax call to fetch the movie details using the//rotten tomatoes public api's$.ajax({url:moviesSearchUrl+'&q='+encodeURI(query)+pageLimit,dataType:"jsonp",success:function(data){that.handleDataLoadSuccess(data);},statusCode:{503:function(){that.handleDataLoadError("page not found");}},error:function(jqXHR,textStatus,errorThrown){that.handleDataLoadError(errorThrown);}});};command.prototype.handleDataLoadSuccess=function(data){varmovies=data.movies;if(!data||!data.movies||data.movies.length<=0){//when there are no movies dispatch an error eventthis.context.dispatch("loadResultsErrorEvent"/*event name*/);}else{//when we get the movies results//construct an object with movie detailsvarresultObj={};resultObj.rated=movies[0].mpaa_rating;resultObj.title=movies[0].title;resultObj.rating=movies[0].ratings.audience_score;resultObj.year=movies[0].year;resultObj.poster=movies[0].posters.original;//dispatch an event on the context with movie details as payloadthis.context.dispatch("loadResultsSuccessEvent"/*event name*/,resultObj);}};command.prototype.handleDataLoadError=function(e){//when there are no movies dispatch an error eventthis.context.dispatch("loadResultsErrorEvent"/*event name*/);};returncommand;})
How does this command get called?
Geppetto controller/context facilitate’s the mappings between events and command. In our case whenever a performSearchEvent is dispatch the context/controller maps it to the SearchMoviesCommand and supplies the eventData as payload to the command.
123456789101112131415161718192021
//application context or controllerdefine(['backbone','geppetto','src/commands/SearchMoviesCommand'],function(Backbone,Geppetto,SearchMoviesCommand){//return a geppetto contextreturnGeppetto.Context.extend({//setup an initialize functioninitialize:function(){// map commands //when ever a "performSearchEvent" is dispatch on this command//the context delegates that call to the SearchMoviesCommandthis.mapCommand("performSearchEvent"/*event name*/,SearchMoviesCommand);}});})
If we look at the initialize function in container view, we create a context using the bindContext function.
We then pass this context into search and result view constructors. Using context, communicating between view becomes really easy. Each view that shares a common context can dispatch and listen to events on the context.
So what’s the benefit of Geppetto
By now, its should be clear as to what advantage Geppetto brings to your Backbone apps. Here is a list of them:
Currently we use the rotten tomatoes api’s for searching movies. If we want to use a different service provider, all we need to do is modify the logic in the command. Our views remain untouched
The same logic goes for the views as well, if we want to change the view layout, the business logic remains untouched.
Since there is a clear separation of concern and de-coupling between our views and command, writing test cases would be really easy.
Where can I find the complete source code for this example?
The complete source code for this example can be found at the following repo link