app/assets/javascripts/luca/containers/container.coffee in luca-0.9.8 vs app/assets/javascripts/luca/containers/container.coffee in luca-0.9.9
- old
+ new
@@ -1,5 +1,150 @@
+# The Luca.Container is the heart and soul of the Luca framework
+# and the component driven design philosophy. The central idea
+# is that every component should be designed as an isolated unit
+# which completely encapsulates its features. It should not know about
+# other components outside of it.
+#
+# It is the responsibility of a `Luca.Container` to define its
+# child `@components`, render them, and broker communication between them
+# in response to events which occur in the user interface.
+#
+# A common use case for this would be a page which has a filter form, and
+# a grid of search results. The fields in the filter form are used to
+# filter the table. Neither the form or the table know about each other,
+# since both can be used in other contexts. A `Luca.Container` would be used
+# to relay events from the form to the table, and in doing so create a higher
+# level component which can be extended and re-used.
+#
+# #### Using a container to combine a Filter View and Results Table
+#
+# form = Luca.register "App.views.FilterForm"
+# form.extends "Luca.components.FormView"
+#
+# form.contains
+# type: "text"
+# label: "Filter by"
+# name: "filter_text"
+# ,
+# type: "button"
+# className: "filter"
+# value: "Filter"
+#
+#
+# form.defines
+# toolbar: false
+#
+# Elsewhere, we have a table that lists records in a collection:
+#
+# table = Luca.register "App.views.ResultsTable"
+# table.extends "Luca.components.TableView"
+# table.defines
+# striped: true
+# collection: "components"
+# columns:[
+# header: "Component Class"
+# reader: "class_name"
+# ,
+# header: "Component Type Alias"
+# reader: "type_alias"
+# ]
+#
+# We can join these two components together by declaring their relationship
+# in a `Luca.Container`. Remember the components we defined above are just
+# prototypes. We can override specific instance configuration and properties
+# in our container.
+#
+# #### Container Example
+#
+# container = Luca.register "App.views.ComponentFinder"
+# container.extends "Luca.Container"
+#
+# # This is the same as defining a components property on the component.
+# # The type alias is derived from the name of the component. It is
+# # a short hand way of referencing a component you might reuse a lot.
+# container.contains
+# type: "filter_form"
+# role: "filter"
+# ,
+# type: "results_table"
+# # change the prototype's default
+# striped: false
+# role: "results"
+# filterable: true
+#
+# # A Container will generally define some component event bindings
+# # and handler methods to handle the communication between its sub
+# # components. By default a container is able to access events
+# # from all of its descendants in the hierarchy.
+# container.defines
+# # These will be applied to each of our components.
+# defaults:
+# attributes:
+# "data-attribute": "whatever"
+#
+# componentEvents:
+# # Any time any of our child components emit
+# # the on:change event, pass it to the filterTable method
+# "* on:change" : "filterTable"
+#
+# # Communicates between the filter and the table's
+# # underlying collection. NOtice the use of the @role
+# # property. It automatically creates getter helpers for us.
+# filterTable: ()->
+# filter = @getFilter()
+# results = @getResults()
+# # filter.getValues() is a hash of each field and its value
+# results.applyFilter( filter.getValues() )
+#
+# ### DOM Layout Configuration
+#
+# Another responsibility of the container is to structurally layout its
+# child components in the DOM. There are a number of different
+# options available depending on how you need to do this. By default,
+# a `Luca.Container` will simply append the @$el of all of its views
+# to its own.
+#
+# The `Luca.components.Controller` is a container which hides every page
+# but the active page. Similarly, there is the `Luca.containers.TabView`
+# which does the same thing, but renders a tab selector menu for you. You
+# can create any type of interface you want using containers.
+#
+# To make this easy for you, you can do a few different things:
+#
+# #### Use the Twitter Bootstrap Fluid Grid
+#
+# container = Luca.register "App.views.ColumnLayout"
+# container.extends "App.views.ComponentFinder"
+#
+# container.contains
+# span: 4
+# type: "filter_form"
+# role: "filter"
+# ,
+# span: 8
+# type: "results_table"
+# role: "results"
+#
+# container.defines
+# rowFluid: true
+#
+# #### Using a layout template with CSS Selectors
+# If you find yourself needing a container view with a complicated
+# visual layout, you can provide your own DOM template as a `@bodyTemplate`
+# and assign each child view in `@components` to its own specific CSS selector.
+#
+# ...
+# container.contains
+# role: "filter"
+# container: "#filter-wrapper-dom-selector"
+# ,
+# role: "results"
+# container: "#results-wrapper-dom-selector"
+# ...
+# container.defines
+# # assumes the template will provide the CSS selectors used above
+# bodyTemplate: "layouts/custom_template"
container = Luca.register "Luca.Container"
container.extends "Luca.Panel"
container.triggers "before:components",
@@ -9,40 +154,99 @@
"after:layout",
"first:activation"
container.replaces "Luca.Container"
-container.defines
- className: 'luca-ui-container'
+container.publicConfiguration
+ # @components should contain a list of object configurations for child view(s)
+ # of this container. The values specified in the configuration object will override the
+ # values defined as properties and methods on your view prototypes.
+ #
+ # There are special properties you can define in your components configuration items
+ # that will effect the container:
+ #
+ # - role: will create a camelized getter for you on the container. e.g. when role is `my_custom_role`,
+ # the container will have a method `getMyCustomRole()` that returns that child view.
+ #
+ # - name: a name for the child view. this allows you to access the component by name using
+ # the find() method on the container.
+ #
+ # - type: a type alias from the component registry. type alias are underscore'd strings
+ # matching the component class name. e.g. App.views.MyCustomView type alias is `my_custom_view`
+ #
+ # - component: a convenience property for setting type, role, and name to be equal.
+ components:[]
- componentTag: 'div'
+ # The `@defaults` property is an object of configuration parameters which will be set
+ # on each child component. Values explicitly defines in the components config will
+ # take precedence over the default.
+ defaults: {}
- componentClass: 'luca-ui-panel'
+ # The `@extensions` property is useful when you are subclassing a container view
+ # which already defines an array of components, and you want to specifically override
+ # properties and settings on the children. The `@extensions` property expects either:
+ #
+ # An object whose keys match the names of the `@role` property defined on the child components.
+ # The value should be an object which will override any values defined on the parent class.
+ #
+ # or:
+ #
+ # An array of objects in the same array position / index as the target child view you wish to extend.
+ extensions: {}
- isContainer: true
-
- rendered: false
-
- components: []
-
# @componentEvents provides declarative syntax for responding to events on
# the components in this container. the format of the syntax is very similar
# to the other event binding helpers:
#
- # component_accessor component:trigger
+ # `component_accessor component:trigger`
#
- # where component_accessor is either the name of the role, or a method on the container
- # which will find the component in question.
- #
- # myContainer = new Luca.Container
- # componentEvents:
- # "name component:trigger" : "handler"
- # "role component:trigger" : "handler"
- # "getter component:trigger" : "handler"
- #
+ # where component_accessor is either the name of the component, or a the role
+ # property on the component, component:trigger is the event that component fires.
+ # handler is a method on the container which will respond to the child component event.
+ # <pre>
+ # myContainer = new Luca.Container
+ # componentEvents:
+ # "name component:trigger" : "handler"
+ # "role component:trigger" : "handler"
+ # "getter component:trigger" : "handler"
+ # components:[
+ # name: "name"
+ # ]
+ # </pre>
componentEvents: {}
+container.privateConfiguration
+ className: 'luca-ui-container'
+
+ # This is a convenience attribute for identifying
+ # views which are luca containers
+ isContainer: true
+
+ # if set to true, we will generate DOM elements
+ # to wrap each of our components in. This should
+ # generally be avoided IMO as it pollutes the DOM,
+ # but is currently necessary for some container implementations
+ generateComponentElements: false
+
+ # if set to true, the DOM elements which wrap
+ # our components will be emptied prior to rendering
+ # the component inside this container.
+ emptyContainerElements: false
+
+ # if @generateComponentElements is true, which tag should this
+ # container wrap our components in?
+ componentTag: 'div'
+
+ # if @generateComponentElements is true, which class should we
+ # apply to the container elements which wrap our components?
+ componentClass: 'luca-ui-panel'
+
+ rendered: false
+
+
+
+container.privateMethods
initialize: (@options={})->
_.extend @, @options
# aliases for the components property
@components ||= @fields ||= @pages ||= @cards ||= @views
@@ -58,56 +262,16 @@
validateContainerConfiguration(@)
Luca.View::initialize.apply @, arguments
- # Rendering Pipeline
- #
- # A container has nested components. these components
- # are automatically rendered inside their own DOM element
- # and then CSS configuration is generally applied to these
- # DOM elements. Each component is assigned to this DOM
- # element by specifying a @container property on the component.
- #
- # Each component is instantiated by looking up its @ctype propery
- # in the Luca Component Registry. Then the components are rendered
- # by having their @render() method called on them.
- #
- # Any class which extends Luca.View will have its defined render method
- # wrapped in a method which triggers "before:render", and "after:render"
- # before and after the defined render method.
- #
- # so you can expect the following, for any container or nested container
- #
- # DOM Element Manipulation:
- #
- # beforeRender()
- # beforeLayout()
- # prepareLayout()
- # afterLayout()
- #
- # Luca / Backbone Component Manipulation
- #
- # beforeComponents()
- # prepareComponents()
- # createComponents()
- # beforeRenderComponents()
- # renderComponents() ->
- # calls render() on each component, starting this whole cycle
- #
- # afterComponents()
- #
- # DOM Injection
- #
- # render()
- # afterRender()
- #
- # For Components which are originally hidden
- # ( card view, tab view, etc )
- #
- # firstActivation()
- #
+ # Removing a container will call remove on all of the nested components as well.
+ remove: ()->
+ Luca.View::remove.apply(@, arguments)
+ @eachComponent (component)->
+ component.remove?()
+
beforeRender: ()->
doLayout.call(@)
doComponents.call(@)
Luca.Panel::beforeRender?.apply(@, arguments)
@@ -142,11 +306,10 @@
specialComponent.container += "[data-container-assignment='#{ containerAssignment }']"
prepareComponents: ()->
container = @
-
_( @components ).each (component, index)=>
ce = componentContainerElement = @componentContainers?[index]
# support a variety of the bad naming conventions
ce.class = ce.class || ce.className || ce.classes
@@ -203,12 +366,21 @@
# you can also just pass a string representing the component_type
component = if Luca.isComponent( object )
object
else
+ # if a component is tagged with a @component property
+ # we assume this is the kind of singleton component
+ # and set the type, role and name to the same value (if they're blank)
+ if object.component? and not (object.type || object.ctype)
+ object.type = object.component
+ object.name ||= object.component
+ object.role ||= object.component
+
object.type ||= object.ctype
+ # guess the type based on the properties
if !object.type?
# TODO
# Add support for all of the various components property aliases
if object.components?
object.type = object.ctype = 'container'
@@ -236,12 +408,11 @@
@componentsCreated = true
map
- # Trigger the Rendering Pipeline process on all of the nested
- # components
+ # Trigger the Rendering Pipeline process on all of the nested components
renderComponents: (@debugMode="")->
@debug "container render components"
container = @
@@ -258,25 +429,27 @@
# try in the window context. this is almost always certainly a bug
# so look into wtf is going on and which components are problematic
containerElement = @$( component.container ).eq(0) if containerElement.length is 0
+ if @emptyContainerElements is true
+ containerElement.empty()
+
containerElement.append( component.el )
component.trigger "after:attach"
component.render()
+ component.rendered = true
catch e
console.log "Error Rendering Component #{ component.name || component.cid }", component
if _.isObject(e)
console.log e.message
console.log e.stack
throw e unless Luca.silenceRenderErrors? is true
- #### Container Activation
- #
# When a container is first activated is a good time to perform
# operations which are not needed unless that component becomes
# visible. This first activation event should be relayed to all
# of the nested components. Components which hide / display
# other components, such as a CardView or TabContainer
@@ -289,32 +462,11 @@
# passing as arguments the component itself, and the component doing the activation
unless component?.previously_activated is true
component?.trigger?.call component, "first:activation", component, activator
component.previously_activated = true
- #### Underscore Methods For Working with Components
- _: ()-> _( @components )
-
- pluck: (attribute)->
- @_().pluck(attribute)
-
- invoke: (method)->
- @_().invoke(method)
-
- select: (fn)->
- @_().select(fn)
-
- detect: (fn)->
- @_().detect(attribute)
-
- reject: (fn)->
- @_().reject(fn)
-
- map: (fn)->
- @_().map(fn)
-
- registerComponentEvents: (eventList)->
+ registerComponentEvents: (eventList, direction="on")->
container = @
for listener, handler of (eventList || @componentEvents||{})
[componentNameOrRole,eventId] = listener.split(' ')
@@ -329,36 +481,76 @@
unless component? and Luca.isComponent(component)
console.log "Error registering component event", listener, componentNameOrRole, eventId
throw "Invalid component event definition: #{ componentNameOrRole }"
- component?.bind eventId, @[handler], container
+ component[direction](eventId, @[handler], container)
+container.publicMethods
+ # Returns an underscore.js object that wraps the components array
+ _: ()-> _( @components )
+ # Return the value of attribute of each component
+ pluck: (attribute)->
+ @_().pluck(attribute)
+
+ # Invoke the passed method name on each component
+ invoke: (method)->
+ @_().invoke(method)
+
+ # Select any component for which the passed iterator returns true
+ select: (iterator)->
+ @_().select(iterator)
+
+ # Find the first matching component for which the passed iterator returns true
+ detect: (iterator)->
+ @_().detect(iterator)
+
+ # Return a list of components without the components for which the passed iterator returns true
+ reject: (iterator)->
+ @_().reject(iterator)
+
+ # Run the passed iterator over each component and return the result in an array
+ map: (fn)->
+ @_().map(fn)
+
+ # Returns a list of nested components which are also containers
subContainers: ()->
@select (component)->
component.isContainer is true
roles: ()->
- _( @allChildren() ).pluck('role')
+ _( @allChildren() ).chain().pluck('role').compact().value()
allChildren: ()->
children = @components
- grandchildren = _( @subContainers() ).invoke('allChildren')
+
+ grandchildren = _( @subContainers() ).map (component)->
+ component?.allChildren?()
+
_([children,grandchildren]).chain().compact().flatten().value()
+ # Find a direct component on this card by its name.
+ find: (name)->
+ _( @components ).detect (c)->
+ c.name is name
+
findComponentForEventBinding: (nameRoleOrGetter, deep=true)->
@findComponentByName(nameRoleOrGetter, deep) || @findComponentByGetter( nameRoleOrGetter, deep ) || @findComponentByRole( nameRoleOrGetter, deep )
findComponentByGetter: (getter, deep=false)->
_( @allChildren() ).detect (component)->
- component.getter is getter
+ component?.getter is getter
findComponentByRole: (role,deep=false)->
_( @allChildren() ).detect (component)->
- component.role is role or component.type is role or component.ctype is role
+ component?.role is role or component?.type is role or component?.ctype is role
+ findComponentByType: (desired,deep=false)->
+ _( @allChildren() ).detect (component)->
+ desired is (component.type || component.ctype)
+
findComponentByName: (name, deep=false)->
_( @allChildren() ).detect (component)->
component.name is name
findComponentById: (id, deep=false)->
@@ -387,14 +579,21 @@
eachComponent: (fn, deep=true)->
_( @components ).each (component, index)=>
fn.call component, component, index
component?.eachComponent?.apply component, [fn,deep] if deep
- indexOf: (name)->
+ indexOfComponentName: (name)->
names = _( @components ).pluck('name')
_( names ).indexOf(name)
+ indexOf: (nameOrComponent)->
+ if _.isString(nameOrComponent)
+ return @indexOfComponentName(nameOrComponent)
+
+ if _.isObject(nameOrComponent)
+ _( @components ).indexOf( nameOrComponent )
+
activeComponent: ()->
return @ unless @activeItem
return @components[ @activeItem ]
componentElements: ()->
@@ -423,10 +622,12 @@
_.compact matches
_.flatten( components )
+container.register()
+
# This is the method by which a container injects the rendered child views
# into the DOM. It will get passed the container object, and the component
# that is being rendered.
Luca.Container.componentRenderer = (container, component)->
attachMethod = $( component.container )[ component.attachWith || "append" ]
@@ -462,26 +663,25 @@
createGetterMethods = ()->
container = @
childrenWithGetter = _( @allChildren() ).select (component)->
- component.getter?
+ component?.getter?
_( childrenWithGetter ).each (component)->
- container[ component.getter ] ||= ()->
- component
+ container[ component.getter ] ||= ()-> component
createMethodsToGetComponentsByRole = ()->
container = @
childrenWithRole = _( @allChildren() ).select (component)->
- component.role?
+ component?.role?
_( childrenWithRole ).each (component)->
getter = _.str.camelize( "get_" + component.role )
- container[ getter ] ||= ()->
- component
+ getterFn = ()-> component
+ container[ getter ] ||= _.bind(getterFn, container)
doComponents = ()->
@trigger "before:components", @, @components
@prepareComponents()
@trigger "before:create:components", @, @components
@@ -497,12 +697,9 @@
@registerComponentEvents()
validateContainerConfiguration = ()->
true
-
-# Private Helpers
-#
# indexComponent( component ).at( index ).in( componentsInternalIndexMap )
indexComponent = (component)->
at: (index)->
in: (map)->
if component.cid?