Maximize code reuse, minimize DOM management, optimize happiness. All the features you need to build stunning HTML5 web and mobile applications with efficiency and ease. Lightweight, high performance, incredibly flexible, insanely powerful.
Base.js is a project designed to combine the very best feautres of backbone, angular, and ember with inspiration from many other great javascript MVC frameworks into one cohesive, highly performant, maximally flexible and extensible package.
Built with Backbone, jQuery, and Ractive
Sorry to those eager to use this project. I have since been unable to maintain this project and instead would personally recommend using React instead
HTML (DOM updates automatically on model changes)
<body base-app="myApp"> <h1>{{user.name}}</h1> <div class="controls"> <button on-click="set: 'mode', 'grid' " class="grid {{ mode == 'grid' ? 'active' : 'inactive' }}"></button> <button on-click="set: 'mode', 'grid' " class="single {{ mode == 'single' ? 'active' : 'inactive' }}"></button> </div> <base-view type="grid"> {{#picts}} <img outlet="pict" src="{{url}}" on-click="set: 'activePict', 'pict' "> {{/picts}} </base-view> <base-view type="lightbox" visible="{{!!activePict}}" on-click="hide: true "> <img src="{{activePict.url}}" outlet="pict"> </base-view> </body>
JS (in Coffeescript):
class App extends Base.App plugins: lazyLoadImages: true animateImagesOnLoad: type: 'fade' class Grid extends Base.View plugins: masonry: true defaults: mode: 'grid' class Lightbox extends Base.View defaults: showLightbox: false onChangeShowLightbox: -> @doSomething()
CSS (in Stylus):
[ data-view = pict ] [ data-mode = single ] & position relative
All base templates update automatically as your views and models change, no more jquery is needed! No, seriously, stop using .append(), .clone(), .addClass(), .removeClass(), etc. Just like angular, meteor, derby, and the many other fantastic live template libraries, once you use it you will not look back. No more spaghetti code, no more exponentially rising DOM manipulation complexity, no more rerendering entire views just to update one list.
Base live templates are built on top of Ractive, check out the examples for amazing examples of using live templates for all kinds of applications and purposes. And see below for how Base extends Ractive for additional power and flexibility including app level binding, route binding, singleton binding, and more.
{{someGlobalProperty}}
<!-- 'EACH' -->
{{#someArray}}
{{title}}
<!-- The dot indicates relative to the array-->
{{.someArrayProperty}}
<!-- This renders the array item itself, typically used if the value is a string in an array of strings -->
{{.}}
{{/someArray}}
<!-- 'IF' -->
{{#someNonArray}}
<h1> {{someNonArray}} is truthy! </h1>
{{/someNonArray}}
<!-- 'UNLESS' -->
{{^someNonArray}}
<h1> {{someNonArray}} is falsey! </h1>
{{/someNonArray}}<!-- Expressions --> {{ name ? name : 'You have no name!' }} <div class="button {{ active ? 'active' : 'inactive' }}"></div> <div name="{{name}}"></div> <!-- Block expressions --> {{# foo == 'bar' && bar == 'foo' }} Foo is bar and bar is foo! {{/}} {{^ typeof bar is 'string'}} Bar is not a string! {{/}}
<!-- Current view methods --> {{ $view.getProduct( product ) }} <!-- Filters --> {{ $filter.capitalize( name ) }} {{ $filter.orderBy( someArray, 'name' ) }} <!-- Parent view --> {{ $parent.foo.bar }} {{ $parent.$parent.foo.bar }} <!-- App --> {{ $app.foo == 'bar' ? foo : bar }} <!-- Singletons --> {{# $user.type == 'brand' }} You're a brand, here are some brand options! {{/}} {{# $user.type == 'publisher' }} Hello publisher! {{/}} <!-- Routes --> {{# $router.path[0] == 'share' && $router.params.foo == 'bar' }} We're on the shar epage and foo is bar! {{/}} {{# $router.route == 'share/photo' }} You're sharing a photo! {{/}} <!-- Mix and match --> {{# shareType == 'photo' && $user.type == 'brand' }} Something special just for brands shareing photos {{/}}
Base extends Ractive to allow multiple arguments with object getters
<button on-click="someButtonWasClicked"></button> <button on-hover="set: 'foo', bar"></button> <button on-touchend="activate: foo"></button>
class View extends Base.View someButtonWasClicked: (e) -> $clickedButton = $ e.currentTarget activate: (name) -> name # => 'foo'
<button outlet="foo"></button>
class View extends Base.View construcotr: -> super # First way to bind @on 'click:foo', -> bind: # Second way to bind 'click:foo', -> # Simplest way to bind onClickFoo: (e) ->
Extend the functionality of any type of module. Configurable at the Base (global) level, app level, and the per module level (also by module type). The ultimate goal is to distill applications development to basic configuration, through the use of building and applying reusable components. This is heavily inspired by grunt.
The ultimate goal here is to maximize code reusability across applications, provide basic utilities that help this process (modeled after the grunt apis). Ultimately this library will be broken down into the Base core and a suite of plugins to assemble the features you want (e.g. ractive templating, state handling, etc). Eventually nearly every feature herein should be moved to a plugin so applications can be assembled with as much or as little as they want/choose, and can swap out any part or piece at any time (e.g. use another template library, use a different state handler, etc)
Base.plugins.view.defaults.lazyLoad = true App.plugins.defaults.state = true class View extends Base.View plugins: ractive: true lazyLoadImages: true fadeInImages: className: 'fade' selector: '.lazy-loaded'
# Plugin code runs on initialize, is called in the context of the module # it is being applied to, and can retutn methods to apply to the module Base.view.plugin 'ractive', (view, config) -> @ractive = new Ractive el: @el, template: @template, data: @toJSON() @ractive.bind Ractive.adaptors.backboneAssociatedModel @state @on 'render', => @ractive.render() # Applies to all classes Base.plugin 'state', (module, config) -> @state = new Base.State @state.on 'all', (eventName, args...) => @trigger.apply @, ["state:#{eventName}"].concat args # You can return methods to apply to the module getState: (name) -> @state.get name setState: (name, value) -> @state.set name, value App.view.plugin 'fadeInImages', (view, config) -> @on 'render', -> # You can use config.className or apply config as a function to set defaults config = config className: 'hide', selector: 'img' $images = @$ config.selector $images.addClass config.className $images.on 'load', (e) => $(e.target).removeClass config.className # Plugins can also apply to specific module types App.plugin ['view', 'collection', 'model'], 'state', -> # Plugins can also have dependences App.view.plugin 'ractive', ['view:state'], ->
Web components are custom HTML tags with special behaviors for making application markup dead simple. These can range from basic simplications (e.g. '' as a simpler form of typing ) to highly dynamic components (e.g. that automatically creates and destroys subviews as a paired collection changes)
<base-view type="foo" foo="bar"></base-view> <base-collection subject="picts" view="pict"></base-collection> <base-icon name="foo"></base-icon> <base-switch name="bar"></base-switch>
# add child views with simple markup Base.component 'view', ($el, attributes) -> view = new app.views[attributes.type] _.extend attributes, parent: @ @insertView view # For a dynamic list of views that updates when a collection changes Base.component 'collection', ($el, attributes) -> View = app.views[attributes.view] collection = @get attributes.subject @listenTo collection, add: (model) => @insertView new View parent: @, model: model remove: (model) => @destroyView model: model reset: => @destroyViews() # For simpler markup of sprited icons generated by grunt-glue Base.component 'icon', ($el, attributes) -> $el.append "<i class='icon sprite-#{attributes.name}'></i>" # For IOS style switches Base.component 'switch', ($el, attributes) -> $input = $ "<input type='checkbox' type='switch' name='#{attributes.name}>'" $input.on 'click', => $el.prop 'checked', $input.prop 'checked'
class View extends Base.View constructor: -> render: -> super @insertView new SomeView @insertView '.some-selector', new SomeView @subView new SomeView
<!-- This is equivalent to parnetView.subView new MyViewName foo: 'bar' --> <base-view type="MyViewName" foo="bar"></base-view>
class MyView extends Base.View render: -> super # broadcasts an event to all children @broadcast 'rendered' # emits an event to all children @emit 'rendered' # broadcasts and emits an event to all parents and children @trigger 'rendered' # Runs when any child emits 'rendered' onChildRendered: -> # Runs when any child named 'myOtherView' emits 'rendered' onChildMyOtherViewRendered: -> # Runs when any immediate child emits 'rendered' onFirstChildRendered: -> # Runs when any view (parent or child) nadmed 'myOtherView' rendered onMyOtherViewRendered: -> onChildChangeActive: -> class MyOtherView extends Base.View render: -> super @emit 'rendered' # These are all valid ways of binding to the same events described above @on 'child:rendered', -> @on 'firstChild:rendered', -> @on 'child:myThirdView:rendered', -> @on 'myThirdView:rendered', -> @on 'child:change:active', -> class MyThirdView extends Base.View rener: -> @emit 'rendered' # Runs when any parent broadcasts 'rendered' onParentRendered: -> # Runs when a parent named 'MyView' broadcasts 'rendered' onParentMyViewRendered: -> # Runs only when this views first (closest) parent broadcasts 'rendered' onFirstParentRende: ->red
# All emitted and broadcasted events inject a first # argument, a Base.Event (similar to a DOM event object) # that gives listeners some extra information and actions class View extends Base.View onChildChangeActive: (e) -> if e.target.is 'listItem' # Stop this event from further propagating (to parents # if the event was emitted, to children if the event was # broadcasted) e.stopPropagation() # Sets e.defaultPrevented to true e.preventDefault() # in this case the currentTarget is this view if e.currentTarget is @ true
view.children # => Base.List (evented array) of children view.parent # => view's immediate parent view.findView 'name' # => first view named 'name' view.findViews 'name' # => array of subviews named 'name' view.childView 'name' # => first immediate child named 'name' view.childViews 'name' # => array of immediate children with name 'name' view.parentView 'name' # => first parent with name 'name' view.parentViews 'name' # => array of parents with name 'name' # All view accesors also take objects view.findViews model: model view.parentView foo: 'bar', bar: 'foo' # All view accessors can also take iterators (functions) view.childView (view) -> view.isActive() view.parentViews (view) -> view.
view.children.on 'add', (childView) -> # a new child view as added view.children.on 'remove', (childView) -> # a child view was removed view.childre.non 'reset', -> # children were reset
class PhotoModel extensd Base.Model constructor: -> super # Relations can be added dynamically @addRelation 'activeProduct', ProductModel # Relations can be configured relations: productsTagged: ProductsCollection class Model extends Base.Model constructor: -> super @set 'photos', [ url: 'hi.png' ] @get 'photos' # => Photo list with one photo model in it @get 'photos[0]' # => a Photo model @get 'photos[0].url' # => 'hola.png' @set 'photos[0].url, 'foo.com/bar.png @get('photos').add url: 'hello.png' @get('photos').reset() @on 'add:photos', -> # a photo model was added @on 'reset:photos', -> # the photos collection was reset @on 'remove:photos', -> # a photo was removed @on 'change:photos[0]', -> # this first photo model changed @on 'change:photos[*]', -> # any photo model changed @on 'change:photos[0].url', -> @on 'change:photos[*].url', -> # Infinite nestings are supported @get 'photos[0].productsTagged[0].id' @set 'photos[0].productsTagged[0].id', newId @on 'change:photos[0].productsTagged[0].id', -> # Syntax sugar for listening for above events onChangePhotosUrl: -> onAddPhotos: -> onResetPhotos: -> onRemovePhotos: -> # Defining relations relations: photos: PhotoList # Views also support relations class View extends Base.View relations: photo: PhotoModel onChangePhotoUrl: -> onChangePhoto: ->
Nearly all Base classes support state models (view, router, model, collection, app, etc). This lets you attach properties to models, collections, routers, etc without clashing with data you want synced with your backend (or other persistence layer such as localStorage)
model.state # => Base.State instance that inherits from Base.Model model.state.get 'active' # view getters and setters forward to the view state model view.get 'active' # equivalent to view.state.get 'active' view.set 'active', true # equivalent to view.state.set 'active', true view.toJSON() # equivalent to view.state.toJSON() view.toggle 'active' # equivalent to view.state.toggle 'active'
class Collection extends Base.Collection # configure state defaults # this is valid for all stated classes (e.g. router, model, collection, etc) stateDefaults: active: false class View extends Base.View # delegates to state.defaults defaults: # delegates to state.relations relations: foo: Foo
All foolowing methods work for all stated classes (routers, models, views, collections, etc)
model.setState 'active', true # equivalent to model.state.set 'active', true model.getState 'active' # equivalent to model.state.get 'active' model.toggleState 'active' # equivalent to model.state.toggle 'active' # other model methods supported model.changedState 'active' model.cloneState() model.unsetState 'active' model.clearSate()
class Model extends Base.Model constructor: -> super # all state events bubble to their parent prefixed by 'state:' @on 'state:change:active', -> onStateChangeActive: ->
<!-- properties in templates are view state properties --> {{hello}} <!-- bind to models that are nested in state (using relations) --> {{model.property}} <!-- bind to model state --> {{model.$state.active}}
Any event on any evented object (model, view, collection, etc) can be subscribed to directly by camelizing the event name.
class View extends Base.View onChange: (stateModel) -> # triggers on 'change:active' onChangeActive: (stateModel) -> # triggers on 'child:change:active' onChildChangeActive: (e) -> # triggers on 'firstParent:render onFirstParentRender: (e) -> # Triggers when @$el was clicked onClick: (e) -> # Triggers when a dom element where outlet="myButton" fired a 'mouseenter' event onMouseenterMyButton: (e) -> class Collection extends Base.Collection # triggers on 'add' onAdd: (model) -> # triggers on 'remove' onRemove: (model) ->
NOTE: this feature is a work in progress, it is not yet ready for usage in development or production
Anything can be stored as a module at the app or base level (though the app level is most recommended).
Base.app 'Pict', -> class Pict extends Base.App constructor: -> super Base.apps.pict is Pict # => true Base.app('pict') is Pict # => true app.view 'Photo', -> class Photo extends Base.View constructor: -> super app.view('Photo') is Photo # => true app.views.Photo is Photo # => true app.model 'MyData' -> class MyData extends Base.Model constructor: -> super app.service 'http', -> ( get: (url, callback) -> $.get url, callback )
Module functions are executed as soon as all dependency requirements are met. At this point the module is set to the value of the function called and any modules whose last remaining dependency is the result of the function just called while also get called.
# Simplest method. A 'PhotoView' arg looks for app.views.Photo, # 'httpService' arg looks for app.services.http, etc app.view 'Foo' (PhotoView, httpService, MyDataModel) -> class Foo extends Base.View get: (url, callback) -> httpService.get url, callback # To avoid minification issues, you can instead separately define # an array of dependencies app.view 'Foo', ['PhotoView', 'httpService'], (Photo, http) -> class Foo extends Base.View constructor: -> super
Base.define 'foobar', (require) -> # Base can figure out your dependencies. It will first # look in the 'app' context for your module, followed by the # global (Base) context foo = require 'foo' bar = app.require 'bar' baz = Base.require 'baz' app.define 'FooView', -> class Foo extends
# Inherits from and supports full Base.View API (below) class App extends Base.App constructor: -> super @get('picts').fetch() defaults: mode: 'grid' relations: picts: PictsCollection
class View extends Base.View # View state defaults defaults: visible: false # Model relations relations: pict: PictModel constructor: -> super # DOM Events can be bound via method syntax sugar onClick: -> onKeypress: -> onWindowResize: -> onDocumentKeypress: -> # Or using dom element outlet names onClickMainImage: -> # When parent or child emits or broadcasts an event onParentFoo: -> onChildFoo: -> # When a parent or child with name 'grid' emit or change events onParentGridChangeActive: -> onChildGridChangeVisible: -> # Listen to change event in property visible onChangeVisible: -> # Listen to a state property of related model pict onChangePictStateActive: -> # Called after render afterRender: -> # Actions to run before a view is destroyed cleanup: -> # Send a response to a child view requesting some information onRequestSomeQuestion: ->
view.subView new View # add a nested view view.insertView '.foo', view # add a nested view at selector view.insertView view # add a nested view at @$el view.findView 'photoGrid' # find a nested view named 'photoGrid' view.findView model: model # find nested view that matches property keyvals view.findView (view) -> # find view via a function view.childView 'foo' # only search immediate children view.is 'foo' # view matches a string, object, or function view.parent # the views parent view view.children # list of subviews - inherits from Base.List view.parentView 'foo' view.childViews 'foo' view.findViews 'foo' view.parentViews 'foo' # emit an event to all parents, seen by parents as 'child:foo' view.emit 'foo', arg1, arg2 # broadcast an event to all children, seen as 'parent:foo' view.broadcast 'foo', arg1, arg2 # callback when a parent broadcasts event 'foo' view.on 'parent:foo', -> # callback when a parent named 'parentViewName' broadcasts event 'foo' view.on 'parent:parentViewName:foo', (e, args...) -> # callback when a child emits an event 'foo' view.on 'child:foo', (e, args...) -> # callback when a child emits an event 'foo' view.on 'child:childViewName:foo', (e, args...) -> # Destroy a view, unbind all listeners, and cleanup view.destroy() # Request a response from a parent, bubbles up to all parents # until one parent has an on 'request:someQuestion' handler or # an onRequestSomeQuestion method. The request is sent to the # first parent with a handler and then the request stops propagating view.request 'someQuestion', (response) ->
# Models are inherited from backbone models class Model extends Base.Model stateDefaults: active: false defaults: price: 0 # models support computed properties that auto update on change # of other properties compute: # 'priceString' will update on every change of 'price' and/or 'currencyCode' priceString: (price, currencyCode) -> getCurrencyString(currencyCode) + price # Confogire nested model associations relations: pict: PictModel
# State Syntax Sugar model.state # => sate model (inherited from Base.State) model.setState 'foo', bar # equivalent of model.state.set 'foo', bar model.getState 'foo' # => 'bar' model.toggle 'foo' # same as model.set 'foo', !model.get 'foo' model.toggleState 'active' # equivalent of model.state.toggle 'active' model.addRelation 'pict', PictModel # Add a nested model model.set 'pict', foo: 'bar' # Creates a new pict model model.get 'pict' # => pictModel object model.get 'pict.foo' # => 'bar' model.set 'pict.foo', 'baz' # Nested Events model.on 'change:pict.foo', -> # valid as expected model.on 'change:pict.products[0].foo', -> # also valid # State Events model.on 'state:change:foo.bar', -> # same as model.state.on 'change:foo.bar'
<!-- Update DOM on model state changes --> {{#model.$state.active }} <h1>I am active!</h1> {{/}}
Singletons inherit from Base.Model and are accessible via the app object and anywhere via templates
JS (in Coffeescript)
app.singleton -> class User extends Base.Singleton defaults: name: 'You have no name!' app.mySingleton is MySingleton # => true
HTML
<!-- All singletons are accessible in templates prefixed by $ --> <h1>{{$user.name}}</h1>
# Collections are inherited from backbone collections class Collection extends Base.Collection stateDefaults: synced: false
# State Syntax Sugar collection.state # => sate model (inherited from Base.State) collection.setState 'synced', true # same as collection.state.set 'foo', bar collection.getState 'synced' # => true collection.toggleState 'synced' # same as collection.state.toggle 'active' collection.on 'state:change:synced', -> # same as model.state.on 'change:foo'
<!-- Update DOM on collection state changes --> {{#collection.$state.synced }} <h1>I've been synced!</h1> {{/}}
An evented array, similar to a backbone collection, but can store any type of data. Used internally to store view children (view.children) and listen to events and changes
class List extends Base.List # Any class (constructor) can be a model that new additions # passed to the list are constructed by. That or set no model # And model: Base.View stateDefaults: active: false # Like any class, a list can support custom methods getActiveChild: (e) -> @find (child) -> child.active # And you can override methods as expected find: (e) -> log 'someone is looking for something!' super
list = new Base.List list.on 'add', -> log 'added!' list.push 'hello!' # triggers the 'add' event # Lists are just typical arrays, you have access to all native # array methods ('forEach', 'map', 'indexOf', etc) and all # underscore array and collection methods as well ('find', 'contains', etc) list[0] # => 'hello' list.find (item) -> _.isString item # => 'hello' list.contains 'hello' # => true list.isEmpty() # => false # Event bubbling (similar to backbone collections) view = new Base.View list.add view list.on 'anEvent', (e) -> log 'a child triggered an event!' # triggers the above log view.trigger 'anEvent' # You can create models of any type, just pass any class (constructor) # as a list's model property list = new Base.List list.model = ListItemView list.push tagName: 'li' list[0] # => a new ListItemView with tagName: 'li' # Lists also support all state methods list.setState 'active', false list.getState 'active' list.toggleState 'active' list.hasState 'active' list.state.toJSON() list.on 'state:change:active', ->
# Inherits from Backbone.Router class Router extends Base.Router stateDeafults: firstRoute: true routes: '*': (route) -> @setState 'firstRoute', false onChangeFirstRoute: (stateModel, value, options) ->
# Supports all stated methods router.setState 'firstRoute', true router.getState 'firstRoute' router.toggleState 'firstRoute'
Inherits from Base.Model The state model used by Base classes. Bubbles all events received to parent as 'state:#{eventName}', so, for example, on its parent you can listen to 'state:change:someAttribute'
State models must be inited with a parent (the owner of the state model in which the state model describes the state of). E.g.
class MyStatedClass constructor: -> @state = new State parent: @ @state.set 'inited', true @state.get 'inited' # => true @listenTo @state, 'change:inited', -> # valid @on 'state:change:inited', -> # also valid
Easier wasy of creating a new stated object. Inherits from Base.Object
class Stated extends Base.Stated constructor: -> super @toggleState 'inited' @setState 'active', true @getState 'active' # => true stateDefaults: inited: false
Simple evented object contrsuctor. Supports full Backbone events API 'on', 'off', 'listenTo', etc
class MyObject extends Base.Object constructor: -> super @on 'foobar', ->
Constructor for base events. Every bubbled and broadcasted view event injects a first argument that is an instanceof Base.Event which supports
view.on 'child:change:foo', (e) -> e.preventDefault() # sets e.defaultPrevented to true e.stopPropagation() # prevents this event from further propagating e.target # reference to view that first triggered the event e.currentTarget # reference to the current view handling the event
Despite the examples herein being in coffeescript, like any other coffeescript library base.js does not require that you write any code in coffeescript. Just use the .extend() method to subclass Base classes
var View = Base.View.extend({ initialize: function () { // Do stuff }, someMethod: function () { // The JS way of calling super (if you ever find you need it) Base.View.prototype.someMethod.apply(this, arguments); // Alternatively, you can use Base._super to help out a bit return Base._super(this, 'someMethod', arguments); } });