Fork me on GitHub

Subo

A Javascript framework to bring the Presentation-abstraction-control pattern into your Web pages

Live Demos

NumberGuess  -  Sourcecode
TreeMap  -  Sourcecode

Downloads
Source code

S

ubo is a Javascript client-side Web user interface framework that exploits a combination of the Presentation-abstraction-control (PAC), state machine and portlet patterns. A Subo application is a hierarchy of controller-state-machines that run within your browser window. Each controller has an HTML view, a data model, and zero or more child controllers. These are switched on state transitions: on entry, each state activates for its controller a different view, model, and set of child controllers. A view can embed views of child controllers. When a controller receives an event from it's current view, it can update its model, transition, or fire control events up or down the hierarchy. On receiving a control event, the recipient controller can perform a model update, transition or fire further control events, and so on. A Subo application doesn't necessarily need a server, but you can connect it to one if you wish via service objects. Subo was spawned from a defunct XeoLabs framework called Jandal, which was inspired by frameworks like Apache Wicket.

Try it out

Have a play with this live number-guessing game that was built with Subo, then take a look at the source code for it below. If you're using Firefox, then try watching the DOM with FireBug as you play.




var NumberGuess = function(cfg) {
    cfg = cfg ? cfg : {};
    return new Subo.Application({
        name: cfg.name || 'numberguess', // Default name
 
        onStart: function(args) {
            args = args || {};
 
            // Add a service to provide the number to guess. A service can be any kind of object; we call it a
            // service because its intended to provide the service layer to the application.
 
            this.addService('numberGenerator', {
                getNumber : function() {
                    return 5;
                }
            });
 
            // Add a service to get URLs to resources (images etc) relative to the application's home URL.
 
            this.addService('resourceFinder', {
                getUrl : function(name) {
                    return (args.homeUrl) ? args.homeUrl + '/' + name : name;
                }
            });
 
            // Application's root controller just provides a frame around the whole application
 
            this.setRootController(new Subo.Controller({
                name:'frame',
 
                onStart:function() {
 
                    // On start, the root controller adds its sole state
 
                    this.addState(new Subo.State({
                        name:'playing',
 
                        // As soon as it starts, the root controller enters the first State that was added. This state
                        // adds to its Controller a View, a simple frame around everything with a logo, plus a child-Controller
                        // that manages the actual game.
 
                        onEnter:function(args) {
 
                            // You can see below how the View has the child-Controller's HTML embedded within it, containing an image.
                            // We're using the resource finder service to get an absolute URL to the image:
 
                            this.setView(new Subo.View({
                                getHtml: function() {
                                    return '<img src="' + this.getService('resourceFinder').getUrl('logo.jpg') + '"/>' +
                                           '<br/><br/>' + this.getChildControllerHtml('game');
                                }
                            }));
 
                            this.addChildController(new Subo.Controller({
                                name: 'game',
 
                                onStart: function() {
 
                                    this.addState(new Subo.State({
                                        name: 'guessing',
 
                                        onEnter: function() {
 
                                            this.setView(new Subo.View({
                                                getHtml: function() {
                                                    return this.getViewEventForm(
                                                            'guess',
                                                            'Guess a number: <input name="number" type="text" value=""/>'
                                                                    + '<br/><br/><input type="submit" value="Submit"/>'
                                                            );
                                                }
                                            }));
 
                                            this.addViewEventHandler(new Subo.EventHandler({
                                                name: 'guess',
 
                                                onEvent:function(args) {
                                                    var correctNumber = this.getService('numberGenerator').getNumber();
                                                    var testNumber = parseInt(args['number'], 10);
 
                                                    if (isNaN(testNumber)) {
                                                        this.doTransition('incorrect', {message:'That\'s not a number!'});
                                                    } else if (correctNumber > testNumber) {
                                                        this.doTransition('incorrect', {message:'Too low'});
                                                    } else if (correctNumber < testNumber) {
                                                        this.doTransition('incorrect', {message:'Too high'});
                                                    } else {
                                                        this.doTransition('correct');
                                                    }
                                                }
                                            }));
                                        }
                                    }));
 
                                    this.addState(new Subo.State({
                                        name: 'incorrect',
 
                                        onEnter:function(args) {
 
                                            this.setModel({
                                                message : args['message']
                                            });
 
                                            this.setView(new Subo.View({
 
                                                getHtml: function() {
                                                    return '<b>'




                                                            + this.getModel().message
                                                            + '</b><br/><br/><a href="'
                                                            + this.getViewEventUrl('playAgain')
                                                            + '">Play Again</a>';
                                                }
                                            }));
 
                                            this.addViewEventHandler(new Subo.EventHandler({
                                                name:'playAgain',
 
                                                onEvent:function(args) {
                                                    this.doTransition('guessing');
                                                }
                                            }));
                                        }
                                    }));
 
                                    this.addState(new Subo.State({
                                        name: 'correct',
 
                                        onEnter:function(args) {
 
                                            this.setView(new Subo.View({
 
                                                getHtml: function() {
                                                    return '<b>Correct!</b><br/><br/><a href="'
                                                            + this.getViewEventUrl('playAgain')
                                                            + '">Play Again</a>';
                                                }
                                            }));
 
                                            this.addViewEventHandler(new Subo.EventHandler({
                                                name:'playAgain',
 
                                                onEvent:function(args) {
                                                    this.doTransition('guessing');
                                                }
                                            }));
                                        }
                                    }));
                                }
                            }));
                        }
                    }));
                }
            }));
        }
    });
}

Shown below is the code in this page that embeds this example. The DIV tag defines where the example's markup will be rendered. The first script tag loads the Subo framework, while the second loads the example. The third script tag instantiates the example while identifying the DIV to render at. It then starts up the example, passing in an argument specifying the location of the example's directory so that images and CSS can be resolved.