EN RU
Forum

Methodology

Technology

Toolbox

Libraries

Tutorials

decl

A tool for working with declarations in BEM.

NPM Status

Introduction

A declaration is a list of BEM entities (blocks, elements and modifiers) and their technologies that are used on a page.

A build tool uses declaration data to narrow down a list of entities that end up in the final project.

This tool contains a number of methods to work with declarations:

This tool also contains the assign() method. You can use this method to populate empty BEM cell fields with the fields from the scope.

Note. If you don't have any BEM projects available to try out the @bem/sdk.decl package, the quickest way to create one is to use bem-express.

Installation

To install the @bem/sdk.decl package, run the following command:

npm install --save @bem/sdk.decl

Quick start

Attention. To use @bem/sdk.decl, you must install Node.js 8.0+.

Use the following steps after installing the package.

To run the @bem/sdk.decl package:

Loading declarations from files

Create two files with declarations and insert the following code into them:

set1.bemdecl.js:

exports.blocks = [
    {name: 'a'},
    {name: 'b'},
    {name: 'c'}
];

set2.bemdecl.js:

exports.blocks = [
    {name: 'b'},
    {name: 'e'}
];

In the same directory, create a JavaScript file with any name (for example, app.js), so your work directory will look like:

app/
├── app.js — your application file.
├── set1.bemdecl.js — the first declaration file.
└── set2.bemdecl.js — the second declaration file.

To get the declarations from the created files, use the load() method. Insert the following code into your app.js file:

const bemDecl = require('@bem/sdk.decl');

// Since we are using sets stored in files, we need to load them asynchronously.
async function testDecl() {
    // Wait for the file to load and set the `set1` variable.
    const set1 = await bemDecl.load('set1.bemdecl.js');

    // `set1` is an array of BemCell objects.
    // Convert them to strings using the `map()` method and special `id` property:
    console.log(set1.map(c => c.id));
    // => ['a', 'b', 'c']


    // Load the second set.
    const set2 = await bemDecl.load('set2.bemdecl.js');
    console.log(set2.map(c => c.id));
    // => ['b', 'e']
}

testDecl();

Subtracting declarations

To subtract one set from another, use the subtract() method. Insert this code into your async function in your app.js file:

console.log(bemDecl.subtract(set1, set2).map(c => c.id));
// => ['a', 'c']

The result will be different if we swap arguments:

console.log(bemDecl.subtract(set2, set1).map(c => c.id));
// => ['e']

Intersecting declarations

To calculate the intersection between two sets, use the intersect() method:

console.log(bemDecl.intersect(set1, set2).map(c => c.id));
// => ['b']

Merging declarations

To add elements from one set to another set, use the merge() method:

console.log(bemDecl.merge(set1, set2).map(c => c.id));
// => ['a', 'b', 'c', 'e']

Saving declarations to a file

To save the merged set, use the save() method. Normalize the set before saving:

const mergedSet = bemDecl.normalize(bemDecl.merge(set1, set2));
bemDecl.save('mergedSet.bemdecl.js', mergedSet, { format: 'v1', exportType: 'commonjs' })

The full code of the app.js file will look like this:

const bemDecl = require('@bem/sdk.decl');

// Since we are using sets stored in files, we need to load them asynchronously.
async function testDecl() {
    // Wait for the file to load and set the `set1` variable.
    const set1 = await bemDecl.load('set1.bemdecl.js');

    // `set1` is an array of BemCell objects.
    // Convert them to strings using the `map()` method and special `id` property:
    console.log(set1.map(c => c.id));
    // => ['a', 'b', 'c']


    // Load the second set.
    const set2 = await bemDecl.load('set2.bemdecl.js');
    console.log(set2.map(c => c.id));
    // => ['b', 'e']

    console.log(bemDecl.subtract(set1, set2).map(c => c.id));
    // => ['a', 'c']

    console.log(bemDecl.subtract(set2, set1).map(c => c.id));
    // => ['e']

    console.log(bemDecl.intersect(set1, set2).map(c => c.id));
    // => ['b']

    console.log(bemDecl.merge(set1, set2).map(c => c.id));
    // => ['a', 'b', 'c', 'e']

    const mergedSet = bemDecl.normalize(bemDecl.merge(set1, set2));
    bemDecl.save('mergedSet.bemdecl.js', mergedSet, { format: 'v1', exportType: 'commonjs' })
}

testDecl();

RunKit live example.

Run the app.js file. The mergedSet.bemdecl.js file will be created in the same directory with the following code:

module.exports = {
    format: 'v1',
    blocks: [
        {
            name: 'a'
        },
        {
            name: 'b'
        },
        {
            name: 'c'
        },
        {
            name: 'e'
        }
    ]
};

BEMDECL formats

There are several formats:

Note. bem-decl controls all of them.

API reference

load()

Loads a declaration from the specified file.

This method reads the file and calls the parse() function on its content.

/**
 * @param  {string} filePath — Path to file.
 * @param  {Object|string} opts — Additional options.
 * @return {Promise} — A promise that represents `BemCell[]`.
 */
format(filePath, opts)

You can pass additional options that are used in the readFile() method from the Node.js File System.

The declaration in the file can be described in any format.

parse()

Parses the declaration from a string or JS object to a set of BEM cells.

This method automatically detects the format of the declaration and calls a parse() function for the detected format. Then it normalizes the declaration and converts it to a set of BEM cells.

/**
 * @param {string|Object} bemdecl — String of bemdecl or object.
 * @returns {BemCell[]} — Set of BEM cells.
 */
parse(bemdecl)

RunKit live example.

normalize()

Normalizes the array of entities from a declaration for the specified format. If successful, this method returns the list of BEM cells which represents the declaration.

This method is an alternative to the parse() method. In this method, you pass a format and the declaration contents separately.

/**
 * @param {Array|Object} decl — Declaration.
 * @param {Object} [opts] — Additional options.
 * @param {string} [opts.format='v2'] — Format of the declaration (v1, v2, enb).
 * @param {BemCell} [opts.scope] — A BEM cell to use as the scope to populate the fields of normalized entites. Only for 'v2' format.
 * @returns {BemCell[]}
 */
normalize(decl, opts)

RunKit live example.

subtract()

Calculates the set of BEM cells that occur only in the first passed set and do not exist in the rest. Read more.

/**
 * @param {BemCell[]} set — Original set of BEM cells.
 * @param {...(BemCell[])} removingSet — Set (or sets) with cells that should be removed.
 * @returns {BemCell[]} — Resulting set of cells.
 */
subtract(set, removingSet, ...)

RunKit live example.

intersect()

Calculates the set of BEM cells that exists in each passed set. Read more.

/**
 * @param {BemCell[]} set — Original set of BEM cells.
 * @param {...(BemCell[])} otherSet — Set (or sets) that should be merged into the original one.
 * @returns {BemCell[]} — Resulting set of cells.
 */
intersect(set, otherSet, ...)

RunKit live example.

merge()

Merges multiple sets of BEM cells into one set. Read more

/**
 * @param {BemCell[]} set — Original set of cells.
 * @param {...(BemCell[])} otherSet — Set (or sets) that should be merged into the original one.
 * @returns {BemCell[]} — Resulting set of cells.
 */
merge(set, otherSet, ...)

RunKit live example.

save()

Formats and saves a file with BEM cells from a file in any format.

/**
 * @param   {string} filename — File path to save the declaration.
 * @param   {BemCell[]} cells  — Set of BEM cells to save.
 * @param   {Object} [opts] — Additional options.
 * @param   {string} [opts.format='v2'] — The desired format (v1, v2, enb).
 * @param   {string} [opts.exportType='cjs'] — The desired type for export.
 * @returns {Promise.<undefined>} — A promise resolved when the file is stored.
 */

You can pass additional options that are used in the methods:

Read more about additional options for the writeFile() method in the Node.js File System documentation.

Example:

const decl = [
    new BemCell({ entity: new BemEntityName({ block: 'a' }) })
];
bemDecl.save('set.bemdecl.js', decl, { format: 'enb' })
    .then(() => {
        console.log('saved');
    });

stringify()

Stringifies a set of BEM cells to a specific format.

/**
 * @param {BemCell|BemCell[]} decl — Source declaration.
 * @param {Object} opts — Additional options.
 * @param {string} opts.format — Format of the output declaration (v1, v2, enb).
 * @param {string} [opts.exportType=json5] — Defines how to wrap the result (commonjs, json5, json, es6|es2015).
 * @param {string|Number} [opts.space] — Number of space characters or string to use as white space (exactly as in JSON.stringify).
 * @returns {string} — String representation of the declaration.
 */
stringify(decl, options)

RunKit live example.

format()

Formats a normalized declaration to the target format.

/**
 * @param  {Array|Object} decl — Normalized declaration.
 * @param  {string} opts.format — Target format (v1, v2, enb).
 * @return {Array} — Array with converted declaration.
 */
format(decl, opts)

assign()

Populates empty BEM cell fields with the fields from the scope, except the layer field.

/**
 * @typedef BemEntityNameFields
 * @property {string} [block] — Block name.
 * @property {string} [elem] — Element name.
 * @property {string|Object} [mod] — Modifier name or object with name and value.
 * @property {string} [mod.name] — Modifier name.
 * @property {string} [mod.val=true] — Modifier value.
 */

/**
 * @param {Object} cell - BEM cell fields, except the `layer` field.
 * @param {BemEntityNameFields} [cell.entity] — Object with fields that specify the BEM entity name.
 *                               This object has the same structure as `BemEntityName`,
 *                               but all properties inside are optional.
 * @param {string} [cell.tech] — BEM cell technology.
 * @param {BemCell} scope — Context (usually the processing entity).
 * @returns {BemCell} — Filled BEM cell with `entity` and `tech` fields.
 */
assign(cell, scope)

RunKit live example.

See another example of assign() usage in the Select all checkboxes section.

Usage examples

Select all checkboxes

Let's say you have a list of checkboxes and you want to implement the "Select all" button, which will mark all checkboxes as checked.

Each checkbox is an element of the checkbox block, and checked is the value of the state modifier.

const bemDecl = require('@bem/sdk.decl');
const bemCell = require('@bem/sdk.cell');

// Sets the 'state' modifier for the entity.
function select(entity) {
    const selectedState = {
        entity: { mod: { name: 'state', val: 'checked'}}
    };
    return bemDecl.assign(selectedState, entity);
};

// Sets the 'state' modifier for the array of entities.
function selectAll(entities) {
    return entities.map(e => select(e));
};

// Let's define BEM cells that represent checkbox entities.
const checkboxes = [
    bemCell.create({ block: 'checkbox', elem: '1', mod: { name: 'state', val: 'unchecked'}}),
    bemCell.create({ block: 'checkbox', elem: '2', mod: { name: 'state', val: 'checked'}}),
    bemCell.create({ block: 'checkbox', elem: '3', mod: { name: 'state'}}),
    bemCell.create({ block: 'checkbox', elem: '4'}),
];

// Select all checkboxes.
selectAll(checkboxes).map(e => e.valueOf());
// => [
//      { entity: { block: 'checkbox', elem: '1', mod: { name: 'state', val: 'checked'}}}
//      { entity: { block: 'checkbox', elem: '2', mod: { name: 'state', val: 'checked'}}}
//      { entity: { block: 'checkbox', elem: '3', mod: { name: 'state', val: 'checked'}}}
//      { entity: { block: 'checkbox', elem: '4', mod: { name: 'state', val: 'checked'}}}
//  ]

RunKit live example.

License

© 2019 Yandex. Code released under Mozilla Public License 2.0.