scaleApp is a tiny JavaScript framework for scalable One-Page-Applications / Single-Page-Applications. The framework allows you to easily create complex web applications.
You can dynamically start and stop/destroy modules that acts as small parts of your whole application.
scaleApp is based on a decoupled, event-driven architecture that is inspired by the talk of Nicholas C. Zakas - "Scalable JavaScript Application Architecture" (Slides). There also is a little Article that describes the basic ideas.
Unlike Zakas recommendations to abstract DOM manipulations and separating the framework from the base library, scaleApp does not implement any DOM methods.
Instead scaleApp can be extended by plugins. So you can just use one of your
favorite libs (e.g. jQuery) as base library or you are going to implement all
your needed DOM methods into the DOM plugin (scaleApp.dom.coffee
) for a more
clean and scalable architecture.
- loose coupling of modules
- small (about 340 sloc / 10k min / 3.4k gz)
- no dependencies
- modules can be tested separately
- replacing any module without affecting other modules
- extendable with plugins
- browser and node.js support
- flow control
scaleApp itself is very small but it can be extended with plugins. There already are some plugins available:
mvc
- simple MVCi18n
- multi language UIspermission
- take care of method accessstate
- Finite State Machinesubmodule
- cascade modulesdom
- DOM manipulationstrophe
- XMPP communication
You can easily define your own plugin (see plugin section).
Link scaleApp.min.js
in your HTML file:
<script src="scaleApp.min.js"></script>
If you're going to use it with node:
npm install scaleapp
var sa = require("scaleapp");
or use bower:
bower install scaleapp
First of all create a new core instance:
var core = new scaleApp.Core();
core.register( "myModuleId", function( sb ){
return {
init: function(){ /*...*/ },
destroy: function(){ /*...*/ }
};
});
As you can see the module is a function that takes the sandbox as a parameter
and returns an object that has two functions init
and destroy
.
Of course your module can be any usual class with those two functions.
Here an coffee-script example:
class MyGreatModule
constructor: (@sb) ->
init: -> alert "Hello world!"
destroy: -> alert "Bye bye!"
core.register "myGreatModule", MyGreatModule
The init
function is called by the framework when the module is supposed to
start. The destroy
function is called when the module has to shut down.
core.lsModules() // returns an array of module names
core.lsInstances() // returns an array of instance names
scaleApp.lsPlugins() // returns an array of plugin names
You can also init or destroy you module in a asynchronous way:
class MyAsyncModule
constructor: (@sb) ->
init: (options, done) ->
doSomethingAsync (err) ->
done err
destroy: (done) ->
doSomethingAsync (err) ->
done err
core.register "myGreatModule", MyGreatModule
end -> alert "now the initialization is done"
core.start "myGreatModule", callback: end
It's simple:
core.unregister("myGreatModule");
After your modules are registered, start your modules:
core.start( "myModuleId" );
core.start( "anOtherModule" );
You may also want to start several instances of a module:
core.start( "myModuleId", {instanceId: "myInstanceId" } );
core.start( "myModuleId", {instanceId: "anOtherInstanceId" });
If you pass a callback function it will be called after the module started:
core.start( "myModuleId", {callback: function(){ /*...*/ } );
All you attach to options
is accessible within your module:
core.register( "mod", function(sb){
return {
init: function(opt){
(opt === sb.options) // true
(opt.myProperty === "myValue") // true
},
destroy: function(){ /*...*/ }
};
});
core.start("mod", {
instanceId: "test",
options: { myProperty: "myValue" }
});
If all your modules just needs to be instanciated once, you can simply starting them all:
core.startAll();
To start some special modules at once you can pass an array with the module names:
core.startAll(["moduleA","moduleB"]);
You can also pass a callback function:
core.startAll(function(){
// do something when all modules were initialized
});
It's obvious:
core.stop("moduleB");
core.stopAll();
lsModules() // returns an array of all registered module IDs
lsInstances() // returns an array of all running instance IDs
If the module needs to communicate with others, you can use the emit
and
on
methods.
The emit
function takes three parameters whereas the last one is optional:
topic
: the channel name you want to emit todata
: the data itselfcb
: callback method
The emit function is accessible through the sandbox:
sb.emit( "myEventTopic", myData );
You can also use the shorter method alias emit
.
A message handler could look like this:
var messageHandler = function( data, topic ){
switch( topic ){
case "somethingHappend":
sb.emit( "myEventTopic", processData(data) );
break;
case "aNiceTopic":
justProcess( data );
break;
}
};
... and it can listen to one or more channels:
sub1 = sb.on( "somthingHappend", messageHandler );
sub2 = sb.on( "aNiceTopic", messageHandler );
Or just do it at once:
sb.on({
topicA: cbA
topicB: cbB
topicC: cbC
});
You can also subscribe to several channels at once:
sb.on(["a", "b"], cb);
If you prefer a shorter method name you can use the alias on
.
A subscription can be detached and attached again:
sub.detach(); // don't listen any more
sub.attach(); // receive upcoming messages
You can unsubscribe a function from a channel
sb.off("a-channel", callback);
And you can remove a callback function from all channels
sb.off(callback);
Or remove all subscriptions from a channel:
sb.off("channelName");
var task1 = function(next){
setTimeout(function(){next(null, "one");},0);
};
var task2 = function(next){
next(null, "two");
};
scaleApp.util.runSeries([task1, task2], function(err, results){
// result is ["one", "two"]
});
var task1 = function(next){
setTimeout(function(){
next(null, "one", "two");
},0);
};
var task2 = function(res1, res2, next){
// res1 is "one"
// res2 is "two"
next(null, "yeah!");
};
scaleApp.util.runWaterfall([task1, task2], function(err, result){
// result is "yeah!"
});
Link scaleApp.i18n.min.js
in your HTML file:
<script src="scaleApp.min.js"></script>
<script src="scaleApp.i18n.min.js"></script>
If your application has to support multiple languages, you can pass an objects containing the localized strings with the options object.
var myLocalization =
{
en: { welcome: "Welcome", ... },
de: { welcome: "Willkommen", ... },
...
}
...
core.register( "moduleId", myModule, { i18n: myLocalization } );
Now you can access these strings easily trough the sandbox using the _
method.
Depending on which language is set globally it returns the corresponding
localized string.
sb._("myStringId");
You can set the language globally by using the setLanguage
method:
core.i18n.setLanguage( "de" );
You can also set a global i18n object which can be used by all modules:
core.i18n.setGlobal( myGlobalObj );
Within your module you can define your local texts:
function(sb){
init: function(){
sb.i18n.addLocal({
en: {hello: "Hello" },
de: {hello: "Hallo" }
});
},
destroy: function(){}
}
Subscribe to change event:
sb.i18n.onChange(function(){
// update ui
});
Here is a sample use case for using the MVC plugin (in coffeescript).
class MyModel extends scaleApp.Model name: "Noname"
class MyView extends scaleApp.View
constructor: (@model, @sb, @template) -> super @model
# The render method gets automatically called when the model changes
# The 'getContainer' method is provided by the dom plugin
render: -> @sb.getContainer.innerHTML = @template @model
class MyController extends scaleApp.Controller
changeName: (name) -> @model.set "name", name
core.registerModule "myModule", (@sb) ->
init: (opt) ->
# You can use any template engine you like. Here it's
# just a simple function
template = (model) -> "<h1>Hello #{model.name}</h1>"
@m = new MyModel
@v = new MyView @m, @sb, @template
@c = new MyController @m, @v
# listen to the "changeName" event
@sb.on "changeName", @c.changeName, @c
destroy: ->
delete @c
delete @v
delete @m
@sb.off @
core.emit "changeName", "Peter"
The state plugin is an approach to implement a Finite State Machine that can be used to keep track of your applications state.
s = new scaleApp.StateMachine
start: "a"
states:
a: { enter: (ev) -> console.log "entering state #{ev.to}" }
b: { leave: (ev) -> console.log "leaving state #{ev.from}" }
c: { enter: [cb1, cb2], leave: cb3 }
fatal: { enter: -> console.error "something went wrong" }
transitions:
x: { from: "a" to: "b" }
y: { from: ["b","c"] to: "c" }
uups: { from: "*" to: "fatal" }
s.addState "d", { enter: -> } # add an additional state
s.addState { y: {}, z: { enter: cb } } # or add multiple states
s.addTransition "t", { from: "b", to: "d" } # add a transition
s.can "t" # false because 'a' is current state
s.can "x" # true
s.onLeave "a", (transition, eventName, next) ->
# ...
next()
s.onEnter "b", (transitioin, eventName, next) ->
doSomething (err) -> next err
s.fire "x"
s.current # b
If you include the permission
plugin, all Mediator
methods will be rejected
by default to enforce you to permit any message method explicitely.
core.permission.add "instanceA", "on", "a"
core.permission.add "instanceB", "emit", ["b", "c"]
core.permission.add "instanceC", "emit", '*'
core.permission.add "instanceD", '*', 'd'
Now instanceA
is allowed to subscribe to channel a
but all others cannot
subscribe to it.
InstanceB
can emit data on channels a
and c
.
InstanceC
can emit to all channels.
InstanceD
can perform all actions (on
, off
, emit
)
but only on channel d
.
Of course you can remove a permission at any time:
core.permission.remove "moduleA", "emit", "x"
Or remove the subscribe permissions of all channels:
core.permission.remove "moduleB", "on"
This is an adapter plugin for Strophe.js with some helpful features (e.g. automatically reconnect on page refresh).
core.xmpp.login("myjid@server.tld", "myPassword");
core.xmpp.logout();
core.xmpp.jid // the current JID
core.register("parent", function(sb){
var childModule = function(sb){
return({
init: function(){
sb.emit("x", "yeah!");
},
destroy: function(){}
});
});
return({
init: function(){
sb.sub.register("child",childModule);
sb.permission.add("child", "emit", "x");
sb.sub.on("x",function(msg){
console.log("a child send this: " + msg);
});
sb.sub.start("child");
},
destroy: function(){}
});
});
core.start("parent");
// the "parent" module starts a child within the init method
core.stop("parent");
// all children of "parent" were automatically stopped
sb.mixin(receivingClass, givingClass, override=false)
sb.countObjectKeys(object)
- dom - basic DOM manipulations (currently only used for
getContainer
)
scaleApp.registerPlugin
# set the ID of your plugin
id: "myPlgin"
# define the core extensions
core:
myCoreFunction: -> alert "Hello core plugin"
myBoringProperty: "boring"
# define the sandbox extensions
sandbox: (@sb) ->
appendFoo: -> @sb.getContainer.append "foo"
# define base extensions
base:
globalHello: -> "Hello"
# define methods for module changes
on:
instantiate: ->
destroy: ->
Usage:
core.myCoreFunction() # alerts "Hello core plugin"
class MyModule
constructor: (@sb) ->
init: -> @sb.appendFoo() # appends "foo" to the container
destroy: ->
You can find some example modules in src/modules/
.
If you want scaleApp bundled with special plugins type
grunt custom[:PLUGIN_NAME]
e.g. cake custom:dom:mvc
creates the file scaleApp.custom.js
that
contains scaleApp itself the dom plugin and the mvc plugin.
Mediator
: do not clone objects any more (do it manually instead)- drop
subscribe
,unsubscribe
,publish
from Mediator API (useon
,off
andemit
instead) - added a
Core
class that can be instantiated - new submodule plugin
- emit events on module state changes
- improved permission and i18n plugins
- grunt as build systemt
- added waterfall flow control method
- improved permission plugin
- improved state plugin (thanks to Strathausen)
- added xmpp (stropje.js) plugin
- added a simple clock module
- added bower support
- added this changelog
- bug fixes
- added support for async. callback of the
publish
method - added amd support
- bug fixes
- added permission plugin
- ported specs to buster.js
- support for global i18n properties
- support for async. and sync. module initialization
- simpified Mediator code
- bugfixes
- added
lsModules
andlsInstances
- improved README
- run tests with jasmine-node instead of JSTestDriver
- added travis testing
- improved README
- bugfixes
- improved Mediator
- ported to Coffee-Script
- removed jQuery dependency
- bugfixes
- improvements
- first release
npm test
WARNING: the demo is totally out of date!
You can try out the sample application that is build on scaleApp. Also have a look at the source code.
scaleApp is licensed under the MIT license. For more information have a look at LICENCE.txt.