a framework preferring simplicity and clarity

Routing and Controllers

The only markup in the entire project should be in index.html:


      <!doctype html>
      <html>
        <head>
          <script type="text/javascript" src="js/browser.pajama.js"></script>
          <script type="text/javascript" src="js/routes.js"></script>
          <!-- css / font includes should go in here -->
        </head>
      </html>
      

The project kicks off in js/routes.js, it associates URLs with controllers:


      window.onload = function() {
        pjs.defineRoutes(document.body, [
          { path: "", controller: "main" },                        // <-- page #1
          { path: "booking/*/lookup", controller: "viewBooking" }, // <-- page #2
          { path: "booking/*/edit", controller: "editBooking" },   // <-- page #3
          /* more routes go here */
        ]);
      };
      

The first parameter to defineRoutes is the DOM element to contain our app. The second parameter is an array of routes, with each route connecting a path to a controller.


Controller "main" for page #1, demonstrating how to move between pages:


      pjs.defineController("main", function(container) {
        // In 3 seconds, send us to the next page
        setTimeout(function() {
          pjs.route("booking/*/view", 1392);
        }, 3000);
      });
      

Top level controllers are invoked with the top level DOM element abstracted away behind an instance of PjsElement.


Controller "viewBooking" for page #2, demonstrating a URL containing data:


      // Asterisks in a route are read out of the url and passed into the controller
      pjs.defineController("viewBooking", function(container, bookingId) {
        console.log("bookingId is:", bookingId);
        // In 3 seconds, send us to the next page
        setTimeout(function() {
          var bigObject = new Array(1024*1024);
          pjs.route("booking/*/edit", 1392, bigObject);
        }
        }, 3000);
      });
      

Controller "editBooking" for page #3, demonstrating a URL with additional big data:


      // Additional parameters are also passed into the controller
      pjs.defineController("editBooking", function(container, bookingId, bigObject) {
        console.log("bigObject is", bigObject);
      });
      

Data passed around in the routing will persist through page reloads and browser navigation.


Data Binding and Reactive Views

This is how we bind a model's value to an element's attribute:


      myModel = { price: "1234567" };
      var elem = new pjs.element(
        { tag: 'input', type: 'text', name: 'priceInput', value: { price: myModel } }
      );
      container.append(elem);
      

Typing in the input box updates myModel. Changing myModel updates the input box.


This is how we bind DOM events back to a function on the model:


      var myModel = {
        alert: function() { alert("This rocks!"); }
      };
      var elem = new pjs.element(
        { tag: "input", type: "button", click: { alert: myModel } }
      );
      container.append(elem);
      

When the click event fires on the button, the alert function on myModel will be invoked.


Taking things further, consider this example:


      var myModel = {
        counter: 0,
        increment: function() { this.counter++; }
      };
      var elem = new pjs.element(
        { tag: "div", innerHTML: { counter: myModel } },
        { tag: "input", type: "button", click: { increment: myModel } }
      );
      container.append(elem);
      

Clicking the button updates the model, which in turn updates the view. Magic.


We can also bind arrays:


      var myModel = {
        books: [ { name: 'book1', pages: 168 } ]
      };
      var elem = new pjs.element(
        { tag: 'div', contains: { books: myModel }, forEach: function(book) {
          return [ { tag: 'input', value: { name: book  } },
                   { tag: 'input', value: { pages: book } } ];
        } }
      );
      container.append(elem);
      

Pushing a new object into myModel.books will invoke the forEach function, the result of which will be appended to the DOM. Updating the new myModel.books[1] will update the newly appended DOM elements. Typing in the new DOM elements will update the new object in myModel.books. Pop()'ing the new book from the array will remove it from the DOM. And so on.


Other functions on pjs.element:


      var elem = new pjs.element({ tag: 'input', type: 'text' });
      elem.append( { tag: 'span' } );
      elem.append( { tag: 'span' }, { tag: 'div' }, ... );
      elem.append( [ { tag: 'span' }, ... ] );
      elem.prepend( { tag: 'div' } );
      elem.remove();
      elem.clear();
      elem.getValue();                // These five functions are great
      elem.setValue('newValue');      // for writing tests on views!
      elem.fireEvent('click');        // ..
      elem.getAttr('className');      // ..
      elem.setAttr('src', 'blah');    // ..
      var children = elem.getNodes();         // returns: [ PjsElements ]
      var html = elem.toMarkup();             // returns: string of static markup
      document.body.appendChild(elem.dom);    // to insert elements into the DOM
      

Using PajamaJS events:

An event bus is provided to allow models and controllers to interact with each other without retaining object references.


      pjs.defineController("EventReceiver", function(container) {
        pjs.eventWaitAll("HelloWorldEvent", function(data) {
          console.log("HelloWorldEvent ->", data);
          // pjs.route(...)
        });
        pjs.eventWaitOne("HelloWorldEvent", function(data) {
          console.log("HelloWorldEvent ->", data);
        });
      });
      


      pjs.defineModel("EventEmitter", function() {
        this.submitForm = function() {
          pjs.triggerEvent("HelloWorldEvent", { foo: 'bar' } );
          pjs.triggerEvent("HelloWorldEvent", { foo: 'foo' } );
        };
      });
      

Using PajamaJS models:


        pjs.defineModel("ExampleModel", function(arg1, arg2) {
          // Append everything to 'this'
          this.number = 1;
        });

        var instance = new pjs.model("ExampleModel", arg1, arg2);
      

Using PajamaJS views:


        pjs.defineView("ExampleView", function(arg1, arg2) {
          // 'this' is an instance of pjs.element({ tag: 'div' })
          this.append({ innerHTML: 'w00p' });
          
          // This is a nice shorthand to map an array of models into views:
          this.append({ tag: 'div', contains: { books: myModel }, usingView: 'LibraryBook' });
        });

        var instance = new pjs.view("ExampleView", arg1, arg2);
        container.append(instance);
      

Using PajamaJS controllers:


        pjs.defineController("ExampleController", function(arg1, arg2) {
          this.action = function() { };
        });

        var instance = new pjs.controller("ExampleController", arg1, arg2);
        instance.action();
      

Separating the functional from the presentational:


      pjs.defineView("SomeView", function(myModel) {

        this._valueInput = { tag: 'input', value: { someValue: myModel } };
        this._submitButton = { tag: 'input', click: { someFunction: myModel } };

        this.append(
          { id:'container', classes: ['content-box'], contains: [
            { tag: 'img', src: 'images/flow.png' },
            { tag: 'div', classes: ['float-right'], contains: [
              { tag: 'span', text: 'Total Price:' },
              this._valueInput,
              { tag: 'span', classes: ['small-text'], text: '(in pence)' }
            ] },
            { tag: 'div', classes: ['clear-right'], contains: [
              this._submitButton
            ] }
          ] }
        );

      });
      

Check out how we've separated the functional and presentational parts of the view!


Putting it all together:


      <!doctype html>
      <html>
        <head>
          <script type="text/javascript" src="js/browser.pajama.js"></script>
        </head>
      </html>
      


      window.onload = function() {
        pjs.defineRoutes(document.body, [
          { path: "", controller: "main" }
        ]);
      };
      


      pjs.defineController("main", function(container) {
        var myModel = new pjs.model("LibraryModel");
        var myView = new pjs.view("LibraryView", myModel);
        container.append(myView);
      });
      


      pjs.defineModel("LibraryModel", function() {
        this.name = "Default";
        this.books = [ { name: 'brochure', pages: 10 }, 
                       { name: 'poster',   pages: 1  } ];
        this.addBook = function() {
          this.books.push({ name: 'test', pages: Math.random()*100 });
        };
        this.sortBooks = function() {
          this.books.sort(function(a, b) { return a.pages - b.pages; });
        };
        this.popBook = function() {
          this.books.pop();
        };
      });
      


      pjs.defineView("LibraryView", function(myModel) {
        this._container = new pjs.element(
          { tag: 'div', contains: { books: myModel }, usingView: 'LibraryBook' }
        );
        this._addButton = { tag: "input", type: "button", value: "Add", 
                            click: { addBook: myModel } };
        this._sortButton = { tag: "input", type: "button", value: "Sort", 
                             click: { sortBooks: myModel } };
        this._popButton = { tag: "input", type: "button", value: "Pop", 
                            click: { popBook: myModel } };
        this.append(this._container, this._addButton, 
                    this._sortButton, this._popButton);
      });

      pjs.defineView("LibraryBook", function(book) {
        this.append({ tag: 'div', contains: [
          { tag: 'input', value: { name: book } },
          { tag: 'input', value: { pages: book} } 
        ]});
      });