legs-js
legs-js copied to clipboard
A JavaScript homage to the RobotLegs micro-architecture.
legs.js
A JavaScript homage to the RobotLegs micro-architecture.
What legs.js is
Totally being re-done, that's what! More to come soon...
Legs.Class
legs-js comes with a super-simple class implementation inspired by projects like js-oo and klass. It's not nearly as full-featured as either of those libraries, but it is enough to define and extend classes with accurate inheritance.
All factory methods on Legs.Class
take an attributes hash that will be applied to the prototype of the class.
To create a new subclass of Legs.Class
, use extend
. The result is a type that can be instantiated and inherits from Legs.Class
.
var MyClass = Legs.Class.extend( { foo : 'bar' } );
var instance = new MyClass( );
instance.foo // => 'bar'
You can use create
to instantiate a new object with an attributes hash applied:
var MyClass = Legs.Class.extend( );
var instance = MyClass.create( { foo : 'bar' } );
instance.foo // => 'bar'
You can also use include
to apply attributes to a class prototype after the class has been created.
var MyClass = Legs.Class.extend( );
MyClass.include( { foo : 'bar' } );
var instance = new MyClass( );
instance.foo // => 'bar'
All members in the legs-js extend Legs.Class
.
Legs.Context
A context defines all parts of your application. It enumerates the application actors, defines the commands that will allow them to communicate, and provides a startup routine where you can wire everything together.
The anatomy of a Legs.Context
looks like this:
var Context = Legs.Context.extend(
{
// events is the only special package name in that all members of this object get merged onto
// the application event bus. This is a good place to define event types so your app doesn't
// feel too magic-stringy
events :
{
// event type definitions here...
}
// other package names inside a context are totally arbitrary and not even necessary
// but are definitely more familiar to those coming from RL proper
actors :
{
// actor definitions here...
},
commands :
{
// command definitions here...
},
foobarbaz :
{
// seriously, name these packages whatever you want to...
},
// startup is a special function that should be overridden here. This is the proper
// place to wire up your application actors.
startup : function( )
{
}
} );
Context instances auto-startup by default. You can set the autoStartup
property to false to disable this behavior,
which is useful for testing.
var Context = Legs.Context.extend(
{
autoStartup : false,
// ... other stuff
} );
Disabling autoStartup
will still call your startup routine, but it will not dispatch the STARTUP_COMPLETE
event
on the event bus. This way, if you want to test your actors and commands in-context, you can. When you're done testing,
you may either create a new instance of the context and override the autoStartup
property, or you can call the postStartup
method directly:
var Context = Legs.Context.extend(
{
autoStartup : false,
// ... other stuff
} );
// create an instance that will auto-startup
var app = Context.create( { autoStartup : true } );
// OR
// call postStartup manually
var app = new Context( );
app.postStartup( );
Contexts also come with a contextView
property, which defaults to a jquery selector for the document
.
You probably want to get more specific than this, so be sure to define your contextView
in the context definition:
var Context = Legs.Context.extend(
{
contextView : $( '#app-context' )
} );
Again, you can override this value during your tests or whenever else.
var Context = Legs.Context.extend(
{
contextView : $( '#app-context' )
} );
var instance = Context.create( { contextView : $( '#another-context' ) } );
Legs.Injector
Each context comes with its own Legs.Injector
, which is the method by which framework entities can reference each other.
Dependencies are denoted as attributes on your actors prefixed with an underscore whose values are strings. When an injector
finds these, it will try to look up any types that have been mapped to that name and will create a new attribute on the actor
with the same attribute name, but without the underscore. Finally, injection mappings are created in the startup routine.
A simple command with a dependency on an actor will look like this:
var Context = Legs.Context.extend(
{
actors :
{
MyActor : (Class definition here...)
},
commands :
{
MyCommand : Legs.Command.extend(
{
_foo : 'bar',
execute : function( )
{
this.foo; // => an instance of MyActor
}
} )
},
startup : function( )
{
this.injector.mapClass( 'bar', this.actors.MyActor );
this.commandMap.mapEvent( this.events.STARTUP_COMPLETE, this.commands.MyCommand );
}
} );
Here, MyCommand
is mapped to the STARTUP_COMPLETE
event and will fire once the context has finished starting up.
The command is automatically passed through the injector, and the injector finds the attribute _foo
. _foo
has a value
of 'bar'
, the name to which MyActor
has been mapped. The injector will automatically create a new instance of MyActor
and place it on the command instance as foo
(without the underscore).
The injector offers several types of mappings to configure how you want dependencies to be filled:
-
mapValue
maps an instance or literal for injection. The value injected will be the same every time. It's useful for application constants, likethis.injector.mapValue( 'host', 'http://localhost:3000' );
-
mapClass
maps a type or class for injection. The value injected will be a new instance of that class every time. All dependencies on the created instance will be filled recursively. Use it when you will need a new instance of a class for each actor that depends on it, likethis.injector.mapClass( 'validator', this.utils.Validator );
-
mapSingleton
is likemapClass
, but once the instance is created, every other dependency will be filled with that same instance. This is useful for when you only want one of something, likethis.injector.mapSingleton( 'service', this.services.Facebook )
Legs.Events
Each context has one event bus through which framework actors communicate. This differs from the idea of having every framework actor be an event dispatcher. The idea here is that each framework actor can send an event out into the context, you're not supposed to know where it came from. This approach decouples the sender from the receiver.
Legs.Actor
Legs.Actor
is the base class for framework actors. An "actor" might be defined as any member of the application that may need to
communicate back into the application via the event bus. All actors and subclasses come with a dependency on the context's event bus
as well as a sugar trigger
method that will trigger an event on the context. Here's how a user model update method might work:
User : Legs.Actor.extend(
{
update : function( attributes )
{
$.extend( this, attributes );
this.trigger( this.events.USER_UPDATED );
}
} )
Legs.Command
Commands can be thought of as one unit of work within the application. Commands are mapped to events that occur within the application,
and are created and executed as soon as that event type is triggered. Commands extend Legs.Actor
, so they also have the ability to trigger
events on the context if they need to.
A Legs.Command
should override the execute
method. This method will be called automatically after all command dependencies have been filled.
UpdateUserCommand : Legs.Command.extend(
{
_user : 'user', // assumes 'user' has been mapped in the injector
execute : function( attributes )
{
this.user.update( attributes );
}
} )
The execute
method will be called with all payload values that come with the event. Commands are stateless, and can be considered discarded
after they have executed. If the event occurs again in the application, a new instance of the mapped command will be created and executed.
Legs.CommandMap
The command map is the mechanism for mapping commands to events in an application. A simple command can be mapped to the STARTUP_COMPLETE
event of a context like this:
var Context = Legs.Context.extend(
{
commands :
{
StartupCompleteCommand : Legs.Command.extend(
{
execute : function( )
{
alert( 'all started up, yo' );
}
} )
},
startup : function( )
{
this.commandMap.mapEvent( this.events.STARTUP_COMPLETE, this.commands.StartupCompleteCommand );
}
} );
commandMap.mapEvent
takes an optional third parameter oneShot
, which if true will automatically unmap the command after it has been
executed once.
Testing with legs-js
TODO