Правила разработки контрола на TypeScript

Импорты

// Хорошо
import {Control, IControlOptions} from 'UI/Base';
// Плохо
import Control = require('Core/Control');

Через require допускается загружать только модули, которые грузятся с использованием плагина RequireJS, например wml.

// Хорошо
import template = require('wml!MyModule/MyControl');

Интерфейсы

export interface ICheckableOptions {
   value?: boolean;
}

/**
 * Interface for 2-position control.
 *
 * @interface Controls/_toggle/interface/ICheckable
 * @public
 * @author Красильников А.С.
 */
export interface ICheckable {
   readonly '[Controls/_toggle/interface/ICheckable]': boolean;
}
/**
 * @name Controls/_toggle/interface/ICheckable#value
 * @cfg {Boolean} Current state.
 */
/**
 * @event Controls/_toggle/interface/ICheckable#valueChanged Occurs when value changes.
 * @cfg {Boolean} New value.
 */ 
  1. Файл с интерфейсом экспортирует отдельный интерфейс с опциями. Название – имя интерфейса + Options.
    export interface ICheckableOptions {
       value?: boolean;
    }
  2. Файл с интерфейсом экспортирует сам интерфейс:
    export interface ICheckable {...}
  3. Всю документацию по интерфейсу опишите в файле интерфейса.
  4. Если интерфейс используется не более чем в одном классе, для него можно не создавать отдельный файл.
  5. Если интерфейс используется в рамках одной библиотеки, его размещают в директории interface внутри приватной папки библиотеки. И такой интерфейс должен экспортироваться библиотекой.
  6. Если интерфейс используется в нескольких библиотеках, его размещают в директории interface в корне интерфейсного модуля. И такой интерфейс должен экспортироваться библиотекой interface.
    • Это правило используется в интерфейсном модуле Controls. Папка interface необязательная. В корне интерфейсного модуля можно разместить общие для нескольких библиотек интерфейсы.
      Controls/_toggle/interface/ICheckable.d.ts
  7. В файле контрола, реализующего интерфейс – импортируйте интерфейс и пропишите в директиве implements.
    import ICheckable from './interface/ICheckable';
    class BigSeparator extends Control implements ICheckable {...}
  8. Описание интерфейса в доклетах идет прямо перед его объявлением в коде, а описание опций, методов и событий — в конце файла.
    /**
     * Interface for 2-position control.
     *
     * @interface Controls/_toggle/interface/ICheckable
     * @public
     * @author Красильников А.С.
     */
    export interface ICheckable {
       readonly '[Controls/_toggle/interface/ICheckable]': boolean;
    }
    /**
     * @name Controls/_toggle/interface/ICheckable#value
     * @cfg {Boolean} Current state.
     */
    /**
     * @event Controls/_toggle/interface/ICheckable#valueChanged Occurs when value changes.
     * @cfg {Boolean} New value.
     */ 
  9. Поле типа Boolean с именем интерфейса в квадратных скобках делается по двум причинам:
    • В TS/ES6 нет нативной проверки на имплементацию инстансом интерфейса (аналога instanceof для интерфейсов). Подобное поле с уникальным именем позволит писать проверки вида:
      if (instance['[Controls/_toggle/interface/ICheckable]'])
    • TS ругается на пустой интерфейс.

Интерфейсы для опций

  1. Импортируйте опции интерфейса и всех контролов, от которых наследуетесь.
    import {ICheckable, ICheckableOptions} from './interface/ICheckable';
  2. Каждый контрол и интерфейс экспортирует свой набор опций. Для этого объявите интерфейс опций и соберите его из всех родительских интерфейсов-опций. Вот пример для наследника Control и интерфейса ICheckable.
    export interface IBigSeparatorOptions extends IControlOptions, ICheckableOptions {...}
  3. Поле _options описано в базовом UI/Base:Control, и его можно не описывать его в своих контролах.
  4. Интерфейс опций используйте в хуках жизненного цикла, чтоб не ругался tslint.
    protected _beforeMount(newOptions: IBigSeparatorOptions): void {
       this._iconChangedValue(newOptions.value);
    }
     
    protected _beforeUpdate(newOptions: IBigSeparatorOptions): void {
       this._iconChangedValue(newOptions.value);
    }

Классы и методы

  1. Контрол — это класс. Базовый Control – Generic class, который принимает интерфейс опций.
    // Хорошо
    class BigSeparator extends Control<IBigSeparatorOptions> implements ICheckable {...}
    // Плохо
    var BigSeparator = Control.extend({...})
  2. Приватные методы оформляйте синтаксисом TypeScript, называем с подчеркиванием, в том числе protected.
    // Хорошо
    class BigSeparator extends Control<IBigSeparatorOptions> implements ICheckable {
     
       private _iconChangedValue(value: boolean): void {
          if (value) {
             this._icon = 'icon-AccordionArrowUp ';
          } else {
             this._icon = 'icon-AccordionArrowDown ';
          }
       }
    }
    // Плохо
    var protected = {
       iconChangedValue: function (self, options) {
          if (options.value) {
             this._icon = 'icon-AccordionArrowUp ';
          } else {
             this._icon = 'icon-AccordionArrowDown ';
          }
       }
    }
  3. Хуки жизненного цикла — protected методы.
    protected _beforeMount(newOptions: ICheckableOptions): void {
       this._iconChangedValue(newOptions.value);
    }
     
    protected _beforeUpdate(newOptions: ICheckableOptions): void {
       this._iconChangedValue(newOptions.value);
    }
  4. Обработчики событий – protected методы.
    protected _clickHandler(): void {
       this._notify('valueChanged', [!this._options.value])
    }
    Для аргумента типа события event используется
    import {SyntheticEvent} from 'Vdom/Vdom';
  5. getDefaultOptions, getOptionTypes – статические методы.
    static getDefaultOptions(): ICheckableOptions {
       return {
          value: false
       };
    }
     
    static getOptionTypes(): object {
       return {
          value: EntityDescriptor(Boolean)
       };
    }
  6. У методов и переменных указывайте корректные модификаторы.
    protected _icon: string;
    protected _clickHandler(): void {
       this._notify('valueChanged', [!this._options.value])
    }
  7. Расставляйте типы у полей, аргументов методов и возвращаемых значений методов.
    protected _iconChangedValue(value: boolean): void {
       if (value) {
          this._icon = 'icon-AccordionArrowUp ';
       } else {
          this._icon = 'icon-AccordionArrowDown ';
       }
    }
    protected _icon: string;
  8. Тему и template оформляйте следующим образом (TemplateFunction импортим из библиотеки UI/Base):
    import {Control, IControlOptions, TemplateFunction} from 'UI/Base';
    protected _template: TemplateFunction = checkBoxTemplate;
    static _theme: string[] = ['Controls/toggle'];
  9. Контрол по умолчанию экспортирует себя.
    // Хорошо.
    export default BigSeparetor;
    // Плохо.
    export = BigSeparetor;
    Исключение: На время переходного этапа, если ваш контрол не является частью библиотеки (например, контрол SwitchableArea) и его могут грузить через require, надо писать export = для совместимости
  10. Полное имя контрола в составе JavaScript библиотеки соответствует форме "Модульсбис/библиотека:контрол", например: Controls/list:View. В этом примере:
    • Conrtols — это имя модуля СБИС. Он физически хранится здесь, а также распространяется в составе SBIS SDK.
    • list — это имя JavaScript-библиотеки.
    • View — краткое имя контрола в составе библиотеки.

Экспорт из библиотек

// Хорошо.
export {default as BigsSeparator} from 'Controls/_toggle';
 
export {
   Button,
   Switch,
   DoubleSwitch
   RadioGroup,
   Checkbox,
   Separator,
   BigSeparator
}
// Плохо.
import BigSeparator = require ('Controls/_toggle/BigSeparator');
 
export {
   Button,
   Switch,
   DoublecSwitch,
   RaduioGrouop,
   Checkbox,
   Separator,
   BigSeparator
}

Кастомные типы

Если в публичном методе используется какой-то сложный тип аргумента, который объявляется в вашем модуле, такие типы лучше тоже экспортировать. Они могут потребоваться при задании типов переменных в прикладном коде.

Пример: тип where – аргумент вызова фильтрации на источнике данных

type Where = IHashMap<string> | ((item: any, index: number) => boolean);

Набор утилит

Если модуль экспортирует утилиты: набор методов или констант, каждый метод/константу экспортим отдельно. В ts можно будет сделать удобный импорт нужных утилит по отдельности. А если вас импортят в js через require, то набор утилит придет в одном объекте и будет работать «как раньше».

export const showType = (...);

export function getMenuItems<T>(items: RecordSet<T> | T[]): ChainAbstract<T>{
   return factory(items).filter((item) =>{
      return item.get('showType') != this.showType.TOOLBAR;
   })
}

SyntheticEvent

protected _onMouseDownHandler(event: SyntheticEvent<MouseEvent>): void {
   if (!this._options.readOnly) {
      const box = this._children.area.getBoundingClientRect();
      const ratio = Utils.getRatio(event.nativeEvent.pageX, box.left + window.pageXOffset, box.width);
      this._value = Utils.calcValue(this._options.minValue, this._options.maxValue, ratio, this._options.precision);
      this._setValue(this._value);
      this._children.dragNDrop.startDragNDrop(this._children.point, event);
   }
}

См. также