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
Use
blocks.query()
method and pass an object which could be accessed in the HTML. Calling the method executes all data-query attributes and {{expressions}}blocks.query({ type: 'firstName', name: 'John Doe' });
data-query
attribute on a element is used to call query methods. Syntax is the same as calling a method in JavaScript with support for chaining<input data-query="val(name).setClass(type)" /> <div data-query="setClass(type)" />
{{expressions}}
are used to render a value from the model into the DOM<input class="{{type}}" /> <h2>{{name}}</h2>
blocks.observable()
is the object you create when you need a value that will be always synced in the model and in the DOMblocks.query({ // the value will be always in sync with the DOM and vice versa name: blocks.observable('John Doe') });
Use
blocks.Application
and its MVC(Model-View-Collection) structure to create better architecture and maintainability for your applicationvar 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:
- creates an express app listening to the specified port (default 8000)
- expects an
app
folder where to placeindex.html
file which will be the main application view - register a middleware which handles all requests and servers all routes that you have created in your client-side code
- when a route is satisfied it renders the page executing your client-side application logic on the server
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?
MV-ish
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.
Server-side rendering
Client-side frameworks suffer major drawbacks like:
- Lack of SEO optimization because content is rendered on the client and search engines do not execute JavaScript
- Slow performance because entire app logic is executed and rendered on the user machine on every page load
- Laggy experience represented by the content changing between the page first loads and the time DOM is ready
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
- Two-way data binding using observables that could be extended to bring any level of customization needed
- Build in queries to work with the DOM while never actually touching it
- Intuitive CSS3 Transitions & Animations and advanced, cross browser compatible JavaScript animations
- Model-View-Controller architecture for building complex applications
- Routing mechanism for single-page applications
- Easy to setup connections to a service that automatically observes and changes via observables
- Lazy loading of resources
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.
- Platform. not just a framework - frameworks like Angular, Ember, React handle only the client-side of things. However we build complete solutions and there should be an easy(while optional) way to work with databases and services.
- Virtual DOM - jsblocks architecture is build on top of a Virtual DOM and we want to share it with you. The final goal is cleaner way to work with the DOM while having better performance than directly touching the DOM.
- jsvalue(utility library) as a separate library - the built-in utility library will be soon available as a separate project
- Templating engine - it's also exciting to know that we are working on other interesting projects that aim to ease developers lives
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, otherwisenull
$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:
- changing - fires before an observable value is changed. Could be canceled by returning false
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());
- change - fires after an observable value have been changed
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:
- push
- pop
- reverse
- shift
- sort
- splice
- unshift
- concat
- slice
- join
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:
- adding - fires before adding items to the array. Could be canceled by returning false
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);
- add - fires after items have been added to the array
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);
- removing - fires before removing items from the array. Could be canceled by returning false
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();
- remove - fires after items have been removed from the array
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>
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>
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>
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:
- Easy to use
- Transitions are played only in browsers that support CSS3 Animations which speeds your animation in modern browsers and doesn't slow down old browsers
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:
- Easy to use - does not require additional setup and is the default option
- Cross browser solution and consistent across users
Cons:
- Appends # to the end of the URL and could be not so intuitive for the end user
- As the server couldn't know about the #hash value it could not prerender the appropriate content and leaves the client code to
pushState history
var App = blocks.Application({
history: 'pushState'
});
Pros:
- It is harder to implement
Cons:
- Inconsistent behavior cross browsers
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:
- Improve performance because by default they are requested only when needed
- Improve code architecture and maintainability by extracting HTML in different files
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:
- validate() - call this method to validate and update all values below
user.username.validate();
- valid() - is the validators have successfully passed
<span data-query="visible(!username.valid())"></span>
- errorMessage() - the error message for the failed validator. If the validation have succeeded the value is empty string.
<h2>{{username.errorMessage}}</h2>
- errorMessages() - all error messages for all failed validators. If the validation have succeeded the array is empty.
<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.
- validate() - call this method to call all Model Property validate() methods and update all their values and the Model observables described below
user.validate();
- valid() - are all validators for all properties in the model have succeeded
<h2 data-query="visible(!profile.valid())"> There is at least one validation error in the Model. </h2>
- validationErrors() - all error messages from all failed validators in the entire Model
<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
})
});