πŸ“œ ⬆️ ⬇️

The site from scratch on a full stack of BEM technologies. Yandex Methodology

Last week, the BBC said that for the new version of the main page, it used the BEM methodology created in Yandex. On this occasion, we decided to raise the materials of the master class β€œ Developing a website from scratch on a full stack of BEM technologies ” and tell you how to start using a full stack of BEM technologies in our projects.

BEM simplifies the development of sites that need to be quickly created and maintained for a long time. This technology is used in the frontend of almost all Yandex services, and it has already managed to acquire many libraries and tools that we want to share with you.


')
In the article, we will tell you what the advantage of layout is by independent blocks and what are the levels of redefinition, we will get acquainted with ready-made block libraries and tools for automation of assembly. We show how different tools - for example, autoprefixer , Stylus css-preprocessor or YModules modular system - simplify the life of a developer and create a truly convenient platform if you integrate them into the BEM development process.

With a live example, we will explain what is the use of a declarative approach, when the same ideas can be used for both CSS and JavaScript. Let us focus separately on the declarative templates BEMHTML and BEMTREE , which allow you to convert data into a BEM tree, described in BEMJSON format , and then into HTML. Let us consider in detail how to write the server part of the application according to the BEM methodology.

We will use the Twitter API to create our project. As a result, we will get a working site on the full stack of BEM technologies and a step-by-step article guide on how all this can be reproduced.

Especially for the master class, we wrote a mini-service that deals with search on various social networks and displays the result in an orderly manner. We posted it on github in the github.com/bem/sssr repository - look , get acquainted.
And we go in order.

Theory


What is BEM ?
BEM (abbreviation for words - Lock, Element, and M identifier) ​​is a methodology for developing programs and interfaces, a way of describing entities that are not tied to specific implementation technologies.

BEM provides an abstraction over the DOM tree. The blocks are independent of each other and encapsulate all the functionality and elements. No matter what HTML tags a block div or form will be implemented in, you can always change this or add additional wrappers. Any changes should not affect the remaining blocks. We describe the application as interface components, not HTML tags.

Each block lies in its own folder in the file system, in which all technologies describing the block, its elements and modifiers are added.

 desktop.blocks/ input/ __box/ #  __clear/ #  __control/ #  _focused/ #  _type/ #  input.css # css   input.js # js   input.ru.md # markdown  … 

If you are interested in details on how and why BEM appeared, read Vitaly Kharisov’s article β€œ The History of BEM ” and watch the video of the report.
A detailed description of the BEM methodology can be found on our website .

Creating a project blank


Install everything you need to work.
First we need a terminal and a git version control system. You can install it from git-scm.com .
Almost all of our tools are written in JavaScript, so you need node.js or io.js.
To create a draft of our project, use the generator generator-bem-stub .
 > npm install -g generator-bem-stub 

Then run the generator itself:
 > yo bem-stub 

Answering questions regarding the technologies used, we will get the pre-assembled and configured for assembly.
Let's go through the questions:



The screenshot shows the results of the answers to the questions. The first three questions are obvious, after the interesting begins:

It's time to consider what .

Override Level


This is a set of block implementations. A project can have several levels, on each of which the implementation of blocks is added or changed. The final implementation of the block is collected from all levels sequentially in a given order.
We can add and redefine styles, templates, JavaScript-implementation of blocks at the level of redefining our project. At the same time, we do not change anything in the source files of the library, allowing us to save our changes if it is updated.

Let's give an example of how it looks in the file system:

 … libs/ bem-components/ desktop.blocks/ input/ input.css desktop.blocks/ input/ input.css … 

By creating a block at the desktop.blocks level of our project, we can add or override the technologies we need.
In the example above, we can edit the styles of the input block, adding its implementation to CSS technology.

So, our project preparation is ready. Go to the project directory:
 > cd sssr-tutorial 

Layout


To begin with, let's create a static prototype of our page. To describe its structure, we will use the BEMJSON technology.

BEMJSON describes a BEM tree: the order and nesting of blocks, the names and states of BEM entities, additional arbitrary fields.

Let's collect the generated project and see what happened. For convenient work with a locally installed ENB package, you need to run the following command:

 > export PATH=./node_modules/.bin:$PATH 

Or manually run the enb command from the ./node_modules/.bin/ subdirectory
To build, we will use the enb server command:

 > enb server 

Now the page can be opened at the address: http: // localhost: 8080 / desktop.bundles / index / index.html. Our builder will collect all the necessary dependencies, and by them will collect the files of the necessary blocks and technologies.



Open the inspector in the browser and look at the DOM tree. Although we have not yet written a single line of code, there is already generated HTML on this page. This is because templates from our libraries are used. For example, the page block template from the bem-core library generates a page doctype ( doctype , html , head , body , etc.).

Our project contains the file index.bemjson.js in the folder ./desktop.bundles/index/ :

 ({ block: 'page', title: 'Hello, World!', styles: [ { elem: 'css', url: 'index.min.css' } ], scripts: [ { elem: 'js', url: 'index.min.js' } ], content: [ 'Hello, World!' ] }) 

This file is a description of the page in BEM terms. The root block in our BEM tree is page . It has an API β€” additional keywords β€” title , favicon , etc. The templates of this block are in the bem-core library.

Our application consists of two main parts - caps and content. Add the sssr block to the page content, in which parts of the interface will be described as elements. To do this, edit ./desktop.bundles/index/index.bemjson.js :

 ({ block: 'page', //… content: [ { block: 'sssr', content: [ { elem: 'header' }, { elem: 'content' } ] } ] }); 

In the header, in turn, will be located the search form and the name of the site with the logo:

 { block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: 'Social Services Search Robot:' }, { block: 'form', content: [ { elem: 'search' }, { elem: 'filter', content: '[x] twitter' } ] } ] }, { elem: 'content' } ] } 



Use the input , button , spin and checkbox blocks from the bem-components library. In our project, this library is in the folder ./libs/bem-components . Each of these blocks has its own API, which can be viewed in the documentation .

Add the necessary blocks to BEMJSON:

 { block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: [ { block: 'icon', mods: { type: 'sssr' } }, 'Social Services Search Robot:' ] }, { block: 'form', content: [ { elem: 'search', content: [ { block: 'input', mods: { theme: 'islands', size: 'm', 'has-clear' : true }, name: 'query', val: '#b_', placeholder: 'try me, baby!' }, { block: 'button', mods: { theme: 'islands', size: 'm', type: 'submit' }, text: '' }, { block: 'spin', mods: { theme: 'islands', size : 's' } } ] }, { elem: 'filter', content: '[] twitter [] instagram' } ] } ] } ] } 

In this fragment of BEMJSON there is a field mods . It indicates the modifiers used and their values. The mods field contains the : - mods: { type: 'sssr' } .

In BEMJSON, you can use arbitrary JavaScript expressions. Add a map construction for duplicate checkbox blocks in the content element of the filter element:

 //… { elem: 'filter', content: ['twitter', 'instagram'].map(function(service) { return { block: 'checkbox', mods: { theme: 'islands', size: 'l', checked: service === 'twitter' }, name: service, text: service }; }) } //… 

Full index.bemjson.js file:

 ({ block: 'page', title: 'Social Services Search Robot', favicon: '/favicon.ico', head: [ { elem: 'meta', attrs: { name: 'description', content: 'find them all' }}, { elem: 'css', url: '_index.css' } ], scripts: [{ elem: 'js', url: '_index.js' }], content: { block: 'sssr', content: [ { elem: 'header', content: [ { elem: 'logo', content: [ { block: 'icon', mods: { type: 'sssr' } }, 'Social Services Search Robot:' ] }, { block: 'form', content: [ { elem: 'search', content: [ { block: 'input', mods: { theme: 'islands', size: 'm', 'has-clear' : true }, name: 'query', val: '#b_', placeholder: 'try me, baby!' }, { block: 'button', mods: { theme: 'islands', size: 'm', type: 'submit' }, text: '' }, { block: 'spin', mods: { theme: 'islands', size : 's' } } ] }, { elem: 'filter', content: ['twitter', 'instagram'].map(function(service) { return { block: 'checkbox', mods: { theme: 'islands', size: 'l', checked: service === 'twitter' }, name: service, text: service }; }) } ] } ] }, { elem: 'content' } ] } }) 

After we describe the interface structure, we need to write and add styles for our blocks. All main styles come to us with the library bem-components . So we need to add quite a bit.

For writing styles, we use Stylus CSS preprocessor. All files with the *.styl will be processed by the preprocessor and glued to the final CSS file. You can also use the *.css extension for styles that do not need to be processed by the preprocessor.
We write styles for the form block in the file ./desktop.blocks/form/form.styl :

 .form { display: flex; &__search { margin-right: auto; } .input { width: 400px; } .checkbox { display: inline-block; margin-left: 15px; user-select: none; vertical-align: top; } } 

For the page block in the ./desktop.blocks/page/page.css file:

 .page { font-family: Tahoma, sans-serif; min-height: 100%; margin: 0; padding-top: 100px; background: #000; } 

For the sssr block in the sssr file:

 .sssr { &__header { position: fixed; z-index: 1; top: 0; box-sizing: border-box; width: 100%; padding: 10px 10%; background: #f6f6f6; box-shadow: 0 0 0 1px rgba(0,0,0,.1), 0 10px 20px -5px rgba(0,0,0,.4); .button { margin-left: 10px; } } &__logo { font-size: 18px; margin: 0 0 10px; } &__content { padding: 10px 10%; column-count: 4; column-gap: 15px; transition: opacity .20s linear; } a[rel='nofollow'], a[xhref], [name][server] { text-decoration: none; color: #038543; } } 

And for the user block, desktop.blocks/user/user.styl :

 .user { &__name { display: inline-block; margin-right: 10px; text-decoration: none; color: #000; &:hover { text-decoration: underline; color: #038543; } } &__post-time { font-size: 14px; display: inline-block; color: #8899a6; } &__icon { position: absolute; right: 5px; bottom: 5px; width: 30px; height: 30px; border-radius: 3px; } } 

We will not dwell on the issues of CSS-layout in great detail, let's go further.

It remains for us to add blocks with found messages. We describe them in index.bemjson.js and use the JavaScript features for prototyping.

 { elem: 'content', content: (function() { return 'BEM is extermly cool'.split('').map(function() { var service = ['twitter', 'instagram'][Math.floor(Math.random()*2)]; return { service: service, user: [{ login: 'tadatuta', name: 'Vladimir', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_batman.png' }, { login: 'dmtry', name: 'Dmitry', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_captain-america.png' }, { login: 'sipayrt', name: 'Jack Konstantinov', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_ironman.png' }, { login: 'einstein', name: 'Slava', avatar: 'https://raw.githubusercontent.com/bem/bem-identity/master/sign/_theme/sign_theme_robin.png' }][Math.floor(Math.random()*4)], time: Math.floor((Math.random()*12)+1) + 'h', img: service === 'instagram' ? 'http://bla.jpg' : undefined, text: [ ' β€”    .       (  ).', ' β€”    .', '        .'][Math.floor(Math.random()*3)] }; }).map(function(dataItem) { return { block: 'island', content: [ { elem: 'header', content: { block: 'user', content: [ { block: 'link', mix: { block: 'user', elem: 'name' }, url: 'https://www.yandex.ru', target: '_blank', content: dataItem.user.name }, { elem: 'post-time', content: dataItem.time }, { block: 'image', mix: { block: 'user', elem: 'icon' }, url: dataItem.user.avatar, alt: dataItem.user.name } ] } }, { elem: 'text', content: dataItem.text }, { elem: 'footer', content: [ { block: 'service', mods: { type: dataItem.service } } ] } ] }; }); })() } 

and add styles for the island block to the ./desktop.blocks/island/island.styl file:

 .island { font-size: 18px; line-height: 140%; position: relative; display: inline-block; box-sizing: border-box; width: 100%; margin-bottom: 15px; padding: 15px 5px 5px 15px; border-radius: 3px; background: #fff; box-shadow: inset 0 0 1px rgba(0, 0, 0, .4); &__footer { margin-top: 10px; } &__image { display: block; width: 100%; border-radius: 3px; } } 

Let's look at the result:



BEMHTML Template Engine


Declarative template making


Yandex is very fond of declarativeness - not only in CSS, but in templates and in JavaScript.
What CSS declarative looks like:

 .menu__item { display: inline-block; } 

item style will be applied to all items in the menu block display: inline-block; i.e. we declare how to be processed
Our DOM nodes are selected by condition:

  {  } 

We select all the nodes of the DOM tree that match the condition, and apply the template body to them.

For declarative templating in Yandex, they wrote their own template engine BEMHTML. More information about its architecture can be found in the article Templating data in bem-core .
An example of a declarative template in BEMHTML:

 block('menu').elem('item').tag()('span'); 

All blocks of the BEM tree corresponding to our conditions are selected, then the template body is applied to them:

 ()( ) 

BEMHTML is written in JavaScript. Its syntax is pure javascript. You can use JavaScript functions in subpredicates and the body of the template. For production mode, templates will be compiled into optimized JavaScript.
BEMHTML is responsible for how the BEM tree is converted to an HTML string. The input data is a BEM tree or its fragment, described in the BEMJSON technology. A BEMHTML template is superimposed on this BEMJSON. And the output is an HTML string.

In general, the pattern is as follows:

 match(1, 2, 3)(); 

Subpredicates are conditions under which a pattern is applied. For example:

 match(1, 2, 3)(); 

This template checks if the current block is a link block, if there is a url variable in the context of this.ctx , and if the current mode is a fashion tag . If all these conditions are met, the a tag will be applied to the block.

Fashion


Fashion is the step of generating output HTML. Each mod is responsible for its piece of the resulting HTML-code. The default mode describes the set and order of passing the other modes. This diagram shows what each mod is responsible for:

Mod scheme when generating HTML

We recommend that you carefully read the BEMHTML documentation described in the BEMHTML Template Reference Guide .

Let's return to our project. We need a form block. It should appear as a <form> and have a JavaScript implementation.
If we add another such block to the page, we will have to edit these parameters directly in the BEMJSON file. This is similar to using inline styles in HTML. Let's do everything right and put the parameters of the block in its template:
./desktop.blocks/form/form.bemhtml :

 block('form')( tag()('form'), js()(true) ); 

Now we can edit the block template in one place, transfer and reuse this block with ease.

Let's take a look at the DOM tree in the inspector - our form block is now displayed as a <form> with the i-bem class. This class says that the block has a javascript implementation.



We described how our BEM blocks should be converted to HTML. Now let's look at how twitter data will be received and processed.

Application architecture


Two-stage templating


Our application will work as follows:

BEMTREE


We talked about how to convert a BEM tree to HTML. This is the task of the frontend server. The BEMTREE template engine deals with the task of building a BEM tree and saturating it with data. It has the same syntax as BEMHTML. The main difference is the number of standard modes available. BEMTREE has only default and content .
The input data for BEMTREE is the raw data with which the block templates are saturated. At the output we get a ready-made fragment of the BEM-tree, which we will transmit further to the BEMHTML template.

Immediately into battle. Write the BEMTREE template for the { type: 'twitter' } modifier, island block:
desktop.blocks/island/_type/island_type_twitter.bemtree

 block('island').mod('type', 'twitter').content()(function() { var data = { postLink: '#', userName: 'user@name', userNick: 'user@nick', createdAt: '19 of July', avatar: '#avatar', text: 'message going here', type: 'twitter' }; return [ { elem: 'header', content: { block: 'user', content: [ { block: 'link', mods: { theme: 'islands' }, mix: { block: 'user', elem: 'name' }, url: data.postLink, content: [data.userName, ' @', data.userNick] }, { elem: 'post-time', content: data.createdAt.toString() }, { block: 'image', mix: { block: 'user', elem: 'icon' }, url: data.avatar, alt: data.userName } ] } }, { elem: 'text', content: data.text }, { elem: 'footer', content: [ { block: 'service', mods: { type: data.type } } ] } ]; }); 

We transfer the image block with the required parameters to the contents of this block and mix the image element of the island block.
In the following, we will replace the static object with the data transmitted for templating. But first, let's see how the server code will be organized and how this data will be transmitted.

On server


Our application will work on the express framework - to give HTML in response to a search query.

Let's write the blocks responsible for collecting data from services. We will write the server code into files with the extension *.node.js , which will be merged into one file during assembly. We will launch it with the help of node.js

Block service_type_twitter


For ease of working with twitter, we use the twit module. Install it using npm :

 > npm i twit --save 

We submitted the authorization data necessary for working with twitter in a separate file . Copy its contents into the file of the same name.

./desktop.blocks/service/_type/service_type_twitter.node.js :

 var twitter = require('twit'), config = require('./service_type_twitter.config'), twit = new twitter(config); var query = '#b_', results = []; twit.get('search/tweets', { q: query, count: 20 }, function(err, res) { if (err) { console.error(err); return []; } results = res.statuses.map(function(status) { var user = status.user; return { avatar: user.profile_image_url, userName: user.name, userNick: user.screen_name, postLink: 'https://twitter.com/' + user.screen_name, createdAt: status.created_at, text: status.text, type: 'twitter' }; }); console.log(results); }); 

This application searches for the keyword #b_ and outputs the result to the console.
Rebuild our project and run it with node.js

 > enb make > node ./desktop.bundles/index/index.node.js 

The result of the execution should be a list of tweets in the console.

Now we need to somehow transfer the results of execution for further work - template making and transfer to the client.
For asynchronous work using promises, we use the vow library.
For organization of server and client JS-code - the modular system YModules .

Modular system


The bem-core library uses the modular system ymodules .
It allows you to wrap the code of our block in a wrapper module and call it, if necessary, from other modules.

Edit the service_type_twitter.node.js file in accordance with these additions:

 modules.define('twitter', function(provide) { var vow = require('vow'), moment = require('moment'), twitter = require('twit'), twitterText = require('twitter-text'), config = require('./service_type_twitter.config'), twit = new twitter(config); provide({ get: function(query) { var dfd = vow.defer(); twit.get('search/tweets', { q: query, count: 20 }, function(err, res) { if(err || !res.statuses) { console.error(err); dfd.resolve([]); } dfd.resolve(res.statuses.map(function(status) { return { avatar: status.user.profile_image_url, userName: status.user.name, userNick: status.user.screen_name, postLink: 'https://twitter.com/' + status.user.screen_name, createdAt: moment(status.created_at), text: twitterText.autoLink(twitterText.htmlEscape(status.text)), type: 'twitter' }; })); }); return dfd.promise(); } }); }); 

As you can see, we wrapped all the code in the modules.define construction. This is the declaration of the twitter module, which will later be available in our application through the modules namespace.
For asynchronous transfer of the result, we return a promise to which, depending on the results of the query, we transfer either an empty array if there was an error, or an array with the search results.
To work with dates, add the module moment.js .
Twitter returns plain text to us in messages, so we use the twitter-text library to highlight hash tags and links.
In addition, as mentioned above, we need express .
Let's install these modules:

 > npm i vow moment twitter-text express --save 

server block


The server block will be responsible for the server part of our application. Add the folder ./desktop.blocks/server/ and create the file server.node.js in it.

This will be an express application that listens to the URL /search and sends the data according to the request.

 modules.require(['twitter'], function(twitter) { var fs = require('fs'), PATH = require('path'), express = require('express'), app = express(), url = require('url'), querystring = require('querystring'), Vow = require('vow'); app.get('/search', function(req, res) { var dataEntries = [], searchObj = url.parse(req.url, true).query, queryString = querystring.escape(searchObj.query), servicesEnabled = []; searchObj.twitter && servicesEnabled.push(twitter.get(queryString)); Vow.all(servicesEnabled) .then(function(results) { res.end(JSON.stringify(results, null, 4)); }) .fail(function() { console.error(arguments); }); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); }); }); 

Create a ./desktop.blocks/sssr/sssr.deps.js file with the following content:

 ({ shouldDeps: [ { block: 'server' }, { block: 'island', mods: { type: ['twitter'] }} ] }) 

It says here that the sssr block needs server and island blocks with the type: 'twitter' modifier type: 'twitter' .

Also add the service_type_twitter modifier depending on the server block. To do this, create a file ./desktop.blocks/server/server.deps.js :

 ({ shouldDeps: [ { block: 'service', mods: { type: ['twitter'] } }, { block: 'sssr', } ] }) 

Now all the blocks we need will fall into the assembly. Rebuild the project and start the server:

 > enb make && node ./desktop.bundles/index/index.node.js 


At http: // localhost: 3000 / search? Query =% 23b_ & twitter = on , a page will open with a JSON data object, which is delivered by the service_type_twitter block.



Now add the conversion of this data to BEMJSON using BEMTREE. Edit server.node.js :

 modules.require(['twitter'], function(twitter) { var fs = require('fs'), PATH = require('path'), VM = require('vm'), express = require('express'), app = express(), url = require('url'), querystring = require('querystring'), moment = require('moment'), Vow = require('vow'), pathToBundle = PATH.join('.', 'desktop.bundles', 'index'); app.use(express.static(pathToBundle)); var bemtreeTemplate = fs.readFileSync(PATH.join(pathToBundle, 'index.bemtree.js'), 'utf-8'); var context = VM.createContext({ console: console, Vow: Vow }); VM.runInContext(bemtreeTemplate, context); var BEMTREE = context.BEMTREE; app.get('/search', function(req, res) { var dataEntries = [], searchObj = url.parse(req.url, true).query, queryString = querystring.escape(searchObj.query), servicesEnabled = []; searchObj.twitter && servicesEnabled.push(twitter.get(queryString)); Vow.all(servicesEnabled) .then(function(results) { //      , //     Object.keys(results).map(function(idx) { dataEntries = dataEntries.concat(results[idx]); }); //     dataEntries.sort(function(a, b) { return b.createdAt.valueOf() - a.createdAt.valueOf(); }); //  BEMJSON     BEMTREE  BEMTREE.apply(dataEntries.map(function(dataEntry) { dataEntry.createdAt = moment(dataEntry.createdAt).fromNow(); return { block: 'island', data: dataEntry, mods: { type: dataEntry.type } }; })) .then(function(bemjson) { //   JSON res.end(JSON.stringify(bemjson, null, 4)); }); }) .fail(function() { console.error(arguments); }); }); var server = app.listen(3000, function() { console.log('Listening on port %d', server.address().port); }); }); 

BEMTREE- , vow , .

, , .

BEMTREE.apply() , , - , BEMTREE-.

./desktop.blocks/island/_type/island_type_twitter.bemtree :

 block('island').mod('type', 'twitter').content()(function() { var data = this.ctx.data; return [ //     ]; }); 

this.ctx.data , BEMTREE.apply() .

http://localhost:3000/search?query=%23b_&twitter=on . BEMJSON, BEMTREE.

BEMJSON HTML BEMHTML.apply() . server.node.js :

 var BEMHTML = require(PATH.join('../../' + pathToBundle, 'index.bemhtml.js')).BEMHTML; //… BEMTREE.apply(dataEntries.map(function(dataEntry) { dataEntry.createdAt = moment(dataEntry.createdAt).fromNow(); return { block: 'island', data: dataEntry, mods: { type: dataEntry.type } }; })) .then(function(bemjson) { if (searchObj.json) { return res.end(JSON.stringify(bemjson, null, 4)); } res.end(BEMHTML.apply(bemjson)); }); //… 

, HTML, β€” AJAX.

json=on β€” BEMJSON- β€” http://localhost:3000/search?query=%23b_&twitter=on&json=on .



JavaScript i-bem.js


JavaScript JavaScript- - - – i-bem.js . bem-core . i-bem.js β€” i-bem js . jQuery API .

, i-bem.js .

:

js-


js-, . , , js-, BEMHTML js , BEMJSON β€” js :

 // bemhtml block('form').js()(true); 

 // bemjson { block: 'form', js: true } 

 // bemjson with js params { block: 'form', js: { p1: 'v1', p2: 'v2' } } 

js , , js- . HTML:

 <div class="form i-bem" data-bem="{form: {p1: 'v1', p2 : 'v2'}}"></div> 

i-bem , DOM- js-. - data-bem
, js-, β€” , .

js


form


./desktop.blocks/form/form.js :

 modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); } } }, _onSubmit: function(e) { e.preventDefault(); this.emit('submit'); }, getVal: function() { return this.domElem.serialize(); } })); }); 

bem-core . i-bem β€” . i-bem__dom β€” , DOM . form , i-bem__dom , DOM-. BEMDOM . form . , js inited β€” i-bem.js . , _onSubmit , , getVal , .

_onSubmit() e.preventDefault() , - submit , . API form . -.

sssr


, .
./desktop.blocks/sssr/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._sendRequest, this); } } }, _sendRequest: function() { $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(html) { BEMDOM.update(this.elem('content'), html); } })); }); 

. sssr i-bem__dom , DOM-, jquery AJAX.
submit form . _sendRequest , AJAX-. , _onSuccess , sssr__content .

, i-bem.js , sssr js-:

 // desktop.blocks/sssr/sssr.bemhtml block('sssr').js()(true); 

, , . index.node.js :

 $ enb make && node ./desktop.bundles/index/index.node.js 

. localhost:3000 , - , . , .



, . borschik . .borschik :

 { "freeze_paths": { "libs/**": ":base64:", "libs/**": ":encodeURIComponent:" } } 

production :

 > YENV=production enb make && node desktop.bundles/index/index.node.js 

.



. spin


- , . , «». spin , . BEMJSON-. bem-components API. :

 modules.require(['jquery'], function($) { $('.spin').bem('spin').setMod('visible'); }); 




spin_visible true .

, js - .

./desktop.blocks/sssr/sssr.styl :

 .sssr { .spin { margin-left: 1em; vertical-align: middle; } } 

, . ./desktop.blocks/sssr/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { console.log('visible: ', modVal); this.findBlockInside('spin').setMod('visible', modVal); } }, // … _doRequest: function() { this.setMod('loading'); this._sendRequest(); }, _onSuccess: function(html) { this.delMod('loading'); BEMDOM.update(this.elem('content'), html); } })) }) 

JS-, CSS- . , , . ./desktop.bundles/sssr/sssr.styl :

 .sssr { .spin { margin-left: 1em; vertical-align: middle; } &_loading .content { opacity: 0.5; } } 

: localhost:3000 .
spin , β€” .




, , . isEmpty() :
./desktop.blocks/form/form.js :

 isEmpty: function() { return !this.findBlockInside('input').getVal().trim() || this.findBlocksInside('checkbox').every(function(checkbox) { return !checkbox.hasMod('checked'); }); } 

input checkbox_checked .
, , sssr :
./desktop.blocks/sssr/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function() { if (this.findBlockInside('form').isEmpty()) { return; } this.setMod('loading'); this._sendRequest(); }, _sendRequest: function() { //… }) 

_doRequest() .

, , . _sendRequest() clear() _updateContent() .

./desktop.blocks/sssr/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit', this._doRequest, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function() { if (this.findBlockInside('form').isEmpty()) { return; } this.setMod('loading'); this._sendRequest(); }, clear: function() { this._xhr && this._xhr.abort(); this._updateContent(''); this.delMod('loading'); }, _sendRequest: function() { this._xhr && this._xhr.abort(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } })); }) 


, ,
. form change input :

 modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); this.findBlockInside('input').on('change', this._onChange, this); } } }, _onChange: function() { this.emit('change'); }, // … }) 

change sssr , ./desktop.blocks/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery'], function(provide, BEMDOM, $) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit change', this._doRequest, this); } }, // … })); }) 

, ./desktop.blocks/form.js :

 modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.bindTo('submit', this._onSubmit); this.findBlockInside('input').on('change', this._onChange, this); BEMDOM.blocks.checkbox.on(this.domElem, 'change', this._onChange, this); } } }, // … }) 

:



. . debounce bem-core . sssr sssr.deps.js :

 ({ shouldDeps: [ { block: 'server' }, { block: 'island', mods: { type: ['twitter'] }}, { block: 'functions', elem: 'debounce' } ] }) 

- . , functions__debounce debounce :

 modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this.findBlockInside('form').on('submit change', this._doRequest, this); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this.findBlockInside('spin').setMod('visible', modVal); } }, _doRequest: function(e) { this.setMod('loading'); if (this.findBlockInside('form').isEmpty()) { this._clear(); return; } e.type === 'change' ? this._debounceRequest(): this._sendRequest(); }, _clear: function() { this._xhr && this._xhr.abort(); this._updateContent(''); this.delMod('loading'); }, _sendRequest: function() { this._xhr && this._xhr.abort(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: '/search/', data: this.findBlockInside('form').getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } })); }); 

, . β€” .

Auto update


. sssr params .

index.bemjson.js :

 { block: 'sssr', mods: { autorefresh: true }, js: { url: '/search/', refreshInterval: 10000 }, // ... } 

: ./desktop.blocks/sssr/_autorefresh/sssr_autorefresh.js :

 modules.define('sssr', ['tick'], function(provide, tick, Sssr) { provide(Sssr.decl({ modName: 'autorefresh' }, { onSetMod: { loading: function(modName, modVal) { //    this.__base.apply(this, arguments); //    β€” , //    β€”   modVal ? this._clearTimer(): this._setTimer(); } }, _setTimer: function() { this._counter = 0; tick.on('tick', this._onTick, this); }, _onTick: function() { //       (++this._counter * 50) % this.params.refreshInterval || this._sendRequest(); }, _clearTimer: function() { tick.un('tick', this._onTick, this); }, getDefaultParams: function() { return { refreshInterval: 10000 }; } })); }); 

this.__base sssr_loading . tick . tick 50 . sssr_loading , , .

refreshInterval sssr , . getDefaultParams . , .

sssr . desktop.blocks/sssr/sssr.deps.js :

 ({ shouldDeps: [ 'server', { block: 'functions', elem: 'debounce' }, { block: 'island', mods: { type: ['twitter'] } } ] }) 

. 10 .



, ,
.
this.findBlockInside('form') this._form . spin .
sssr , .

 modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._spin = this.findBlockInside('spin'); this._form = this.findBlockInside('form') .on('submit', this._doRequest, this); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this._spin.setMod('visible', modVal); } }, _doRequest: function(e) { this.setMod('loading'); if (this._form.isEmpty()) { this._clear(); return; } e.type === 'change' ? this._debounceRequest(): this._sendRequest(); }, _clear: function() { this._abortRequest(); this._updateContent(''); this.delMod('loading'); }, _abortRequest: function() { this._xhr && this._xhr.abort(); }, _sendRequest: function() { this._abortRequest(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', cache: false, url: this.params.url, data: this._form.getVal(), success: this._onSuccess.bind(this) }); }, _onSuccess: function(result) { this.delMod('loading'); this._updateContent(result); }, _updateContent: function(result) { BEMDOM.update(this.elem('content'), result); } })); }); 

, , , _abortRequest() .

Delayed initialization


, , .
. live -. i-bem.js .

sssr form , . :
./desktop.blocks/sssr/sssr.js :

 modules.define('sssr', ['i-bem__dom', 'jquery', 'functions__debounce'], function(provide, BEMDOM, $, debounce) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._form = this.findBlockInside('form'); this._spin = this.findBlockInside('spin'); this._debounceRequest = debounce(this._sendRequest, 500, this); } }, loading: function(modName, modVal) { this._spin.setMod('visible', modVal); } }, _clear: function() { this._abortRequest(); this._updateContent(''); this.delMod('loading'); }, _doRequest: function(needDebounce) { if (this._form.isEmpty()) { this._clear(); return; } this.setMod('loading'); needDebounce? this._debounceRequest() : this._sendRequest(); }, _sendRequest: function() { this._abortRequest(); this._xhr = $.ajax({ type: 'GET', dataType: 'html', url: this.params.url, data: this._form.getVal(), cache: false, success: this._onSuccess.bind(this) }); }, _abortRequest: function() { this._xhr && this._xhr.abort(); }, _onSuccess: function(result) { this._updateContent(result); this.delMod('loading'); }, _updateContent: function(html) { BEMDOM.update(this.elem('content'), html); } }, { live: function() { this.liveInitOnBlockInsideEvent('submit change', 'form', function(e) { this._doRequest(e.type === 'change'); }); } })); }); 

live - form :
./desktop.blocks/form/form.js :

 modules.define('form', ['i-bem__dom'], function(provide, BEMDOM) { provide(BEMDOM.decl(this.name, { onSetMod: { js: { inited: function() { this._input = this.findBlockInside('input'); this._checkboxes = this.findBlocksInside('checkbox'); } } }, // … isEmpty: function() { return !this._input.getVal().trim() || this._checkboxes.every(function(checkbox) { return !checkbox.hasMod('checked'); }); } }, { live: function() { var ptp = this.prototype; this .liveBindTo('submit', ptp._onSubmit) .liveInitOnBlockInsideEvent('change', 'input', ptp._onChange) .liveInitOnBlockInsideEvent('change', 'checkbox', ptp._onChange); } })); }); 


input checkbox , , findBlockInside .


, -. i-bem.js , BEMTREE - BEMHTML - HTML. sssr service__type__* API Instagram .. , . , .

, . info@bem.info .

Source: https://habr.com/ru/post/251473/


All Articles