Свойства и опции

Работа с опциями контрола

Опции — это объект, в котором хранятся параметры, переданные при инициализации контрола. В шаблоне опции доступны в переменной _options, а в модуле — в переменной this._options.

Опции доступны только на чтение, и менять их можно только через родительский контрол. Такое ограничение введено для того, чтобы избежать излишнего усложнения кода. Изменение объекта опций подобно изменению аргументов, переданных в функцию. Опции должны меняться только в том случае, если родитель изменил параметры контрола.

В следующем примере мы инициализируем контрол Demo.Module и передаем в него опцию color.

<!-- Parent/Template.wml -->
<Demo.Module color="{{ someColor }}" />
<!-- Demo/Template.wml -->
<div style="color: {{ _options.color }};">
   Hello Wasaby!
</div>
// Demo/Module.ts
import { Control, TemplateFunction } from 'UI/Base';
import * as template from 'wml!Demo/Template';

export default class extends Control {
     protected _template: TemplateFunction = template;
}

См. демо-пример на CodeSandbox

С опциями можно работать в хуках жизненного цикла.

Начальные значения опций

Для указания начального значения опций определите в контроле статическое поле defaultProps. Данное поле устанавливает значение опции по-умолчанию, если эта опция не задана.

// Module.ts
export default class ModuleClass extends Control<IOptions> {
    static defaultProps: Partial<IOptions> = {
        number: 10, 
        text: 'Default text'
    };
}
<!-- Parent.wml -->
<Demo.Module/>
<!-- Module.wml -->
<div>
   {{_options.number}}
</div>

Результат

<div>
   10
</div>

Если опции не указаны явно, их значения могут передаваться от родительских контролов дочерним контролам. Например, со стороны платформы спускаются опции theme и readOnly.

Пример:

Статический метод getInheritOptions() возвращает набор опций по умолчанию, которые будут спускаться дочерним контролам, и будут использоваться в случае, если они не задаются явно.

static getInheritOptions(): { fed: object, gridOptions: object } {
        return {
            fed: {},
            gridOptions: null
        };
    }

Контроль типа данных опций

С помощью статического метода getOptionTypes можно указать типы опций контрола. Когда контрол получает опцию другого типа, в консоль будет выведена соответствующая ошибка (контрол не будет построен).

// Module.ts
import { Control } from 'UI/Base';
import entity = require('Types/entity');

export default class extends Control {
    static getOptionTypes(): object {
        return {
            number: entity.descriptor(Number), 
            text: entity.descriptor(String)
        }
    }
}

К типам опций можно применить модификаторы. С помощью required() обозначают опции, обязательные для построения контрола.

// Module.ts
import { Control } from 'UI/Base';
import {descriptor as EntityDescriptor} from 'Types/entity';

export default class extends Control {
    static getOptionTypes(): object {
        return {
            value: EntityDescriptor(Number).required() 
        }
    }
}

С помощью oneOf() обозначают допустимый набор значений.

// Module.ts
import { Control } from 'UI/Base';
import entity = require('Types/entity');

export default class extends Control {
    static getOptionTypes(): object {
        return {
            alignment: entity.descriptor(String).oneOf([
                'center',
                'left',
                'right'
            ])
        }
    }
}

Синхронизация опций

Wasaby по умолчанию синхронизирует изменения опций в одном направлении — из родительского контрола к дочерним. Для создания двусторонней синхронизации используют директиву bind.

<!-- Module.wml -->
<Demo.TextBox bind:text="myText" />

При этом из контрола, для которого применяется директива bind, должно быть опубликовано событие на изменение значения свойства. Имя публикуемого события должно соответствовать формату <опция дочернего контрола>Changed.

// Module.ts
import { Control, TemplateFunction } from 'UI/Base';
import * as template from 'wml!Demo/Module';

export default class extends Control {
    protected _template: TemplateFunction = template;
    myText = "Hello Wasaby";
}
// TextBox.ts
import { Control, TemplateFunction } from 'UI/Base';
import * as template from 'wml!Demo/TextBox';

export default class extends Control {
    protected _template: TemplateFunction = template;
    changeText(text: string): void {
        this._notify('textChanged', [text]);
    }
}

Полная форма записи директивы такова:

bind:<опция дочернего контрола>="опция родительского контрола"

Событие "<опция дочернего контрола>Changed" может быть предназначено для bind, который объявлен в контроле, который располагается выше родительского контрола. В этом случае нужно выполнить одно из следующих условий:

  1. Отправлять событие с параметром bubbling:true (см. Всплываемость).
  2. Проксировать событие у родительского контрола. Примечание: для проксирования не нужно объявлять bind у родительского контрола. Событие достаточно поймать, а затем — опубликовать такое же.
    Ниже показан пример как это реализуется для трёх контрол:
    • Control-1
      <!-- Control-1.wml -->
      <Demo.Control-2 bind:title="myText" />
      // Control-1.ts
      import { Control, TemplateFunction } from 'UI/Base';
      import * as template from 'wml!Demo/Control-1';
      export default class extends Control {
          protected _template: TemplateFunction = template;
          myText = "Hello Wasaby";
      }
    • Control-2
      <!-- Control-2.wml -->
      <Demo.Control-3 title="{{ _options.title }}" on:titleChanged="changeTitle()" />
      // Control-2.ts
      import { Control, TemplateFunction } from 'UI/Base';
      import * as template from 'wml!Demo/Control-2';
      export default class extends Control {
          protected _template: TemplateFunction = template;
          changeTitle(title): void {
              this._notify('titleChanged', [title]);
          }
      }
    • Control-3
      // Control-3.ts
      import { Control, TemplateFunction } from 'UI/Base';
      import * as template from 'wml!Demo/Control-3';
      export default class extends Control {
          protected _template: TemplateFunction = template;
          changeTitle(title): void {
              this._notify('titleChanged', [title]);
          }
      }

При обратном биндинге можно ссылаться на:

  • Cвойства контрола
bind:prop="myProp"
  • Подсвойства любого уровня вложенности
bind:prop="myProp.innerProp"

При обратном биндинге нельзя:

  • Ссылаться на опции, потому что они могут быть перезаписаны при запуске перерисовки от родительского контрола. Подробнее читайте здесь.
  • Менять подсвойства опций. В потоке данных предполагается передача данных: "сверху вниз" через опции, "снизу вверх" через директиву bind или события. Передача данных через свойство объекта усложняет понимание, отладку и логику приложения. При изменении свойства обычного (неверсионированного) объекта перерисовка не будет вызвана.

Особенности передачи опций сверху вниз

Опции можно пробрасывать сверху вниз двумя способами:

  1. Передавать каждую опцию отдельно, указывая только те из них, которые необходимы внутри контентной опции.
  2. Указать опцию scope="{{someObject}}". При этом будут проброшены вниз все опции из someObject. Часто (например, в HOC-ах) предпочитают прокидывать все опции насквозь следующим образом: scope="{{_options}}".

Эти два способа можно совмещать. Тогда в качестве основы будут взяты опции из scope, а остальные будут добавлены поверх. Подробнее описано здесь.

Также существует функционал автопрокидывания опций. Контрол, который вставляется в контентной опции, помимо указанных опций получит опции, переданные в контентную опцию.

Например:

<!-- Control.wml -->
<SomeControls.Hoc value="123">
    <div>
        <SomeControls.Input/>
    </div>
</SomeControls.Hoc>
<!-- Hoc.wml -->
<ws:partial template="{{_options.template}}" scope="{{_options}}"/>

В данном примере value задан для контрола Hoc. В Hoc вставляется partial, где через scope прокидываются все опции. Это значит, что опция value передается в контентную опцию. В контентной опции вставляется контрол Input, в который явно не задано никаких опций, но опция value автоматически пробросится в Input.

Автопрокидывание может вызывать ошибки, автоматически без ведома пользователя могут проброситься опции, которые не нужны в контроле, и, более того, мешают там.

Чтобы избежать таких проблем, предлагается 2 варианта решения:

  1. Проверить, какие опции передаются в контентную опцию. Вместо прокидывания всех опций через scope, достаточно передавать только те опции, которые действительно нужны в контентной опции.
  2. Использовать _preventMergeOptions, чтобы запретить автопрокидывание опций в контрол.

Пример.

<!-- Control.wml -->
<SomeControls.Input _preventMergeOptions="{{ true }}"/>

В этом случае опция value не будет автоматически прокинута в контрол Input.

Вместо использования передачи всех опций через scope="{{_options}}" и отключения слияния опций с помощью _preventMergeOptions = "{{ true }}", следует передавать в partial только те опции которые действительно необходимы.

В следующем примере в OuterControl содержится две опции value1 и value2. Чтобы в контентный контрол InnerControl передать только value1, укажите в partial опцию которую необходимо передать.

<!-- OuterControl.wml -->
<Example.Hoc value1="1" value2="2">
    <ws:content>
        <div>
            <InnerControl />
        </div>
    </ws:content>
</Example.Hoc>
<!-- Нос.wml -->
<ws:partial 
   template="{{_options.content}}" 
   value1="{{_options.value1}}"/>
<!-- InnerControl.wml -->
<div>
   {{_options.value1}}
</div>

В данном случае, в контентной опции контрола OuterControl открыт доступ к области видимости текущего шаблона. Через название контентной опции обеспечивается доступ к переменным, которые передаются при вставке опции. Опции автоматически прокидываются в контролы, которые вставлены внутри контентной опции.

В следующем примере доступ к value1 организован через название контентной опции.

<!-- InnerControl.wml -->
{{ content.value1 }}

Свойства контрола

Каждый контрол хранит некоторое внутреннее состояние, представленное набором свойств. В каждый момент времени контрол может отличаться своим состоянием. Состояние контрола используется для работы контрола и организуется разработчиком.

Свойства, которые принимают значения типов Number, Boolean и String, объявляют на прототипе. В экземпляр класса контрола они копируются "по значению".

// Module.ts
import { Control } from 'UI/Base';

export default class extends Control {
    simpleVariable: 1;
}

​​Если свойства принимают значения типов Array или Object, то ссылки на них должны быть явно добавлены на инстанс контрола в хуке _beforeMount.

// Module.ts
import { Control } from 'UI/Base';

export default class extends Control {
    filter: null;
    protected _beforeMount(): void {
        this.filter = {
            // Значения объекта.
        }
    }
}

В модуле доступ к свойствам осуществляется через указатель this.

Примечание. При использовании нативных возможностей TypeScript, объявлять свойства типов Object и Array можно так:

class MyClass extends Control {
    myProp: string[] = ['a', 'b', 'c']
}

Доступ к свойствам в шаблоне

Свойства контрола доступны в области видимости его шаблона в качестве переменных. Подробнее о работе с переменными в шаблоне написано здесь.

// Module.ts
import { Control } from 'UI/Base';

export default class extends Control {
    simpleVariable: 1;
}
<!-- Template.wml -->
<div>
{{ simpleVariable }}   <!-- 1 -->
</div>