Правила разработки контрола на 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;
}
  1. Файл с интерфейсом экспортирует сам интерфейс:
export interface ICheckable {...}
  1. Всю документацию по интерфейсу опишите в файле интерфейса.
  2. Если интерфейс используется не более чем в одном классе, для него можно не создавать отдельный файл.
  3. Если интерфейс используется в рамках одной библиотеки, его размещают в директории interface внутри приватной папки библиотеки. И такой интерфейс должен экспортироваться библиотекой.
  4. Если интерфейс используется в нескольких библиотеках, его размещают в директории interface в корне интерфейсного модуля. И такой интерфейс должен экспортироваться библиотекой interface.
    • Это правило используется в интерфейсном модуле Controls. Папка interface необязательная. В корне интерфейсного модуля можно разместить общие для нескольких библиотек интерфейсы.
      Controls/_toggle/interface/ICheckable.d.ts
  5. В файле контрола, реализующего интерфейс – импортируйте интерфейс и пропишите в директиве implements.
import ICheckable from './interface/ICheckable';
class BigSeparator extends Control implements ICheckable {...}
  1. Описание интерфейса в доклетах идет прямо перед его объявлением в коде, а описание опций, методов и событий — в конце файла.
/**
 * 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. Поле типа Boolean с именем интерфейса в квадратных скобках делается по двум причинам:
    • В TS/ES6 нет нативной проверки на имплементацию инстансом интерфейса (аналога instanceof для интерфейсов). Подобное поле с уникальным именем позволит писать проверки вида:
      if (instance['[Controls/_toggle/interface/ICheckable]'])
    • TS ругается на пустой интерфейс.

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

  1. Импортируйте опции интерфейса и всех контролов, от которых наследуетесь.
import {ICheckable, ICheckableOptions} from './interface/ICheckable';
  1. Каждый контрол и интерфейс экспортирует свой набор опций. Для этого объявите интерфейс опций и соберите его из всех родительских интерфейсов-опций. Вот пример для наследника Control и интерфейса ICheckable.
export interface IBigSeparatorOptions extends IControlOptions, ICheckableOptions {...}
  1. Поле _options описано в базовом UI/Base:Control, и его можно не описывать его в своих контролах.
  2. Интерфейс опций используйте в хуках жизненного цикла, чтоб не ругался 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({...})
  1. Приватные методы оформляйте синтаксисом TypeScript — название метода начинается с подчеркивания. Например: _iconChangedValue.
// Хорошо
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 ';
      }
   }
}
  1. Хуки жизненного цикла — protected методы.
protected _beforeMount(newOptions: ICheckableOptions): void {
   this._iconChangedValue(newOptions.value);
}
 
protected _beforeUpdate(newOptions: ICheckableOptions): void {
   this._iconChangedValue(newOptions.value);
}
  1. Обработчики событий – protected методы.
protected _clickHandler(): void {
   this._notify('valueChanged', [!this._options.value])
}

Для аргумента типа события event используется

import {SyntheticEvent} from 'Vdom/Vdom';
  1. defaultPropsстатическое поле, getOptionTypesстатический метод.
static defaultProps: Partial<IOptions> = {
   value: false
};
static getOptionTypes(): object {
   return {
      value: EntityDescriptor(Boolean)
   };
}
  1. У методов и переменных указывайте корректные модификаторы.
protected _icon: string;
protected _clickHandler(): void {
   this._notify('valueChanged', [!this._options.value])
}
  1. Расставляйте типы у полей, аргументов методов и возвращаемых значений методов.
protected _iconChangedValue(value: boolean): void {
   if (value) {
      this._icon = 'icon-AccordionArrowUp ';
   } else {
      this._icon = 'icon-AccordionArrowDown ';
   }
}
protected _icon: string;
  1. Тему и template оформляйте следующим образом (TemplateFunction импортим из библиотеки UI/Base):
import {Control, IControlOptions, TemplateFunction} from 'UI/Base';
protected _template: TemplateFunction = checkBoxTemplate;
static _theme: string[] = ['Controls/toggle'];
  1. Контрол по умолчанию экспортирует себя.
// Хорошо.
export default BigSeparetor;
// Плохо.
export = BigSeparetor;

Исключение: На время переходного этапа, если ваш контрол не является частью библиотеки (например, контрол SwitchableArea) и его могут грузить через require, надо писать export = для совместимости 10. Полное имя контрола в составе JavaScript библиотеки соответствует форме "Модульсбис/библиотека:контрол", например: Controls/list:View. В этом примере: * Controls — это имя модуля СБИС. Он физически хранится здесь, а также распространяется в составе SBIS SDK. * list — это имя JavaScript-библиотеки. * View — краткое имя контрола в составе библиотеки.

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

// Хорошо.
export {default as BigsSeparator} from 'Controls/_toggle/BigsSeparator';
// Плохо.
import BigSeparator = require ('Controls/_toggle/BigSeparator');
 
export {
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);
   }
}

См. также