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?