app/assets/javascripts/luca/core/view.coffee in luca-0.9.8 vs app/assets/javascripts/luca/core/view.coffee in luca-0.9.9

- old
+ new

@@ -1,128 +1,280 @@ +# `Luca.View` is an enhanced `Backbone.View` which provides common patterns for view components, +# and various helper methods and configuration conventions. +# +# #### Instance caching / naming +# +# If you provide a `@name` property to your views, they will be accessible by that property +# using the Application helper. +# +# view = new Luca.View(name:"my_view") +# Luca("my_view") === view +# +# #### CSS @className conventions +# +# In order to make it easier to componentize your views, extending from `Luca.View` will +# enable CSS class based inheritance based on the names of the view class. +# +# ##### For Example: +# +# base = Luca.register "App.views.BaseViewClass" +# base.extends "Luca.View" +# base.defines +# className: "some-other-class" +# +# child = Luca.register "App.views.ChildViewClass" +# child.extends "App.views.BaseViewClass" +# child.defines +# myClasses: ()-> +# @$el.attr('class') +# +# view = new App.views.ChildViewClass() +# view.myClasses() #=> "app-base-view-class app-child-view-class some-other-class" +# +# This establishes a convention for css class names, and allows you to componetize your css +# along with the component by joining them based on the name of your view class. When using +# Sass scoping / nesting it fits very nicely together. +# +# #### Internal state machine +# +# Any `Luca.View` class which defines a `@stateful` property will automatically generate a +# `@state` model that can be used to get/set attributes on the view as well as bind to change events on these attributes. +# +# This gives your views a dedicated place to store state, and you can bind to your data models separately +# and update the DOM without confusing the two. +# +# statefulView = Luca.register "App.views.StatefulView" +# statefulView.extends "Luca.View" +# +# statefulView.defines +# # Passing an object allows you to set default values on the @state model. +# stateful: +# attribute: "value" +# +# # Whenever the attribute specified changes, call the specified method. +# stateChangeEvents: +# "attribute" : "onAttributeChange" +# +# onAttributeChange: (stateMachine, attributeValue)-> +# @doSomethingWhenAttributeChanges() +# +# If this type of declarative style isn't your thing, you can still bind to events in code: +# +# view = new App.views.StatefulView() +# view.on "state:change:attribute", (stateMachine, attributeValue)=> @$el.html("New Attribute: #") +# view.set "attribute", "something" +# +# #### Event binding helpers +# +# In addition to the `@stateChangeEvent` bindings documented above, you have available +# to you similar configuration helpers for binding to events emitted by the singletons: +# +# - `Luca.Application` via `@applicationEvents` +# - `Luca.CollectionManager` via `@collectionEvents` +# - `Luca.SocketManager` via `@socketEvents` view = Luca.register "Luca.View" - view.extends "Backbone.View" # includes are extensions to the prototype, and have no special behavior view.includes "Luca.Events", - "Luca.concerns.DomHelpers" + "Luca.concerns.DomHelpers", + "Luca.concerns.DevelopmentToolHelpers" # concerns are includes with special property / method conventions # which customize the components through the use of __initializer and # __included method names. These will be called every time an instance # is created, and the first time the mixin is used to enhance a component. -view.mixesIn "DomHelpers", +view.mixesIn "DomHelpers", "Templating", "EnhancedProperties", "CollectionEventBindings", "ApplicationEventBindings", "StateModel" # Luca.View classes have the concept of special events called hooks -# which allow you to tap into the lifecycle events of your view to +# which allow you to tap into the lifecycle events of your view to # customize their behavior. This is especially useful in subclasses. # -# You can utilize a @hook method by camelcasing the triggers defined below: +# You can utilize a @hook method by camelcasing the triggers defined below: view.triggers "before:initialize", "after:initialize", "before:render", "after:render", "first:activation", "activation", "deactivation" +view.publicConfiguration + # Specifying a `@name` for your views is useful for views which + # there will only be one instance. This allows you to reference + # the view instances by name using the application helper: + # Luca("my_view_name") + name: undefined + + # Setting this property to true will automatically bind the context + # of your event handler methods to the instance of this view. This + # saves you from having to manually do: + # + # Luca.View.extend + # events: + # "click .one" : "oneHandler" + # "click .two" : "twoHandler" + # initialize: ()-> + # _.bindAll(@, "oneHandler", "twoHandler") + # + # Instead: + # + # Luca.View.extend + # autoBindEventHandlers: true + # events: + # "click .one" : "oneHandler" + # + # Optionally, you can define an array of method names you want bound + # to this view: + # + # Luca.View.extend + # bindMethods:["oneHandler","twoHandler"] + # + autoBindEventHandlers: false + + # Supplying configuration to `@_inheritEvents` will ensure that this configuration + # is present on views which extend from this view. In normal Backbone behavior + # the `@events` property can be overridden by views which extend, and this isn't + # always what you want from your component. + _inheritEvents: undefined + + + # Luca.View decorates Backbone.View with some patterns and conventions. -view.defines +view.publicMethods identifier: ()-> (@displayName || @type ) + ":" + (@name || @role || @cid) + # Calls Backbone.View::remove, and removes the view from the + # instance cache. Triggers a "before:remove" event. + remove: ()-> + @trigger("before:remove", @) + Luca.remove(@) + Backbone.View::remove.apply(@, arguments) + initialize: (@options={})-> @trigger "before:initialize", @, @options _.extend @, @options if @autoBindEventHandlers is true or @bindAllEvents is true - bindAllEventHandlers.call(@) + bindAllEventHandlers.call(@) @cid = _.uniqueId(@name) if @name? @$el.attr("data-luca-id", @name || @cid) - + Luca.registry.cacheInstance( @cid, @ ) @setupHooks _( Luca.View::hooks.concat( @hooks ) ).uniq() Luca.concern.setup.call(@) - @delegateEvents() - + @delegateEvents() unless _.isEmpty(@events) + @trigger "after:initialize", @ _.bindAll(@, @bindMethods...) if @bindMethods?.legth > 0 - #### Hooks or Auto Event Binding - # - # views which inherit from Luca.View can define hooks - # or events which can be emitted from them. Automatically, - # any functions on the view which are named according to the - # convention will automatically get run. - # - # by default, all Luca.View classes come with the following: - # - # before:render : beforeRender() - # after:render : afterRender() - # after:initialize : afterInitialize() - # first:activation : firstActivation() - setupHooks: Luca.util.setupHooks + unless _.isEmpty(@_inheritEvents) + for eventId, handler of @_inheritEvents + @registerEvent(eventId, handler) + + debug: (args...)-> + if @debugMode is true or window.LucaDebugMode is true + args.unshift @identifier() + console.log args... + + trigger: ()-> + if Luca.enableGlobalObserver + if Luca.developmentMode is true or @observeEvents is true + Luca.ViewObserver ||= new Luca.Observer(type:"view") + Luca.ViewObserver.relay @, arguments + + Backbone.View::trigger.apply @, arguments + + # Backbone.View.prototype.make is removed in 0.9.10. + # As we happen to rely on this little utility heavily, + # we add it to Luca.View + make: (tagName, attributes, content)-> + el = document.createElement(tagName); + if (attributes) + Backbone.$(el).attr(attributes) + if (content != null) + Backbone.$(el).html(content) + + el + registerEvent: (selector, handler)-> @events ||= {} - @events[ selector ] = handler if _.isObject(selector) - _.extend(@events, selector) - + @events = _.extend(@events, selector) + else + if _.isFunction(handler) || (_.isString(handler) && @[handler]?) + @events[selector] = handler + @delegateEvents() +view.privateMethods + # Returns a reference to the class which this view is an instance of. definitionClass: ()-> Luca.util.resolve(@displayName, window)?.prototype - collections: ()-> + # Returns a list of all of the collections which are properties on this view. + _collections: ()-> Luca.util.selectProperties( Luca.isBackboneCollection, @ ) - models: ()-> + # Returns a list of all of the models which are properties on this view. + _models: ()-> Luca.util.selectProperties( Luca.isBackboneModel, @ ) - views: ()-> + # Returns a list of all of the views which are properties on this view. + _views: ()-> Luca.util.selectProperties( Luca.isBackboneView, @ ) - debug: (args...)-> - if @debugMode is true or window.LucaDebugMode is true - args.unshift @identifier() - console.log args... + # views which inherit from Luca.View can define hooks + # or events which can be emitted from them. Automatically, + # any functions on the view which are named according to the + # convention will automatically get run. + # + # by default, all Luca.View classes come with the following: + # + # - before:render : beforeRender() + # - after:render : afterRender() + # - after:initialize : afterInitialize() + # - first:activation : firstActivation() + setupHooks: Luca.util.setupHooks - trigger: ()-> - if Luca.enableGlobalObserver - if Luca.developmentMode is true or @observeEvents is true - Luca.ViewObserver ||= new Luca.Observer(type:"view") - Luca.ViewObserver.relay @, arguments +# In order to support some Luca apps which rely +# on Backbone.View.make. +view.afterDefinition ()-> + if not Backbone.View::make? + Backbone.View::make = ()-> + console.log "Backbone.View::make has been removed from backbone. You should use Luca.View::make instead." + Luca.View::make - Backbone.View.prototype.trigger.apply @, arguments +view.register() - Luca.View._originalExtend = Backbone.View.extend # Note: # # I will be removing this prior to 1.0. Optimizing for collection based # views does not belong in here, so the deferrable / collection binding stuff -# needs to go. +# needs to go. # # Being able to defer rendering until the firing of an event on another object # is something that does ask for some syntactic sugar though, so need to rethink. -Luca.View.renderStrategies = +Luca.View.renderStrategies = legacy: ( _userSpecifiedMethod )-> view = @ # if a view has a deferrable property set if @deferrable @@ -130,14 +282,15 @@ unless Luca.isBackboneCollection(@deferrable) @deferrable = @collection target ||= @deferrable - trigger = if @deferrable_event then @deferrable_event else Luca.View.deferrableEvent + trigger = if @deferrable_event then @deferrable_event else Luca.View.deferrableEvent deferred = ()-> _userSpecifiedMethod.call(view) + @rendered = true view.trigger("after:render", view) view.defer(deferred).until(target, trigger) view.trigger "before:render", @ @@ -153,21 +306,21 @@ return @ else @trigger "before:render", @ _userSpecifiedMethod.apply(@, arguments) + @rendered = true @trigger "after:render", @ return @ improved: (_userSpecifiedMethod)-> @trigger "before:render", @ - deferred = ()=> _userSpecifiedMethod.apply(@, arguments) - @trigger "after:render", @ + @trigger "after:render", @ console.log "doing the improved one", @deferrable if @deferrable? and not _.isString(@deferrable) throw "Deferrable property is expected to be a event id" @@ -201,10 +354,10 @@ definition bindAllEventHandlers = ()-> for config in [@events, @componentEvents, @collectionEvents, @applicationEvents] when not _.isEmpty(config) - bindEventHandlers.call(@, config) + bindEventHandlers.call(@, config) bindEventHandlers = (events={})-> for eventSignature, handler of events if _.isString(handler) try