Isomorphic BEM
- Tutorial
When node.js appeared, many web developers began to think about the possibility of using the same code both on the client and on the server. Now there are several frameworks that put the “write the code once, use it everywhere” approach at the forefront, new ones appear from time to time. So I could not pass by, I write a similar micro-framework - bnsf . It is intended for those who prefer to create the front-end of their applications according to the BEM methodology, using the appropriate set of technologies and tools.
Let's try to start writing front-end for a simple one-page web application using bnsf. In order not to be distracted by the creation of the back-end part, we will use the vk.com API as the back-end. Our application will consist of only two pages, the main one - with the user search form by identifier - and the secondary one, on it we will display information about the selected user.
To get started, you need node.js, yeoman, and gulp. I recommend using * nix OS, since the code was not tested under Windows, although, theoretically, it should work. I assume that node.js is already installed. If this is not the case, I advise you to use nvm .
Install gulp, yeoman and the corresponding generator:
We create our project:
You can see which files and folders were generated:
It will output something like this set of files (the order may differ on different operating systems):
The project can already be tried to assemble:
gulp will not only assemble the project, but also start the server, begin to monitor changes in the project and, if necessary, rebuild it.
Check that everything works. We try to open http: // localhost: 3000 in the browser - we should see a page with the text page-index and the title of main page.
We already have one page, let's create a second one to display posts from the user's wall. To do this, we again need a generator. Since it works from the command line, you will need another terminal session so as not to interrupt gulp. At this point, you can simply agree with everything yeoman will ask. It will warn of conflicts - this is standard practice when a file is not created new, but an existing one is edited, so just press enter in answer to all the questions yo. So, let's execute from the project root:
Let me remind you once again, we answer all questions with consent - that is, press input.
gulp should notice the appearance of a new page and rebuild the project. We check: the request for http: // localhost: 3000 / user should return a page with the text page-user.
Let's now place the search form on the main page by editing the file
And change the dependencies in
Now the form is already displayed (you can check by again going to http: // localhost: 3000 ), only the tag is not
Now it may seem redundant to create a separate directory with a file that stores just one line of code. But in a real project, it is almost impossible to meet this: a file with styles or JavaScript necessarily appears, or the block template itself is more complex. Often - all of the above immediately.
Well, we have a form, but it still does not know how to look for anything. Let “search” from the point of view of the form - it redirect to the current page with the query parameter. In order for the form to start doing this, you need the following JS in the file
You will also have to complicate the template a bit by adding information to it that the block has logic, a file
So, now we have a form that can change the get parameter of the page. You can verify this by entering, say, “1” in the text input and pressing enter. It's time to get some data on this parameter. I do not want to use an API that requires authentication, so I will use the method available to anyone by url
The first thing we need to do is add the route to the API route configuration file. This is a file
Second - Create a file
In this data template, we look to see if id came to us, if we did, we request data along the API route with the user identifier and user_ids parameter using the get method. If id is not a number, give the string 'Something goes wrong'. Since the list will need to be displayed, and we love the semantics, we will create
In addition, we need a file for declaring block dependencies
Now the page is already able to search for users and display results. Try it, just remember to refresh the page. If you enter “1”, you should find Pavel Durov in the results. But the trouble is that every time the whole page is redrawn. This is easy to fix by teaching her to update only the essentials. Supplement
Now, by opening the inspector in the browser, you can verify that with new requests to the API, only the search-results block is updated.
Well, the time has come to take up the second page, not in vain because we created it.
Let's start with
We added a fake menu block - just like a wrapper for a link to the main page, the link itself and a user-card block that will display information about the user.
Do not forget to update the dependencies in
I did not add the menu block depending on it, because I am not going to implement it.
To display the user card, create a file
In this form it will already work. You can try clicking on the link in the search results, just remember to refresh the page before that to pull up the new code. But let's make the user card a table by defining
Well, that’s much better.
I think it’s time to end here, although you could still add at least validation of user input, more accurate url, showing the loading process, returning to the last search ... I will leave this to the homework for those who are interested. Well, or the next article, if interested people ask for one.
Useful links:
bnsf - the framework discussed in the article. Actually just a library of blocks in BEM terminology.
bem-core - the library of blocks on which bnsf depends; bem
-components - the library of blocks that is used in the project created above
bem.info - a site about bem with documentation, in particular, you can read about:
bemtree- technology for constructing input data for the template engine based on data from the API and
bemhtml - declarative template engine
Article by Nickolas Zackas. There is a translation .
Let's try to start writing front-end for a simple one-page web application using bnsf. In order not to be distracted by the creation of the back-end part, we will use the vk.com API as the back-end. Our application will consist of only two pages, the main one - with the user search form by identifier - and the secondary one, on it we will display information about the selected user.
To get started, you need node.js, yeoman, and gulp. I recommend using * nix OS, since the code was not tested under Windows, although, theoretically, it should work. I assume that node.js is already installed. If this is not the case, I advise you to use nvm .
Install gulp, yeoman and the corresponding generator:
npm install -g gulp yo generator-bnsf
We create our project:
yo bnsf vk-test-app
cd vk-test-app
You can see which files and folders were generated:
ls
It will output something like this set of files (the order may differ on different operating systems):
README.md desktop.blocks gulpfile.js node_modules
bower.json desktop.bundles libs package.json
The project can already be tried to assemble:
gulp
gulp will not only assemble the project, but also start the server, begin to monitor changes in the project and, if necessary, rebuild it.
Check that everything works. We try to open http: // localhost: 3000 in the browser - we should see a page with the text page-index and the title of main page.
We already have one page, let's create a second one to display posts from the user's wall. To do this, we again need a generator. Since it works from the command line, you will need another terminal session so as not to interrupt gulp. At this point, you can simply agree with everything yeoman will ask. It will warn of conflicts - this is standard practice when a file is not created new, but an existing one is edited, so just press enter in answer to all the questions yo. So, let's execute from the project root:
yo bnsf:page user
Let me remind you once again, we answer all questions with consent - that is, press input.
gulp should notice the appearance of a new page and rebuild the project. We check: the request for http: // localhost: 3000 / user should return a page with the text page-user.
Let's now place the search form on the main page by editing the file
desktop.blocks/page-index/page-index.bemtree
as follows:block('page-index')(
content()(function () {
return [
{
block: 'search-form',
content: [
{
block: 'input',
mods: {
theme: 'simple'
}
},
{
block: 'button',
mods: {
type: 'submit',
theme: 'simple'
},
content: 'search'
}
]
},
{
block: 'search-results'
}
];
})
);
block('page-index').elem('title').content()('main page');
And change the dependencies in
page-index.deps.js:
({
mustDeps: ['i-page'],
shouldDeps: [
{ elem: 'title' },
'search-form',
{
block: 'input',
mods: { theme: 'simple' }
},
{
block: 'button',
mods: { theme: 'simple' }
},
'search-results'
]
})
Now the form is already displayed (you can check by again going to http: // localhost: 3000 ), only the tag is not
form
, but div
. To fix this, create the appropriate template file desktop.blocks/search-form/search-form.bemhtml
:block('search-form').tag()('form');
Now it may seem redundant to create a separate directory with a file that stores just one line of code. But in a real project, it is almost impossible to meet this: a file with styles or JavaScript necessarily appears, or the block template itself is more complex. Often - all of the above immediately.
Well, we have a form, but it still does not know how to look for anything. Let “search” from the point of view of the form - it redirect to the current page with the query parameter. In order for the form to start doing this, you need the following JS in the file
desktop.blocks/search-form/search-form.browser.js
:/**@module search-form*/
modules.define('search-form', ['i-bem__dom', 'app-navigation'], function (provide, BEMDOM, navigation) {
"use strict";
/**
* @class SearchForm
* @extends BEM.DOM
* @exports
*/
provide(BEMDOM.decl(this.name, /**@lends SearchForm#*/{
onSetMod: {
js: {
/**
* @constructs
* @this SearchForm
*/
inited: function () {
this._input = this.findBlockInside('input');
}
}
},
/**
* @param {Event} e
* @private
*/
_onSubmit: function (e) {
e.preventDefault();
var query = this._input.getVal(),
params = query ? {query: query} : null;
navigation.navigate('page-index', params);
}
}, /**@lends SearchForm*/{
/**
* @static
*/
live: function () {
var init = { modName: 'js', modVal: 'inited' };
this
.liveInitOnBlockInsideEvent(init, 'button')
.liveInitOnBlockInsideEvent(init, 'input')
.liveBindTo('submit', function (e) {
this._onSubmit(e)
});
}
}));
});
You will also have to complicate the template a bit by adding information to it that the block has logic, a file
desktop.blocks/search-form/search-form.bemhtml
:block('search-form')(
tag()('form'),
js()(true)
);
So, now we have a form that can change the get parameter of the page. You can verify this by entering, say, “1” in the text input and pressing enter. It's time to get some data on this parameter. I do not want to use an API that requires authentication, so I will use the method available to anyone by url
api.vk.com/method/users.get
. Let the form accept the user identifier, and a link will be displayed to its page (to the user page that we created above) and to the pages of 4 more users with identifiers obtained by a simple increment. As the text of links we will use user names. The first thing we need to do is add the route to the API route configuration file. This is a file
desktop.bundles/index/index.api.routing.yml
, and here is how its contents should turn out:- host: api.vk.com
routes:
- id: users
path: /method/users.get
Second - Create a file
desktop.blocks/search-results/search-results.bemtree
. The main idea is this: who needs to display the data, he goes after them. In our case, the data is needed by the search-results block, it needs to go after the data:block('search-results').content()(function () {
if (!this.route.parameters.query) {
return'';
}
var id = parseInt(this.route.parameters.query, 10);
return id ? this.get('users', { // отправляем запрос на маршрут сервера API с идентификатором user
user_ids: [id, id + 1, id + 2, id + 3, id + 4]
}, function (data) { // в этой функции обрабатываем результаты запросаreturn data.body.response.map(function (dataItem) {
return {
block: 'search-results',
elem: 'item',
content: {
block: 'link',
url: path('page-user', { id: dataItem.uid }), // генерируем url по идентификатору маршрута приложения page-user
content: dataItem.first_name + ' ' + dataItem.last_name
}
};
});
}) : 'Something goes wrong';
});
In this data template, we look to see if id came to us, if we did, we request data along the API route with the user identifier and user_ids parameter using the get method. If id is not a number, give the string 'Something goes wrong'. Since the list will need to be displayed, and we love the semantics, we will create
desktop.blocks/search-results/search-results.bemhtml
:block('search-results')
.tag()('ul')
.elem('item').tag()('li');
In addition, we need a file for declaring block dependencies
desktop.blocks/search-results/search-results.deps.js
:({
shouldDeps: ['link']
})
Now the page is already able to search for users and display results. Try it, just remember to refresh the page. If you enter “1”, you should find Pavel Durov in the results. But the trouble is that every time the whole page is redrawn. This is easy to fix by teaching her to update only the essentials. Supplement
page-index.bemtree
it to look like this:block('page-index')(
content()(function () {
return [
{
block: 'search-form',
content: [
{
block: 'input',
mods: {
theme: 'simple'
}
},
{
block: 'button',
mods: {
type: 'submit',
theme: 'simple'
},
content: 'search'
}
]
},
{
block: 'search-results'
}
];
}),
js()({
update: 'search-results'// мы добавили конфигурацию для клиентского JavaScript: имя блока, который следует обновлять
})
);
block('page-index').elem('title').content()('main page');
Now, by opening the inspector in the browser, you can verify that with new requests to the API, only the search-results block is updated.
Well, the time has come to take up the second page, not in vain because we created it.
Let's start with
desktop.blocks/page-user/page-user.bemtree
:block('page-user').content()(function () {
return [
{
block: 'menu',
content: {
block: 'link',
url: path('page-index'),
content: 'main page'
}
},
{
block: 'user-card'
}
];
});
block('page-user').elem('title').content()('user');
We added a fake menu block - just like a wrapper for a link to the main page, the link itself and a user-card block that will display information about the user.
Do not forget to update the dependencies in
desktop.blocks/page-user/page-user.deps.js
:({
mustDeps: ['i-page'],
shouldDeps: ['link', 'user-card']
})
I did not add the menu block depending on it, because I am not going to implement it.
To display the user card, create a file
desktop.blocks/user-card/user-card.bemtree
:block('user-card').content()(function () {
returnthis.get('users', {
user_ids: this.route.parameters.id
}, function (data) {
return data.body.response.map(function (dataItem) {
var output = [];
for (var key in dataItem) {
if (dataItem.hasOwnProperty(key)) {
output.push({
elem: 'row',
content: [
{
elem: 'key',
content: key
},
{
elem: 'value',
content: JSON.stringify(dataItem[key])
}
]
});
}
}
return output;
});
});
});
In this form it will already work. You can try clicking on the link in the search results, just remember to refresh the page before that to pull up the new code. But let's make the user card a table by defining
desktop.blocks/user-card/user-card.bemhtml
:block('user-card')(
tag()('table'),
elem('row').tag()('tr'),
elem('key').tag()('td'),
elem('value').tag()('td')
);
Well, that’s much better.
I think it’s time to end here, although you could still add at least validation of user input, more accurate url, showing the loading process, returning to the last search ... I will leave this to the homework for those who are interested. Well, or the next article, if interested people ask for one.
Useful links:
bnsf - the framework discussed in the article. Actually just a library of blocks in BEM terminology.
bem-core - the library of blocks on which bnsf depends; bem
-components - the library of blocks that is used in the project created above
bem.info - a site about bem with documentation, in particular, you can read about:
bemtree- technology for constructing input data for the template engine based on data from the API and
bemhtml - declarative template engine
Article by Nickolas Zackas. There is a translation .