javascript

Добавляем в jsx циклы и условия

  • среда, 7 июня 2017 г. в 03:14:53
https://habrahabr.ru/post/330172/
  • ReactJS
  • JavaScript


Если вы используете библиотеку React, то наверняка используете и jsx. Конечно, это не обязательно, и можно писать только js, используя React.createElement, но с jsx получится гораздо лаконичнее, что повышает читаемость. И всё замечательно до появления первой необходимости вывести данные в цикле. В jsx циклы не предусмотрены. Зато предусмотрена вставка js-кода. И тут вновь возникает вопрос читаемости, но теперь она значительно ухудшается. Возникает ситуация, когда в js-коде пишется html, в котором пишется js-код с html. Конечно, можно выделить html в отдельную функцию. Но тогда html будет появляться в коде то тут, то там. А хотелось бы локализовать всё в одном месте. К счастью, в современном javascript почти для любой проблемы, есть решение в виде библиотеки или плагина. Выше обозначенную проблему легко решает плагин для babel transform-react-statements.



Цикл For



Принцип действия плагина прост. Плагин преобразует jsx-компоненты, такие как <For/>, в js-код. Допустим есть вот такой компонент:

const MyComponent = props =>
    <ul>
        <For each="item" in={props.items}>
            <li key={item.id}>
                {item.text}
            </li>
        </For>
    </ul>

После обработки плагином, получим:

var _this = this;
 
const MyComponent = props =>
    <ul>
        {Array.prototype.map.call(props.items, function (item, index) {
            return <li key={item.id}>
                {item.text}
            </li>;
        }, _this)}
    </ul>;


Теперь подробнее о For. В первую очередь атрибут in. Это обязательный атрибут, в котором обозначается способ получения итерируемого объекта (например, переменная). Значение должно быть выражением, т.е. заключено в фигурные скобки.
Атрибут each
задает имя переменной для каждого элемента массива. Он не является обязательным. В случае его отсутствия, элементы массива будут передаваться в качестве spread-атрибута.

<div>
    <For in={items}>
        <Item />
    </For>
</div>

Преобразуется в:

<div>
    {Array.prototype.map.call(items, function (value, index) {
        return <Item {...value} />;
    }, this)}
</div>


Также, как видно из примеров выше, в цикле доступен номер элемента в переменной index. Переменную можно переименовать с помощью атрибута counter:

<For each="row" counter="rowIndex" in={rows}>
    <div key={`row-${rowIndex}` className="row">
        <For each="cell" counter="cellIndex" in="row">
            <div key={`cell-${cellIndex}`} className="ceil">
                { cell.content }
            </div>
        </For>
    </div>
</For>


Атрибут key



Для корректной работы React, каждый элемент в массивах должен иметь атрибут key. Его можно указать очевидно, как в примере выше. Другой способ - использовать атрибут key-is. Это может немного улучшить читаемость. Также можно указать keyIs в параметрах плагина. Тогда key не нужно будет писать в шаблоне - логика его получения уходит в бизнес-логику.

.babelrc
{
    plugins: [["transform-react-statements", { keyIs: "id" }]]
}


<div>
    <For each="item" in={array}>
        <div>{ item.value }</div>
    </For>
    <For each="item" in={array} key-is="someKey">
        <div>{ item.value }</div>
    </For>
    <For each="item" in={array}>
        <div key={item.getKey()}>{ item.value }</div>
    </For>
</div>

Будет преобразовано в:

<div>
    {Array.prototype.map.call(array, function (item) {
        // key берется из параметров плагина
        return <div key={item.id}>{item.value}</div>;
    }, this)}
    {Array.prototype.map.call(array, function (item) {
        // key - из атрибута <For />
        return <div key={item.someKey}>{item.value}</div>;
    }, this)}
    {Array.prototype.map.call(array, function (item) {
        // стандартный для React способ
        return <div key={item.getKey()} key={item.id}>{item.value}</div>;
    }, this)}
</div>;


Условие If



Это просто альтернатива для синтаксиса:

<div>
    { condition && <Component /> }
</div>


Основная задача сделать всё в едином, html-подобном стиле. Есть два атрибута: либо true, либо false, проверяющие условие на истинность или ложность соответственно. Для нескольких дочерних элементов, условие будет применяться к каждому из них:

<div>
    <If false={someCondition}>
        <div> Текст 1 </div>
        <div> Текст 2 </div>
    </If>
</div>


Преобразуется в:
<div>
    { !someCondition && <div> Текст 1 </div> }
    { !someCondition && <div> Текст 2 </div> }
</div>


Switch..Case..Default



Switch ведёт себя также, как и в javascript. У компонента Switch есть атрибут value, значение которого должно быть выражением в фигурных скобках, и дочерние компоненты Case, со своими атрибутами value. Если значение не соответствует ни одному из значений Case, выводится блок Default. Если блок Default отсутствует, возвращается null.
<div>
    <Switch value={x}>
        <Case value={“foo”}>
            <div> Text 1 </div>
        </Case>
        <Case value="bar">
            <div> Text 2 </div>
        </Case>
        <Case value={1}>
            <div> Text 3 </div>
        </Case>
        <Default>
            <div> Default text </div>
        </Default>
    </Switch>
</div>


Component



Довольно специфическое выражение. Превращает содержимое в стрелочную функцию:

<Component>
       <div> text </div>
</Component>

Преобразуется в

props => <div> text </div>;


Соответственно, внутри <Component/> доступна переменная props, которую можно переопределить через атрибут props:

<Component props="item">
    <div {...item} />
</Component>

На выходе получим:

item => <div {...item} />;


Автоматическое обертывание



Предположим, есть такой компонент:

class MyComponent extends React.Component {
    render() {
        return <For each="item" in={props.items}>
            <div key={item.id}>
                {item.text}
            </div>
        </For>
    }
}


For будет преобразован в выражение, возвращающее массив. Однако метод render должен вернуть React-элемент. Для того, чтобы использовать такой компонент, цикл нужно обернуть в элемент. Например так:

class MyComponent extends React.Component {
    render() {
        return <div>
            <For each="item" in={props.items}>
                <div key={item.id}>
                    {item.text}
                </div>
            </For>
        </div>
    }
}


Но делать это не обязательно. Так как плагин сам обернёт массив в react-элемент. По умолчанию, это <span />. Такое поведение можно изменить, указав параметр wrapper в настройках плагина:

{
    plugins: [["transform-react-statements", { wrapper: '<div class=”wrapper” />' }]]
}


Также можно отключить автоматическое обёртывание, используя значение параметра no-wrap:

{
    plugins: [["transform-react-statements", { wrapper: "no-wrap" }]]
}


Отключение и переименование выражений



Допустим, что в проекте уже есть компонент <If />, который вполне справляется со своей задачей. Тогда его можно отключить с помощью параметра disabled:

{
    plugins: [["transform-react-statements", { disabled: ["If"]}]]
}


Или можно переименовать выражение, чтобы в jsx использовать другое имя, например, IfStatement:

{
    plugins: [["transform-react-statements", { rename: { "If": "IfStatement" } }]]
}


Код пишется для людей, поэтому так важна его читаемость. И читаемость является главной проблемой jsx, а конкретно - перемешивание двух языков. Как видно, эта проблема решается, и решается она благодаря гибкости jsx в возможности вставлять javascript-код.

P.S. Автору было бы приятно получить звездочек на github, в качестве благодарности за работу.