Achieved by creating Modules and Components. Modules and Components are self contained parts of functionality that don't depend on anything else.
Modules encapsulate a single route - every route is added a module, and withing that module logic is implemented by utilizing Components.
Modules and Components in eeDepart are completely override-able. They can either be slightly modified in the custom project or behavior can be completely different ( like for SWR )
Extensibility is achieved by using require.js and a plugin we modified for this purpose. Exact details are in the documentation
Override process is very straightforward and when done once becomes very familiar.
Scalability in this sense means that the project can have as many or as little modules and components as necessary. Since every module and every component are completely separate, and usually don't depend on each other, it's perfectly safe to add or remove modules from the application.
Along Modules and Components we have introduced specialized parts of application that serve specific purposes:
* Services - influenced by Anguar.js services
* EventsHub - Flux like dispatcher but with more control
* AjaxCommands - utility to ease preparation of ajax request
* Resources - separate each resource in its own container
Since eeDepart serves as a CORE for all other Check-ins that are build on this architecture we go to great lengths that only necessary changes are added and we try to improve the code quality overall. We do this by:
* New branching mechanism - that allows proper versioning
* (Manually) taking care that only smallest set of common functionality gets into the core
* Using code style tools: jshint and jscs
* Writing Unit Tests ( inprogress )
* Frequent reviews of our processes
Documentation covers the process pretty well but just to summarize:
1. We load all of the dependencies
2. Using pegasus.js we fetch config and resource data
3. When config is ready we start the application
4. On application start ALL modules and standalone components are instantiated
5. Modules set up route listeners and wait to be activated
6. Once a route is reached the module starts and calls the controller to prepare the screen data and render.
What we solved:
* Routing via modules - it's trivially easy to add a new route / module to the app
* Extendible by default
* Modules / Components system - as long as the same principles are followed
* Screen rendering process - already set up for mustache and Marionette
* Local storage handling by default
* Error handling
* Loading screen handling
* Component shareable across projects by default - again as long as principles are followed.
RetrieveApp.js - set up route action
var routerAPI = {
loadRoute: function() {
var route = eeDepart.getCurrentRoute();
Backbone.EventsHub.useHub( 'stateComponentHub' )
.trigger( 'setActiveScreenState', route, {});
}
};
...
...
/**
* Add the route bindings for this module and pass the callback method.
*/
eeDepart.addRoute( routerAPI, routes );
RetrieveApp.js - add route callbacks
function appStateChanged( stateModel, actionData ) {
if ( stateModel.get( 'currentPage' ) === ee.Constants.RouteEvents.RETRIEVE ||
stateModel.get( 'currentPage' ) === '' ) {
API.loadRetrieve( actionData );
}
}
...
...
/**
* Listen to a 'route state' change. When a route is changed in the
* browser, or when a new state is triggered programatically listen
* for the new state change and react accordingly.
*/
Backbone.EventsHub.useHub( 'stateComponentHub' )
.on( 'eeDepart:stateContainer:stateChanged', appStateChanged, this );
/**
* Add the route bindings for this module and pass the callback method.
*/
RetrieveApp.js - when route is reached fire up the screen loading process
loadRetrieve: function( data ) {
var that = this;
require([ 'replace!ROOT_MODULE/retrieve/main/retrieveController:retrieveController' ],
function( RetrieveController ) {
eeDepart.navigate( ee.Constants.RouteEvents.RETRIEVE, data );
if ( !that.controller ) {
that.controller = new RetrieveController();
}
that.controller.loadContent( data );
}
);
}
http://eedepart/app/ck.fly#ck_retrieve
RetrieveController.js - Instantiate the model and the Layout view.
loadContent : function( data ) {
var isDeeplink = Backbone.EventsHub.useHub( 'stateComponentHub' ).request( 'getStateValue', 'isDeeplink' );
this.model = new RetrieveModel( data );
var dataLoadedCallback = _.bind(function() {
var LayoutView = new RetrieveLayoutView({
model: this.model
});
eeDepart.loadContent( LayoutView, isDeeplink );
// Set deeplink as false, we no longer need that info
Backbone.EventsHub.useHub( 'stateComponentHub' ).trigger( 'setStateValue', 'isDeeplink', false );
}, this );
/**
* Promise - based approach for loading data. A model will resolve a
* promise when the data is ready and the callback will fire rendering
* the Views.
*/
this.model.loadAndPrepareData().then( dataLoadedCallback );
}
define([ 'backboneServices' ],
function() {
'use strict';
return Backbone.Service.extend({
initialize : function() {
this.config = new Backbone.Model();
},
services : {
'setConfig': 'setConfigCallback', // Set the initial config we
// get from PHP
'getConfig': 'getConfigCallback', // Get the whole config object
'getParam': 'getParamCallback' // Get one config param
},
setConfigCallback: function( configData ) {
this.config.set( configData );
},
getConfigCallback: function() {
return this.config;
},
getParamCallback : function( param ) {
return this.config.get( param );
}
});
}
);
Backbone.serviceFactory( 'configService', ConfigService); // Register
var myService = Backbone.serviceProvider( 'logtextService' ); // Fetch
/**
* myService now has all above methods available
*/
define([ 'backbone', 'backboneResources' ],
function( Backbone ) {
'use strict';
var airportsResource = Backbone.Resource.extend({
fetch: function() {
this.deferred = $.Deferred();
if ( !this.done ) {
_.defer(function( context ) {
var airports = Backbone.serviceProvider( 'configService' )
.getParam( 'airportList' );
context.resource.set( airports );
context.done = true;
context.deferred.resolve();
}, this );
} else {
this.deferred.resolve();
}
return this.deferred;
},
getAirportCodesWithNames: function() {
return _.object( this.resource.map(function( model ) {
return [ model.get( 'airport_code' ), model.get( 'airport_name' ) ];
}) );
}
});
Backbone.resourceFactory( 'airportsResource', airportsResource ); // Register
}
);
Backbone.resourceProvider( 'airportsResource' ).fetch().then( callback ) // Use
define([ 'marionette' ],
function( Marionette ) {
'use strict';
return Marionette.Behavior.extend({
events : {
'change select' : 'saveByValue',
'change [type="text"]' : 'saveByValue',
'click [type="radio"]' : 'saveByValue',
'click [type="checkbox"]': 'saveByChecked'
},
saveByValue : function( e ) {
var canSave = true;
if ( !_.isEmpty( this.options.allowedTargets ) &&
!_.contains( this.options.allowedTargets, e.target.name ) ) {
canSave = false;
}
if ( canSave ) {
this.view.model.set( e.target.name, e.target.value );
}
},
saveByChecked: function( e ) {
this.view.model.set( e.target.name, e.target.checked );
}
});
});
define([ 'components/loadingScreen/loadingScreenView',
'components/loadingScreen/loadingScreenModel',
'text!components/loadingScreen/loadingScreenTemplate.html'
],
function( View, Model ) {
'use strict';
var loadingScreen = Backbone.Components.Component.extend({
name : 'loadingScreenComponent',
view : View,
model : Model,
template : 'text!components/loadingScreen/loadingScreenTemplate.html',
initialize : function() {
var hub = Backbone.EventsHub.registerHub( 'loadingScreen' );
// we initially set that loading screen is not displayed
this.displayed = false;
// and we tell this component what events to listen to
hub.on( 'loadingScreen:showLoader', this.showLoadingScreen, this );
hub.on( 'loadingScreen:hideLoader', this.hideLoadingScreen, this );
Backbone.EventsHub.useHub( 'eeDepart' ).on( 'navigate', this.showLoadingScreen, this );
Backbone.EventsHub.useHub( 'eeDepart' ).on( 'contentLoaded', this.hideLoadingScreen, this );
Backbone.EventsHub.useHub( 'eeDepart' ).on( 'componentsLoaded', this.componentsLoaded, this );
},
componentsLoaded : function() {
this.create().render();
},
showLoadingScreen: function() {
//...
},
hideLoadingScreen: function() {
// ...
}
});
/**
* Initialize the component immediately
*/
new loadingScreen();
});
While here we're mentioning only client side features rest assured that the changes and future plans for Server side processing of eeDepart are equally important for the project long term success.