Open mehcode opened 11 years ago
In our Chaplin app we've created 'traits' (name is not quite appropriate, but we got used to it) to manage cross-cutting concerns and facade utility libraries/plugins (e.g. keymaster).
module.exports = class ViewTrait
constructor: (options = {}) ->
_(@).extend options
apply: (view) ->
unless (view instanceof View) or (view instanceof CollectionView)
throw new Error 'ViewTrait can only be applied to View or CollectionView'
@_setTraitsAttribute view, this
# configure view here
_setTraitsAttribute: (view, trait) ->
return if trait.constructor.name is 'ViewTrait'
if view.traits
view.traits.push trait.constructor.name
else
view.traits = [trait.constructor.name]
@_setTraitsAttribute view, trait.constructor.__super__
_addFunction: (view, name, f = @[name]) ->
originalFunction = view[name]
if originalFunction and not _(originalFunction).isFunction()
throw new Error "Property #{name} is not a function in #{view}"
f = _(originalFunction).wrap(f)
view[name] = _(f).bind(view, this)
The whole point of having the trait
object passed to the mixed methods is to keep any trait-specific properties out of the view (or whatever) object to achieve separation of concerns and avoid name collision. So we are in fact creating a companion object for each trait application. We keep an array of traits to detect any clashes between them or unmet dependencies that cannot be enforced via trait inheritance.
The traits compose their function with existing ones, so we can wrap an existing dispose
function:
module.exports = class Focusable extends ViewTrait
apply: (view) ->
super
@_addFunction(view, 'beforeFocusLost')
@_addFunction(view, '_focusGained')
@_addFunction(view, 'onFocusLost')
@_addFunction(view, 'dispose')
view.delegate 'click', -> mediator.setFocus view
beforeFocusLost: (originalFunction, trait, deferred) ->
if originalFunction
originalFunction.call(this, deferred)
else
deferred.resolve(true)
_focusGained: (originalFunction, trait) ->
originalFunction.call(this) if originalFunction
mediator.currentFocus = this
dispose: (originalFunction) ->
originalFunction.apply(this) if originalFunction
# if currently focused view is being destroyed then set focus to heirView
mediator.setFocus mediator.heirView if mediator.currentFocus.cid is @cid
We also added some syntactic sugar to base View
that lets us say just @enable Focusable
:
enable: (trait, args...) ->
(new trait(args...)).apply(this)
To sum up, it works pretty well for us, but requires some attention while writing traits to make sure there will be no 'dissonances' in composition, i.e. if the originalFunction
is invoked at right moment etc.
I personally don't like Traits. It's a pattern that never fits quite well in JS, due to language constraints.
I'd rather have it the other way around. For me a plugin should be responsible of enhancing a certain component, such a View
or Model
. Much like @mehcode suggested but instead of creating the mixins from outside, having a Core plugin manager, which would initialize/hook those modules that choose to register
as plugins, and make use of mixins from inside:
define ['plugins/core/core'], (CorePlugin) ->
class ChaplinData extends CorePlugin
name: "Data Manager",
dev: "@Rendez",
alone: true,
type: Chaplin.GENERAL,
###
Standard Extension functionality
###
init: () ->
[...]
register: (defaults) ->
Chaplin.mixin {...}
Here are my thoughts laid out in code.
First from the point-of-view of the user using chaplin and an extension.
# file: lib/chaplin
# requirejs path configuration states that chaplin == lib/chaplin (this file)
Chaplin = require 'vendor/chaplin'
Chaplin.mixin Chaplin.SyncMachine
# Apply all extensions from the following
Chaplin.mixin require 'chaplin-auth'
Chaplin.mixin require 'chaplin-data'
Chaplin.mixin require 'chaplin-forms'
# Apply only the view extension from this because
# we don't like the others
Chaplin.mixin require('chaplin-crazier').View
module.exports = Chaplin
This Chaplin.mixin
call will either take the passed object and apply it (if it
is an extension) or else it will enumerate its properties and apply all found
extensions.
A Chaplin.noConflict
call would be added to reset the internal state of
Chaplin back to no-plugins.
Next from the point-of-view of the extension writer.
# Extension
module.exports = class SyncMachine extends Chaplin.Extension
#! What this extension get applied to:
#! Chaplin[extends[..]] so the following would apply to both
#! Chaplin.Model and Chaplin.Collection
extends: [
'Model'
'Collection'
]
# or extends could be like follows for most cases:
# extends: 'View'
#! What extensions, if any, this requires.
requires: [
]
#! Anything added here is added as an attribute to the applied class
#! like `class::NAME = extension::attributes.NAME`
attributes:
# This becomes Chaplin.Model.state
state: 'unsynced'
#! Anything in here is ensured to be called before the method in question in
#! a pipeline style keeping the order of the @use calls by the chaplin user
methods:
sync: (options) ->
# Facilitate the sync machine
# ... snipped ...
super options
dispose: ->
# Facilitate the sync machine
# ... snipped ...
super
initialize: ->
# Do what you want, have fun I suppose
register: ->
# This is more of a hook to do anything esoteric.
super # Does all of the above comments, etc
# Now anything just weird can be done
I'm not fond of adding metadata like name
or author
inside the extension
defintion though.
Discuss.
I'll get to actually implementing Chaplin.Extension
and friends after I hear some of your thoughts. I don't want to get ahead of myself.
//cc @molily @Rendez @paulmillr
We need to discuss a plugin / extension / component architecture for Chaplin. I
First to talk of extensions. An plugin or extension is something that adds properties or behaviors to one of chaplin's components directly. A Chaplin version of
Backbone.stickit
would be a good example (as it provides and utilizes a bindings hash).I would think we don't want people to extend
Chaplin.View
directly. I'm thinking of something along the lines ofChaplin.mixin
orChaplin.use
(parallels_.mixin
) in which it would take a library say if the library was likeAwesomeChaplin.View
then that class would be mixed in withChaplin.View
, etc intelligently via function composition. What this means is that if such an extension hadAwesomeChaplin.View#initialize
then having it automatically invoked whenChaplin.View#initialize
is invoked would be interesting. Perhaps a method in each chaplin component such asChaplin.View#plugin
that plugin authors would use when creating theirAwesomeChaplin.View
that disallows super calls etc and just composes their methods.In the modular world the end-user (who wanted to make use of extensions) would do something like follows:
Thoughts? Opinions? Better ideas?
Having something like this formalized and documented will help spur the community to develop awesome stuff with Chaplin.
Another thing is simple components or modules. Like an auto-complete box that uses a Chaplin View and Collection View for its components or a login page. Things like this can distributed as modules but the stylesheets must be CSS and the templates must be pre-compiled. Doing this can allow for re-usable modules. We also need to talk about including a modules routes inside of the main applications routes.
Like if a login page had
match 'login', 'login#show'
then our main app would have:And
'account/login'
would match thelogin#show
action from the login module.Just throwing down ideas to spark this discussion. We'll see what comes of it.