Isomorphic BEM

Published on February 04, 2015

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:

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.bemtreeas 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.bemtreeit 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 () {
    return this.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 .