bem-node
Single-page web application with node.js, BEM
Install
Use bem-node with our project-stub
How it works
The main principle of bem-node is presenting page layout as bemjson (russian ref) object. Block templates apply on bemjson tree to produce complete layout with data. Then bemjson is serialising to html.
Demo
https://github.com/delfrrr/bem-node-hello-world/commits/demo
File extensions
####js
Browser code
####common.js
Common (client/server) code
####priv.js
Private (server) code
####server.js
Builded node.js application (server)
####bemdecl.js
Application declaration
exports.blocks = [
{block: 'i-console'},
{block: 'i-enb'},
{block: 'i-bem-node'},
{block: 'hello-world'}
];
####deps.js
Block dependencies
({
shouldDeps: [
{block: 'depended-block'}
],
mustdDeps: [
{block: 'must-dependeded-block'}
]
})
BN
Block constructor. It creates i-bem (russian guide, english guide) blocks with static and dom declarations, and bh (russian ref) templates.
BN(blockName)
- blockName {String}
- return {Object} bem block
Returns link on BEM block static methods and properties.
BN.addDecl(blockName , [type], [options])
- blockName {String}
- [type] {String} - must be "page" or "ajax", if defined
- [options] {Object}
- [options.route] {String|RegExp} route for blocks with type="page"
- [options.apiHost] {String} api host for blocks with type="ajax"
- return {BN.Generator} bem block generator
Creates block generator.
BN.Generator.baseBlock(blockName)
Inherits block declaration from base block.
BN.Generator.staticProp(decl)
- decl {Object} static methods and properties
- return {BN.Generator}
Adds static methods and properties (i-bem).
BN.addDecl('example').staticProp({
someProp: 'a',
someMethod: function(){
return this.someProp + 'b';
}
}).done();
BN('example').someMethod();
BN.addDecl('example').staticProp({
someMethod: function(){
return this.__base() + 'c';
}
}).done();
BN('example').someMethod();
this
– inherits i-bem
this.__instances
– array of block dom instances (only for client side code)
this.__lastInstance
– last created block instances (only for client side code)
BN.Generator.instanceProp(decl)
- decl {Object} dom instances methods and properties
- return {BN.Generator}
Adds methods and properties for block dom instance (i-bem.dom)
BN.Generator.blockTemplate(decl)
- decl {Object|Function} define block bh template
- return {BN.Generator}
BN.Generator.elemTemplate(decl)
- decl {Object} define block element bh template
- return {BN.Generator}
Creates bh templates (see bh) for block, elements and it's modifications.
BN.addDecl('example').blockTemplate(function (ctx) {
ctx.content([
{elem: 'item', url: 'item-1', text: 'item 1'},
{elem: 'item', url: 'item-1', text: 'item 2'}
]);
}).elemTemplate({
'item': function (ctx) {
var json = ctx.json();
ctx.content({
block: 'b-link',
url: json.url,
content: json.text
});
}
});
ctx
is instances of bh.Ctx
BN.Generator.dataTemplate(decl)
- decl {Function} define block data template
- return {BN.Generator}
decl
can return promise (see Vow.promise). If promise is fulfilled, other block templates will be applied. If promise is rejected, block will be removed.
BN.addDecl('example').dataTemplate(function(ctx){
return BN('some-ajax-block').get().then(function(data){
ctx.param('data', data)
return Vow.fulfill();
}).blockTemplate(function(ctx){
ctx.content(ctx.json().data.text);
});
})
ctx
is instance of bh.Ctx
BN.Generator.done()
Creates i-bem block from generator and adds bh matchers. All blocks will be created automatically on next event loop. Use this methods only to force block creation.
Page blocks
Page block are controllers for pages.
BN.addDecl('hello-world', 'page', {
route: /^\/$/
}).staticProp({
init: function () {
return this.out('hello world');
}
});
All pages are extended from i-page
block.
BN.addDecl('usual-page', 'page', {
route: /^\/$/
}).staticProp({
init: function (matchers) {
return this.out();
},
update: function(matchers, prevPath, newPath) {
return Vow.fulfill();
},
destruct: function() {
return Vow.fulfill();
}
});
route
is {RegExp} or {String} which can be matched to request url.
init
is called when page route match to request url. Init should return fulfilled promise. Otherwise page returns error.
update
is called when page should be updated on client (new url matched with the same route). By default update
calls init
destruct
is called on client side before current page is going to be replaced with other page.
Page blocks are extended from i-page block.
Ajax blocks
Ajax blocks are kind of models (or data blocks). They can provide data to view blocks with common interface between client and server.
On server: get → _request (server implementation) → REST API
On client: get → _request (client implementation) → ajax request → _request (server implementation) → REST API
BN.addDecl('example-ajax-block', 'ajax', {
apiHost: 'http://api.example.com/v1/'
});
BN('example-ajax-block').get('some/resource', {
params: {
count: 10
}
}).then(function(result){
console.log(result);
});
Ajax blocks are inheriting from 'i-api-request' block. You can extend block methods to provide additional input params and output data processing:
BN.addDecl('node-doc-api', 'ajax', {
apiHost: 'http://nodejs.org/api/'
}).staticProp({
get: function (resource) {
resource = resource + '.json';
return this.__base(resource);
}
});
By default ajax blocks have only GET
method. To provide POST
PUTand
DELETE` you should declare them:
BN.addDecl('example-ajax-block', 'ajax', {
apiHost: 'http://api.example.com/v1/'
}).staticProp({
post: function (resource, options) {
return this._request('post', resource, options)
}
});
You are able to extend ajax block to fetch data from any source (like database).
Existing blocks
Bem-node includes some visual blocks from bem-bl library
Overriding existing blocks
Power of BEM is ability to override almost any method, template or style of block:
BN.addDecl('b-page').blockTemplate(function (ctx) {
ctx.content([
{block: 'b-head'},
ctx.content()
], true);
});
BN('i-page')
This is a base block for page blocks. But some of the static methods can be called from other blocks (issue).
BN('i-page').setTitle(title)
- title {String}
- return {Object} this
Setting page <title/>
on client and on server
BN('i-page').setDescription(text)
- text {String}
- return {Object} this
Setting page description meta tag on client and on server
BN('i-page').setMeta(name, content)
- name {String} name attribute of
<meta/>
- content {String} content attribute of
<meta/>
- return {Object} this
Setting page <meta/>
tags
BN('i-page').addToHead(bemjson)
- bemjson {Object|String}
- return {Object} this
Adds to page <head/>
any content
.init(matchers)
- matchers {Array} result of appling route regexp on url
- return {Vow.promise}
This method is called when page route match to url. Redefine this method to output your page layout. See example in page blocks and i-page.out method. By default page outputs empty string.
.update(matchers, prevPath, newPath)
- matchers {Array} result of appling route regexp on url
- prevPath {String} url path before update
- newPath {String} new url path
- return {Vow.promise}
This method is called when page should be updated on client (new url matched with the same route). By default it calls `this.init(). You can setup selective block updates by redefining this method.
BN.addDecl('node-doc', 'page', {
route: /^\/node-doc\/?(.+)?$/
}).staticProp({
init: function (matches) {
var section = this._getSectionName(matches);
this.setTitle(section + ' – node.js api');
return this.out({
block: 'node-doc',
content: [
{elem: 'toc', content: {block: 'node-doc-toc'}},
{elem: 'section', content: {block: 'node-doc-section', section: section}}
]
});
},
_getSectionName: function (matches) {
return matches[1] || 'documentation';
},
update: function (matches) {
var section = this._getSectionName(matches);
return BN('node-doc-section').updateSection(section);
}
});
.destruct()
Is called when user leaves page on client and page should be destructed. Extend this method for custom calls on destruct.
.out(bemjson)
- bemjson {Object|String} page content
- return {Vow.promise}
On server:
- wrap bemjson with static layout (header, footer, etc); you can override static layout by redefining
getPageJson()
; page content should be placed in i-content
block; - process bemjson
- serialise bemjson to html
- send http response
200 OK
with resulting html
On client:
- process bemjson
- serialise bemjson to html
- update
i-content
block content with resulting html - init blocks inside
i-content
Use it to output page layout from page blocks. Do not call BN('i-page').out(bemjson)
, use only this.out(bemjson)
BN.addDecl('node-doc', 'page', {
route: /^\/node-doc\/?(.+)?$/
}).staticProp({
init: function () {
return this.out({
block: 'node-doc',
content: [
{elem: 'toc', content: {block: 'node-doc-toc'}},
{elem: 'section', content: {block: 'node-doc-section'}}
]
});
}
});
BN('i-content')
This block is a container for pages content. Content inside i-content
can be automatically updated on client.
Block has api to manipulate content inside pages.
BN('i-content').html(bemjson, [isSync=false])
- bemjson {Object|String}
- [isSync=false] {Boolean}
- return {Vow.promise|String} rendered html
Applies all supported templates (bh, bem.json, bemhtml) to bemjson tree and then serialises it to html.
By setting isSync = true
you will get error when rendering blocks with defined dataTemplate
.
BN('i-content').update | replace | append | before(container, bemjson)
- container {Object} jQuery object
- bemjson {Object}
- return {Vow.promise}
Update dom with new content.
BN.addDecl('example').blockTemplate(function(ctx){
}).dataTemplate(function(ctx){
/.. adding some data to params
}).staticProp({
updateBlock: function(param) {
return BN('i-content').update(this.__lastInstance.domElem.parent(), {
block: this._name,
param: param
});
}
});
BN('i-response')
Manages error pages (404, 50x), http redirect and responses.
BN('i-response').send(status, body, [contentType='text/plain'])
- status {Number} http status
- body {String}
- [contentType='text/plain'] {String}
Sends http response.
BN('i-response').json(json)
Responses with json.
BN('i-response').redirect(path)
Redirects to path with 302 status.
BN('i-response').missing()
Responses with 404 status.
BN('i-response').error(err)
- err {Error|HttpError} http status
Responses with error (503 or status, defined in HttpError) and log error.
BN.addDecl('example').dataTemplate(function(ctx){
var resource = ctx.json().resourceParam;
return BN('ajax-block').get(resource).then(function(dataJson){
ctx.param('data', dataJson);
Vow.fulfill();
}).fail(function(err){
BN('i-response').error(err);
return Vow.reject(err);
});
})
BN('i-errors')
Create user errors
new BN('i-errors').CommonError(message)
Constructor for user errors
new BN('i-errors').HttpError(status)
- status {Number} http status
Https error.
var Errors = BN('i-errors');
var ApiError = function (status, debugInfo) {
HttpError.call(this, status);
this.name = 'ApiError';
this.debugInfo = debugInfo;
}
ApiError.prototype = new HttpError();
ApiError.prototype.constructor = ApiError;
Errors.ApiError = ApiError;
ApiError.prototype.serialize = function () {
var errorObj = HttpError.prototype.serialize.call(this);
errorObj.args = [this.status, this.debugInfo];
return errorObj;
}
var err = new ApiError(404, 'fail');
Errors.isHttpError(err);
err instanceof Error;
err.message;
err.debugInfo;
Returns instance of object with error properties. Can be serialised to JSON
BN('i-errors').createError(obj)
Returns error.
var Errors = BEM.blocks['i-errors'];
BEM.blocks['i-response'].json({
error: Errors.serialize(new Errors.HttpError(404))
});
var Errors = BEM.blocks['i-errors'];
var data = JSON.parse(xhr.responseText);
var err = Errors.createError(data.error);
Errors.isHttpError(err);
BN('i-router')
Manages page blocks and transitions between urls.
BN('i-router').setPath | replacePath(path, [allowFallback=false])
- path {String}
- [allowFallback=false] {Boolean} if history.pushState is not supported, reload page to render on server
Changes url path.
On server: redirects with 302.
On client: destructs current page and inits new page. setPath
uses history.pushState
; replacePath
uses history.replaceState
.
BN('i-router').getPath()
Returns url path
BN('i-router').getUri()
Returns full uri (i.e. protocol, domain, path, query)
BN('i-router').setParams | replaceParams(params, [allowFallback=false], [extend=false])
- params {Object} key-value map of url params
- [allowFallback = false] {Boolean} if history.pushState is not supported, reload page to render on server
- [extend = false] {Boolean} extend current url params with new one
Changes url params.
On server: redirects with 302.
On client: setParams
uses history.pushState
; replaceParams
uses history.replaceState
.
BN('i-router').getHost()
Returns url host.
BN('i-router').escapeHTML | unescapeHTML (html)
DEPRECATED — use BN('i-content').escapeHTML | unescapeHTML instead
Escapes user content to prevent XSS.
BN('i-router').getParams()
GET or POST params.
BN('i-router').getReq()
Node http.IncomingMessage
BN('i-router').getRes()
Node http.ServerResponse
BN('i-router').getCookies()
Request cookies. See node-cookie api;
BN('i-router').getMatchers()
Getting regexp matchers from current router
Event 'update'
Use it to update static page content on client:
BN.addDecl('app-header').instanceProp({
init: function () {
BN('i-router').on('update', this._onPageUpdate);
},
_onPageUpdate: function () {
this.elem('search-input').val(
BN.escapeHTML(
BN('i-router').getParams().q
)
);
}
});
BN('i-api-request')
Base block for ajax blocks.
.get(resource, [options])
Calls this._request('get', resource, options)
;
._request(method, resource, [options])
- method {string} http method
- resource {string} REST api resource
- [options] {Object}
- [options.params] {Object} REST api request params
- [options.body] {Object} REST api request body
On server: makes request to rest api host, defined by this._apiHost
.
On client: makes remote call (through xhr) of server implementation.
Event 'beforerequest' 'afterrequest'
Triggered on client before and after xhr request.
BN('i-api-request').on('beforerequest', function () {
});
BN('i-api-request').on('afterrequest', function () {
});
Event 'error'
Triggered on client on xhr error.
Tests
####run tests
git clone git@github.com:bem-node/bem-node.git
cd bem-node
npm test
####quick tests
./tests.sh -c #client tests only
./tests.sh -s #server tests only
./tests.sh -s -n simple #run server tests only for 'simple' set
./tests.sh -s -n simple -g i-router #grep i-router tests
./tests.sh -b #rebuild tests
./tests.sh -b -s -c #rebuild, server, client
####create tests
You should use .common.test.js
for tests common for client and server, .priv.tests.js
for server tests only, and .tests.js
for client tests only.
Typical test looks like this
describe('whatever', function () {
it ('testing someting', function (done) {
return expect(env('/some-url?param-name=param-value', function () {
return <promise or not>
})).eventually.equal(<some value>)
});
});
describe('i-api-request', function () {
it('get with full path', function () {
return expect(env(function () {
return BEM.blocks['i-api-request'].get('http://nodejs.org/api/index.json')
})).eventually.have.property('source')
});
});
You should use global env
function to create page context