jsblocks - Better JavaScript MV-ish Framework (original) (raw)

Getting started

Just copy the HTML below and run it. Now you have your first working example build with jsblocks.

<html>
  <head>
    <script src="http://jsblocks.com/jsblocks/blocks.js"></script>
    <script>
      blocks.query({
        name: blocks.observable()
      });
    </script>
  </head>
  <body>
    Name: <input placeholder="Enter your name" data-query="val(name)" />
    <h1>Hello, my name is {{name}}!</h1>
  </body>
</html>

Basic concepts

blocks.query({  
  type: 'firstName',  
  name: 'John Doe'  
});  
<input data-query="val(name).setClass(type)" />  
<div data-query="setClass(type)" />  
<input class="{{type}}" />  
<h2>{{name}}</h2>  
blocks.query({  
  // the value will be always in sync with the DOM and vice versa  
  name: blocks.observable('John Doe')  
});  
var App = blocks.Application();  
// create Models, Collections and Views here  

Server-side rendering

Getting started

Download the jsblocks-seed project and run the commands below:

npm install

npm run node

Understanding server-side rendering basics

var blocks = require('blocks');

// creates the server which will automatically handle server-side rendering
blocks.server();

Creating a blocks.server() does couple of things:

blocks.server()

var blocks = require('blocks');
var server = blocks.server({
    // the port at which your application will be run
    port: 8000,

    // the folder where your application files like .html, .js and .css are going to be
    // the value is passed to express.static() middleware
    static: 'app',

    // caches pages result instead of executing them each time
    // disabling cache could impact performance
    cache: true,

    // provide an express middleware function or an array of middleware functions
    // use: [compression(), bodyParser()]
    use: null
});

// returns the `express` app `blocks.server` is using internally
var app = server.express();

Why use jsblocks?

Model-View-Controller, Model-View-Collection, Model-View-ViewModel, Model-View-Whatever, Hierarchical Model-View-Controller or nothing at all - jsblocks has you covered. A Model-View-Collection layer stands on top of the main DOM syncing core. This MVC layer is extremely powerful and enables easy creation of complex applications. The MVC layer is also modular, so you could remove it if you don't need the extra functionality, making your code base lighter.

Debugging experience

The debugging experience is a major factor that is often overlooked. It brings an easier learning curve and faster development cycles. This is why jsblocks concentrates a lot of effort in building a great debugging experience. Let's take a look at an example of what jsblocks offers.

debugging experience

Server-side rendering

Client-side frameworks suffer major drawbacks like:

There is a way to address all this issues by executing the entire client-side logic on the server. This approach enables the content to be sent fully rendered from the server eliminating mentioned problems. Also, jsblocks makes performance improvements so performance will no longer heavily depend on the user machine.

And it's super easy to setup:

var blocks = require('blocks');
var server = blocks.server();

For detailed documentation on server-side rendering you could head up here.

Fast

Performance is important. For a framework that manages your whole site, it's even more important. And for data-heavy operations, it is absolutely essential. This is why jsblocks has an architecture designed with performance in mind. We beat the competition and also provide server-side rendering. We are fast now but we have even bigger plans for the future.

Modular

jsblocks is made out of modules. Each module is independent and could be optionally removed from the framework. You can decide your needs and preferences and optionally remove any unneeded modules. Get your own custom jsblocks build containing only the modules you need here.

Built-in utility library

Since a major part of our application logic is moved to the client, we need tools to handle complex data manipulations. These tools should be intuitive and fast. The jsvalue module that is part of jsblocks achieves extremely high performance by using advanced, dynamic code generation to create the fastest methods on the fly.

Let's look at an example that shows the power of the utility library:

// returns true because the second condition is true
blocks
    .range(1, 100)
    .map(function (value) {
        return value * 2;
    })
    .filter(function (value) {
        return value % 2 == 0 && value < 50;
    })
    .contains(0)
    .or()
    .contains(22);

Feature rich

Forward thinking

We have a lot of things planned for the future, and the readers who are interested enough to read to the bottom are a perfect audience.

data-query syntax

jsblocks aims to reduce learning process by making data-query syntax as familiar to technical people as possible. Calling a query is like calling a method which returns a query so chaining is possible.

<ul data-query="each(items).setClass(listClassName)">
  <li data-query="html(title)"></li>
</ul>
<!-- if and ifnot queries expect queries as second and third parameters -->
<h1 data-query="if(false, html(title), html('no title available'))"></h1>
<h1 data-query="ifnot(true, html('no title available'))"></h1>
<!-- event queries expect callback functions as a parameter -->
<div data-query="click(handler).on('touchend', handler)">
</div>

{{expressions}} syntax

Expressions are a easy way to display a value in the HTML without using a data-query.

<script>
  blocks.query({
    userRole: 'admin',
    profile: {
      username: 'jdoe'
    },

    price: 2.01,
    ratio: 0.76
  });
</script>

<!-- expressions could also be found in attributes except the style attribute¹ use the css-data-query instead-->
<h3 class="user {{userRole}}">
  Welcome, {{profile.username}}.
<h3>

<!-- you could place logic in expressions -->
<input value="{{price * ratio}}" />

¹Caused by behaviour of Internet Explorer removing invalid css (e.g. expressions) from the dom.

Context properties

In order to understand the idea behind context properties take a look at this example:

<div data-query="each(items)">
  <!-- $index is equal to the index for the current iteration -->
  <span>Item number {{$index}}</span>
</div>

All context properties are prefixed with $

But what exactly is a context and when it changes. In the above example the context outside and inside the <ul> element is different. The context have changed after calling each() method.

Let's look at a simple example by using the $this context property which points to the current model object you are in:

<script>
blocks.query({
  // the items that will be iterated
  items: ['first', 'second']
});
</script>
<!-- $this here points to the object passed to the blocks.query() function -->
<span>{{$this}}</span>

<!-- the context inside of the <ul> will be different because of the each() -->
<ul data-query="each(items)">
  <!-- $this here points to the current item that is being iterated from the collection -->
  <li>{{$this}}</li>
</ul>

The above example will produce the following HTML:

<script>
blocks.query({
  items: ['first', 'second']
});
</script>
<!-- $this here points to the object passed to the blocks.query() function -->
<!-- its toString() method is called which results in [object Object] -->
<span>[object Object]</span>
<ul data-query="each(items)">
  <!-- in the first iteration $this points to the first item in the array which is 'first' -->
  <li>first</li>
  <!-- in the second iteration $this points to the second item in the array which is 'second' -->
  <li>second</li>
</ul>

Available properties

$this

Type - *

Description - points to the value for the current context you are in


$index

Type - blocks.observable

Description - points to the index for the current each() iteration

Note: only available in each() queries, otherwise null


$root

Type - *

Description - points to the object passed to the

Note: $root = $this until context is changed


$parent

Type - *

Description - points to the parent context value

Note: null until context is changed


$parents

Type - Array

Description - array of all parent context values

Note: $parents.length = 0 until context is changed


parentContext

Type - Context

Description - points to the parent context object which contains all properties defined in the table

Note: null until context is changed


$view

Type - View

Description - points to the current view you are in

Note: null until view() data-query is called

Observable introduction

Observables are the way to achieve two-way data binding. When an observable value is changed the DOM updates and vice versa.

function Model() {
  this.firstName = blocks.observable('John');
  this.lastName = blocks.observable('Doe');
  this.age = blocks.observable('23');

  this.fullName = blocks.observable(function () {
    return this.firstName() + ' ' + this.lastName();
  }, this);
}

blocks.query(new Model());
<div>
  My name is {{firstName}} {{lastName}} and I am {{age}} years old.
</div>
<div>
  My name is {{fullName}} and I am {{age}} years old.
</div>
<h2>
Change name  
</h2>
FirsName: <input data-query="val(firstName)" />
<br />
Last Name: <input data-query="val(lastName)" />
<br />
Age: <input data-query="val(age)" />

The most commonly used observable is the one that is created when you provide a primitive value or an object to the blocks.observable() method.

var fullName = blocks.observable('My name');

Accessing the observable value is as easy as calling a function. Regardless in code or in the HTML.

var firstName = blocks.observable('John');
alert('My name is ' + firstName());
<script>
  blocks.query({
    firstName: blocks.observable('John')
  });
</script>
<div data-query="setClass(firstName())">
  My name is {{firstName()}}!
</div>

Events

All observables have some common events you could subscribe to:

var number = blocks.observable(4).on('changing', function (newValue, oldValue) {
  // newValue - is the value that will be assigned to the observable
  // oldValue - is the current value of the observable before it will be changed

  if (newValue < 0) {
    // return false will prevent the value from changing
    return false;  
  }
});

// in the current scenario the value will not be changed because it is a negative integer
number(-1);
// alerts 4
alert(number());

// now the value will be changed successfully
number(2);
// alerts 2
alert(number());
var number = blocks.observable(4).on('change', function (newValue, oldValue) {
  // newValue - is the newly changed value
  // oldValue - is the previous value

  alert(newValue);
  alert(oldValue);
});

// this will alert 3(the new value) and then 4(the old value)
number(3);

Array observable

Observables arrays help you keep a collection of DOM elements synced. Observables arrays are automatically initialized when you provide an array to the blocks.observable() method.

var items = blocks.observable([1, 2, 3]);

Observable arrays support all standard JavaScript array methods that you are used to:

var items = blocks.observable([1, 2, 3]);
items.push(4);

All of the methods above and some additional methods for easier work with observable arrays are described in the API documentation.

Events

Observable arrays have additional events for tracking when adding and removing items from the array:

var items = blocks.observable([3, 5, 7, 9]).on('adding', function (args) {
  // args.items - the items that are going to be added to the array
  // args.index - the index where the items will be added
  // args.type = 'adding'

  return false;
});

// the values will not be added to the array because of the return false in the handler
items.push(11, 13);
var items = blocks.observable([3, 5, 7, 9]).on('add', function (args) {
  // args.items - the items that have been added to the array
  // args.index - the index where the items have been added
  // args.type = 'add'
});

// the values will be added to the end of the array
items.push(11, 13);
var items = blocks.observable([3, 5, 7, 9]).on('removing', function (args) {
  // args.items - the items that are going to be removed from the array
  // args.index - the index from where the items will be removed
  // args.type = 'removing'

  return false;
});

// the value will not be removed from the array because of the return false in the handler
items.pop();
var items = blocks.observable([3, 5, 7, 9]).on('remove', function (args) {
  // args.items - the items that have been added to the array
  // args.index - the index where the items have been added
  // args.type = 'remove'
});

// the values will be removed from the array
items.pop();

Dependency observable

Dependency observables make it easy to automatically update observable which is constructed from another observable. Let's take a look at examples with the two types of dependency observables.

Read-only dependency observable

Read only dependency observables are created by providing a function to the blocks.observable() method. The framework will automatically detect which observables are used by immediately calling the function.

var firstName = blocks.observable('John');
var lastName = blocks.observable('Doe');
var fullName = blocks.observable(function () {
  return firstName() + ' ' + lastName();
});

Read-write dependency observable

There are some cases when you need a read\write dependency observable to control more complex scenarios. You could create such an observable by passing an object with get() and set() methods to the blocks.observable() method.

var firstName = blocks.observable('John');
var lastName = blocks.observable('Doe');
var fullName = blocks.observable({
  get: function () {
    return firstName() + ' ' + lastName();
  },

  set: function (value) {
    var splits = value.split(' ');
    firstName(value[0]);
    lastName(value[1]);
  }
});

Extending observable functionality

blocks.observable.extend() method could be used to extend a particular observable functionality. Great example is value formatting. Let's build value formatter.

Before using extend() you will need to implement the formatting logic. After that the formatter could be used for any observable:

// Creating an extender. It should be added as a property on the blocks.observable object
blocks.observable.formatter = function (formatCallback) {
    // The idea behind the formatter is to create an additional property called displayValue
    // which could be used in data-queries to show the formatter value and in the same time
    // use the original observable to work with the raw integer value
    // <span>{{goldPrice.displayValue}}</span>

    // creating a displayValue property on the observable because this points to the observable
    // creating a dependency observable so we could control value assignments
    this.displayValue = blocks.observable({

        // sets the value by calling the format callback and assigning its result
        set: function (value) {
            // this points to the displayValue observable
            this._value = formatCallback(value);
            alert('formatted');
        },

        // returns the value
        get: function () {
            // this points to the displayValue observable
            return this._value;
        }
    });

    this.on('change', function () {
        this.displayValue(this());
    });

    this.displayValue(this());

    // it is possible to return an observable here which will return the observable when calling this extender
    // this is not necessary here because by default if you do not return observable the extender will return itself
    // so writing return this; will have the same effect as leaving it without the return
};

// initializing a formatter, providing the format callback parameter
// the code alerts formatted because the value is formatted initially
// Note: every parameter after the first is passed to the extender function
var goldPrice = blocks.observable(3).extend('formatter', function (value) {
    // a simple example that parses the value and appends to zeros to the end
    return parseInt(value, 10) + '.00';
});

// alerts 3
alert(goldPrice());

// alerts 3.00
alert(goldPrice.displayValue());

// alerts formatted
goldPrice(41);

// alerts 41
alert(goldPrice());

// alerts 41.00
alert(goldPrice.displayValue());

Filtering, Sorting and Paging

Filtering

<script>
  function Model() {
    this.filterValue = blocks.observable();
    this.names = ['Anne', 'Nancy', 'Janet', 'Margaret', 'Steven', 'Michael', 'Laura'];

    // creating the items and using the filter extender
    // this creates a view property which contains the filtered result
    this.items = blocks.observable(this.names).extend('filter', this.filterValue);
  }

  blocks.query(new Model());
</script>
<input data-query="val(filterValue)" />

<!-- Using the items.view property which contains the filtered items -->
<ul data-query="each(items.view)">
  <li>{{$this}}</li>
</ul>

Read filtering extender API

Sorting

<script>
  function Model() {
    this.names = ['Nancy', 'Janet', 'Margaret', 'Anne', 'Steven', 'Michael', 'Laura'];
    this.items = blocks.observable(this.names).extend('sort');
  }

  blocks.query(new Model());
</script>

<h2>Not sorted data</h2>
<ul data-query="each(items)">
    <li>{{$this}}</li>
</ul>

<h2>Sorted data</h2>
<!-- Using the items.view property which contains the sorted items -->
<ul data-query="each(items.view)">
    <li>{{$this}}</li>
</ul>

Read sorting extender API

Paging

<script>
  function Model() {
    this.data = blocks.range(1, 74);
    this.skip = blocks.observable(0);
    this.take = 10;
    this.pages = blocks.range(1, Math.ceil(this.data.length / this.take) + 1);
    this.items = blocks.observable(this.data)
                      .extend('skip', this.skip)
                      .extend('take', this.take);

    this.setPage = function (e) {
      this.skip((blocks.dataItem(e.target) - 1) * this.take);
    }
  }

  blocks.query(new Model());
</script>
<!-- Using the items.view property which contains the paged items -->
<ul data-query="each(items.view)">
    <li>{{$this}}</li>
</ul>
<div data-query="each(pages)">
    <a href="#" data-query="click($root.setPage)" style="margin-right: 5px;">{{$this}}</a>
</div>

Read paging extender API

Queries

1. if

2. ifnot

Event queries

The events below are available out of the box as direct queries. In all other cases you could use the on() data-query to subscribe to an event.

// mouse events
'click dblclick mousedown mouseup mouseover mousemove mouseout';

// keyboard events
'keydown keypress keyup';

// form events
'select, change, submit, reset, focus, blur';

And here is an example of using the click event:

<script>
blocks.query({
  // the event passed to the handler is normalized like a jQuery event
  // e.target, e.relatedTarget, e.pageX, e.pageY, e.which, e.metaKey are normalized
  clicked: function (e) {
    // this points to the model
    this.clickCount(this.clickCount() + 1);
  },

  clickCount: blocks.observable(0)
});
</script>

<button data-query="click(clicked)">Click me</button>

<span>The button have been clicked {{clickCount}} times.</span>

The on() data-query

Use the on() query when you need an event that is not available out of the box.

<a data-query="on('touchend', mobileClick)"></a>

Creating a custom query

In some cases you will need to create your own data-query method so you could reuse code logic. Let's build a simple example.

blocks.queries.formatPrice = {

  // the value is passed when calling the formatPrice
  update: function (value) {
    // this points to the HTML element
    if (value != null) {
      this.innerHTML = 'formated value: ' + value.toString();
    } else {
      this.innerHTML = '';
    }
  }  
};

And this is how you could use your custom query:

<span data-query="formatPrice(price)"></span>

Building custom queries with performance in mind

blocks.queries.formatPrice = {

  preprocess: function (value) {
    // this points to the blocks.VirtualElement instance

    if (value != null) {
      this.html('formated value: ' + value.toString());
    } else {
      this.html('');
    }
  },

  // the value is passed when calling the formatPrice
  update: function (value) {
    // this points to the HTML element

    if (value != null) {
      this.innerHTML = 'formated value: ' + value.toString();
    } else {
      this.innerHTML = '';
    }
  }  
};

CSS3 Transitions

Using CSS3 Transitions in jsblocks is the perfect and recommended way for animating elements in your page:

Note: The b-show, b-show-end, b-hide, b-hide-end classes are predefined by the jsblocks framework

/* set transition options for all <li> elements */
.item {
  -webkit-transition:0.5s linear all;
  transition:0.5s linear all;
}

/*
* The combination of b-show(start state) and b-show-end(end state) classes with
* opacity starting from 0 and ending at 1 achieves a fade-in effect when filtering
*/

/* b-show class represents the starting state when a item is being showed */
.item.b-show {
  opacity: 0;
}

/* b-show-end class represents the ending state when a item is being showed */
.item.b-show-end {
  opacity: 1;
}

/*
 * The same as the example above but for hiding items:
 * The combination of b-hide(start state) and b-hide-end(end state) classes with
 * opacity starting from 1 and ending at 0 achieves a fade-out effect when filtering.
*/

/* b-hide class represents the starting state when a item is being hidden */
.item.b-hide {
  opacity: 1;
}

/* b-hide-end class represents the ending state when a item is being hidden */
.item.b-hide-end {
  opacity: 0;
}
<input data-query="val(filterValue)" />

<!-- Using the items.view property which contains the filtered items -->
<ul data-query="each(items.view)">
    <li class="item">{{$this}}</li>
</ul>

<script>
  function Model() {
    this.filterValue = blocks.observable();
    this.names = ['Anne', 'Nancy', 'Janet', 'Margaret', 'Steven', 'Michael', 'Laura'];

    // creating the items and using the filter extender
    // this creates a view property which contains the filtered result
    this.items = blocks.observable(this.names).extend('filter', this.filterValue);
  }

  blocks.query(new Model());
</script>

.b-show

Represents the start state of the animation when showing or adding an element on the page


.b-show-end

Represents the end state of the animation when showing or adding an element on the page


.b-hide

Represents the start state of the animation when hiding or removing an element from the page


.b-hide-end

Represents the end state of the animation when hiding or removing an element from the page

CSS3 Animations

CSS3 Animations are a perfect fit when you need more advanced control over your CSS animation.

Let's build the same example seen in CSS3 Transitions but using CSS3 Animations so we could compare the differences.

/* the show animation definition which goes from 0 to 1 opacity to achieve a fade in effect */
@keyframes show {
  from { opacity:0; }
  to { opacity:1; }
}

/* support for webkit browsers */
@-webkit-keyframes hide {
  from { opacity:1; }
  to { opacity:0; }
}

/* the hide animation definition which goes from 1 to 0 opacity to achieve a fade out effect */
@keyframes hide {
  from { opacity:1; }
  to { opacity:0; }
}

/* support for webkit browsers */
@-webkit-keyframes hide {
  from { opacity:1; }
  to { opacity:0; }
}

/* applying the show animation to the <li> item when being showed */
.item.b-show {
  -webkit-animation:0.5s show;
  animation:0.5s show;
}

/* applying the hide animation to the <li> item when being hidden */
.item.b-hide {
  -webkit-animation:0.5s hide;
  animation:0.5s hide;
}
<input data-query="val(filterValue)" />

<!-- Using the items.view property which contains the filtered items -->
<ul data-query="each(items.view)">
    <li class="item">{{$this}}</li>
</ul>

<script>
  function Model() {
    this.filterValue = blocks.observable();
    this.names = ['Anne', 'Nancy', 'Janet', 'Margaret', 'Steven', 'Michael', 'Laura'];

    // creating the items and using the filter extender
    // this creates a view property which contains the filtered result
    this.items = blocks.observable(this.names).extend('filter', this.filterValue);
  }

  blocks.query(new Model());
</script>

JavaScript Animations

Programmatic animations using JavaScript are useful when you need cross browser support or some advanced animation depending on values that change. You are free to use any framework you want when animating using JavaScript.

<script src="http://cdn.jsdelivr.net/velocity/1.1.0/velocity.min.js"></script>
<script>
blocks.query({
  visible: blocks.observable(true),
  toggleVisibility: function () {
    // this points to the model object passed to blocks.query() method
    this.visible(!this.visible());
  },

  fade: function (element, ready) {
    Velocity(element, {
      // this points to the model object passed to blocks.query() method
      opacity: this.visible() ? 1 : 0
    }, {
      duration: 1000,
      queue: false,

      // setting the ready callback to the complete callback
      complete: ready
    });
  }
});
</script>
<button data-query="click(toggleVisibility)">Toggle visibility</button>
<div data-query="visible(visible).animate(fade)" style="background: red;width: 300px;height: 240px;">
</div>

MVC(Model-View-Collection) Introduction

So it's time to understand the jsblocks MVC(Model-View-Collection) part and how to build complex applications with solid architecture. jsblocks MVC is a module and it is not mandatory to build your applications with it. However, it is designed in such a way that you just won't ever need anything else. So let's see it in action.

Creating a jsblocks MVC Application is as easy as calling the blocks.Application method which creates a new Application instance.

// creating a new application
// calling the blocks.Application() method multiple times returns the same application instance so you could access it in different files
var App = blocks.Application();

// create your views, models and collections here

Below is an example build with the MVC module. For additional information you could follow the corresponding documentation for views, models and collections.

var App = blocks.Application();

// place methods and values directly on the application object by using the extend() method
App.extend({
  helloMessage: 'Hello from jsblocks MVC'
});

var Article = App.Model({
  content: App.Property({
    defaultValue: 'no content'
  })
});

var Articles = App.Collection(Article, {
  // place your collection methods here
});

var Profile = App.Model({
  // observable property with required validation
  username: App.Property({
    required: true
  }),

  // observable property with email validation
  email: App.Property({
    email: true
  })
});

App.View('SignUp', {
  articles: Articles([{
    content: 'first article'
  }, {
    content: 'second article'
  }, {
    // no article content so the defaultValue 'no content' will be applied
  }]),

  profile: Profile()
});
<h1>{{helloMessage}}</h1>
<div data-query="view(SignUp)">
    <div data-query="with(profile)">
        <input placeholder="username" data-query="val(username)">
        <span data-query="visible(!username.valid()).html(username.errorMessage)"></span>
        <input placeholder="email" data-query="val(email)">
        <span data-query="visible(!email.valid()).html(email.errorMessage)"></span>
    </div>
    <h3>Articles from other users</h3>
    <ul data-query="each(articles)">
        <li>{{content}}</li>
    </ul>
</div>

View Introduction

Views are a way to divide your application into pieces. Often you will find out that views are most appropriate to represent entire pages in your application and Nested Views to be logic separation in your current view.

Let's take a look at a simple example.

<script>
  var App = blocks.Application();

  App.View('HelloView', {
    helloMessage: 'Hello from View',

    // called when the view is created
    // do any initialization work here
    init: function () {
      this.description = 'I am powerful';
    }
  });
</script>
<div data-query="view(HelloView)">
  <h1>{{helloMessage}}</h1>
  <p>{{description}}</p>
</div>

Views are most powerful when combined with Models and Collections.**

Routing

Routing is the soul of a single-page application. It let's you create pages that correspond to a particular URL(route) and are invisible until the corresponding route is hit.

Let's build an example that will have two pages - Home and Contacts.

var App = blocks.Application();

App.View('Home', {
  // options object contains all properties that define the View behavior
  options: {
    // enabling the View routing and setting it to the root page
    // for a www.example.com the root is the same www.example.com
    route: '/'
  }
});

App.View('Contacts', {
  options: {
    // enabling the View routing and setting to a Contacts route
    // for a www.example.com the route will be found under www.example.com/#Contacts
    route: 'Contacts'
  },

  init: function () {

  }
});

App.View('Product', {
  options: {

    route: 'Product/{{type}}'
  },

  // callback called when the view have been successfully
  routed: function () {

  }
});
<a href="#" data-query="navigateTo(Home)">Home</a>
<a href="#" data-query="navigateTo(Contacts)">Contact Us</a>

<div data-query="view(Home)">
  Home
</div>
<div data-query="view(Contacts)">
  Contacts
</div>
<div data-query="view(Product)">
  Product {{route.type}}
</div>

Routes


'/'

'Contacts'

'Product/{{type}}'

blocks.route('Product/{{type}}').optional('type')

hash vs pushState history

jsblocks have two types of history management to best suite your needs. Both have advantages and disadvantages which are illustrated below:

hash type history

Pros:

Cons:

pushState history

var App = blocks.Application({
  history: 'pushState'
});

Pros:

Cons:

Nested Views

While your application grows you will want to keep with that demand and do not compromise on your architecture and code maintainability. This is where nested views come in handy.

var App = blocks.Application();

App.View('Blog', {

});

// creating a nested view
// first parameter is the parent view
// second parameter is the new nested view name
App.View('Blog', 'Navigation', {

  // defining a message for the navigation
  helloMessage: 'Hello from Navigation'
});

// creating a nested view
// first parameter is the parent view
// second parameter is the new nested view name
App.View('Blog', 'Articles', {

  // defining a message for the articles
  helloMessage: 'Hello from Articles'
});
<div data-query="view(Blog)">
  <div data-query="view(Navigation)">
    {{helloMessage}}
  </div>
  <div data-query="view(Articles)">
    {{helloMessage}}
  </div>
</div>

View - lazy loading

Lazy loading of views are a must when building more complex pages for two reasons:

var App = blocks.Application();

App.View('Documentation', {
  options: {
    // the url property points to the HTML file where the view is located
    url: 'views/documentation.html'
  }
});

Preloading

When specifying an url for a view by default they are requested only when needed. Use the preload property to load the content on page load instead of waiting until is needed.

var App = blocks.Application();

App.View('Documentation', {
  options: {
    url: 'views/documentation.html',

    // this will force the view to be cached when the page loads
    preload: true
  }
});

Models

Models are a way to represent a single item in your View. They are useful because their code logic could be reused multiple times and you could achieve validation and custom formatting of your values.

var App = blocks.Application();

// creating a new Model type
var User = App.Model({
  // called when the Model is created
  init: function () {
    // do any initialization work here
  },

  firstName: blocks.observable(),

  lastName: blocks.observable(),

  // defining a dependency observable that returns the full name of the user
  fullName: blocks.observable(function() {
    return this.firstName() + ' ' + this.lastName();
  })
});

App.View('Profile', {
  profile: User({
    firstName: 'John',
    lastName: 'Doe'
  })
});
<div data-query="view(Profile)">
  <h2>Welcome, {{profile.fullName}}!</h2>
  First Name: <input data-query="val(profile.firstName)" />
  <br />
  Last Name: <input data-query="val(profile.lastName)" />
</div>

Model validation

A Model manages all its Property objects and provide the validate() method and the valid and validationErrors observables.

var App = blocks.Application();

var User = App.Model({
  username: App.Property({
    required: 'Username is required!'
  }),

  email: App.Property({
    email: 'Please provide a valid email!'
  })
});

var user = User({
  username: '',
  email: 'email@gmail'
});

// validate the username and email properties
user.validate();

// alerts 'false' (both username and email failed validation)
alert(user.valid());

// alerts 'Username is required!,Please provide a valid email!'
// validationErrors is an array of all validation error messages
// constructed from extracting the values from all properties errorMessages collection
alert(user.validationErrors());

For more information about validation go here.

Model - plug in a service

Creating a record

var Profile = App.Model({
  options: {
    idAttr: 'id',

    create: {
      url: 'profile/create'
    }
  }
});

var profile = Profile({
    username: 'user1'
});

profile.sync();

Updating a record

var Profile = App.Model({
  options: {
    idAttr: 'id',

    update: {
      url: 'person/update'
    }
  },

  changePassword: function (newPassword) {
    this.password(newPassword);
    this.sync();
  }
});

Deleting a record

var Person = App.Model({
  options: {
    idAttr: 'id',

    destroy: {
      url: 'person/destroy'
    }
  },

  deleteItem: function () {
    this.destroy();
    this.sync();
  }
});

You could find how to populate collection of objects from a service here

Collections

Collections are a way to represent repeating data and allow CRUD operations from a remote service. Collections internally are of type blocks.observable and hold items of type Model.

var App = blocks.Application();

var Users = App.Collection({
  // called when the collection is created
  init: function () {
    // to any initialization work here
  },

  // dependency observable that keeps track of the collection length
  count: blocks.observable(function () {
    return this().length;
  })
});

App.View('Profiles', {
  users: Users([{ username: 'admin' }]),

  username: blocks.observable(),

  addNewUser: function () {
    this.users.push({
      username: this.username()
    });
    this.username('');
  }
});
<div data-query="view(Profiles)">
  Username: <input data-query="val(username)" />
  <button data-query="click(addNewUser)">Add new user</button>
  <ul data-query="each(users)">
    <li>{{username}}</li>
  </ul>
  <h3>Total count {{users.count}}</h3>
</div>

Note: When choosing between using pure observables and Collection consider that pure observables have performance benefits over Collection. However, Collection provide you with a lot of flexibility and is best for your architecture. In general choose pure observables only when performance is a must.

Filtering, sorting & paging a Collection

MVC Collection is an observable so adding filtering, sorting or paging could be done the same way you would do it for an observable.

Note: Filter, sorting and paging extenders create additional 'view' property observable which you could use to display the manipulated data

Filtering

<script>
  var App = blocks.Application();

  var Products = App.Collection({

  });

  var productsData = [{
    name: 'bread'
  }, {
    name: 'sweets'
  }, {
    name: 'soups'
  }];

  App.View('Details', {
    filterValue: blocks.observable(),

    products: Products(productsData).extend('filter', function (value) {
      var filter = this.filterValue();
      return !filter || value.name().indexOf(filter.toLowerCase()) != -1;
    })
  });
</script>
<div data-query="view(Details)">
    <input data-query="val(filterValue)" />

    <!-- Using the items.view property which contains the filtered items -->
    <ul data-query="each(products.view)">
        <li>{{name}}</li>
    </ul>
</div>

Collection - plug in a service

Populating the collection with data

var Articles = App.Collection({
  options: {
    read: {
      url: 'http://your-api.com/articles'
    }
  }
});

var articles = Articles().read();

Create/Update/Delete operations could be done through the Model. More information here

Property

Property is a way to define value in a model or in a collection and they convert to an observable so everything that could be done on an observable could be applied to a property. Let's see an example.

Note: Property have advantages over an observable which are described here

Here is the implementation using an observable

var features = blocks.observable([]).extend('filter', function (value) {
  return value.type == 'feature';
});

Here is the equivalent when using Property

var App = blocks.Application();

var Project = App.Model({
  features: App.Property({

  }).extend('filter', function (value) {
    return value.type == 'feature';
  })
});

Property options

Property have couple of advantages over an observable like validation, field mapping, support for default values and also are easier to setup.

var App = blocks.Application();

var Article = App.Model({
  author: App.Property({

    field: 'Author',

    defaultValue: 'John Doe',

    // changing:
    // change:
    // add:
    // adding:
    // remove:
    // removing:
  }),

  date: App.Property({
    on: {
      changing: function () {

      }
    }
  }),

  info: App.Property({
    value: function() {
      return this.author() + ' ' + this.date();
    }
  })
});

Property validation

Property supports validation. Here is a quick example:

var App = blocks.Application();

var User = App.Model({
  username: App.Property({
    required: 'username is required',

    validateOnChange: true
  }),

  email: App.Property({
    email: 'Please enter a valid email',
    minlength: {
      value: 3,
      message: 'The email should be bigger than 3 symbols'
    },

    maxErrors: 2,

    validateOnChange: true
  })
});

App.View('SignUp', {
  user: User()
});
<div data-query="view(SignUp)">
    <div data-query="with(user)">
        <input data-query="val(email)" placeholder="try entering an invalid mail or value smaller than 3 symbols" style="width:100%">
        <span data-query="visible(!email.valid()).html(email.errorMessages)"></span>

        <br />
        <br />

        <input placeholder="try not entering a value here" data-query="val(username)" style="width:100%;">
        <!-- showing the validation error in a message -->
        <span data-query="visible(!username.valid()).html(username.errorMessage)"></span>
    </div>
    <div data-query="visible(!user.valid())">
        <h2>All validation errors:</h2>
        <ul data-query="each(user.validationErrors)">
            <li>{{$this}}</li>
        </ul>
    </div>
</div>

Validation properties & methods

Each Property have three exposed observables for controlling the validation:

 user.username.validate();  
 <span data-query="visible(!username.valid())"></span>  
 <h2>{{username.errorMessage}}</h2>  
 <ul data-query="each(username.errorMessages)">  
   <li>{{$this}}</li>  
 </ul>  

Additionally, each Model have two observables that collect data from each Property to provide validation data for the entire Model.

 user.validate();  
 <h2 data-query="visible(!profile.valid())">  
   There is at least one validation error in the Model.  
 </h2>  
 <ul data-query="each(username.validationErrors)">  
   <li>{{$this}}</li>  
 </ul>  

Validators

Here is a code example that describes all available validators that are supported out of the box.

var Article = App.Model({
  propertyForValidation: App.Property({
    required: 'This field is required',

    email: 'The field should be a valid email',

    url: 'The field should be a valid URL',

    date: 'The value should be a valid date',

    number: 'The value should be a number',

    digits: 'The value should contain only digits',

    letters: 'The value should contain only letters',

    creditcard: 'The value should be a valid credit card number',

    min: {
      value: 0,
      message: 'The value should be a positive number'
    },

    max: {
      value: 100,
      message: 'Your age should be less than 100 years'
    },

    minlength: {
      value: 6,
      message: 'Your password should be longer than 5 symbols'
    },

    maxlength: {
      value: 19,
      message: 'Your username should be shorter than 20 symbols'
    },

    regexp: {
      value: /[0-9]+ [0-9]+ [0-9]+/,
      message: 'Your telephone should be in three groups of digits separated by space'
    },

    equals: {
      value: '1739',
      message: 'Your hardcoded password does not match'
    },

    asyncValidate: {
      // the function accepts a ready callback which should be called when validation decision could be made
      value: function (ready) {
        // do any async work here
        // example: go to the server to check sign in data

        // pass false or true for validation failure or success
        ready(false);
      },
      message: 'Your username or password is incorrect'
    },

    validate: function (value) {
      var number = parseFloat(value);
      if (blocks.isNaN(number)) {
        return 'Value should be a valid number';
      }
      if (number % 2 == 0) {
        return [
          'Value should not be an even number',
          'Value should be an odd number'
        ];
      }
      return true;
    }
  })
});

Additional validation options

Property have additional properties that control the validation behavior. Take a look at the code comments:

var Product = App.Model({

  phone: App.Property({
    // determines if the validation is fired on every value change or will be called only manually from the validate() method
    validateOnChange: false,

    // determines the max numbers of validation errors to be pushed to the property.errorMessages collection
    maxErrors: 1,

    // determines if the validation will be fired the first time a value is assigned to the property or will wait for validate() to be called
    validateInitially: false  
  })
});