Правила разработки контрола на 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.
*/
- Файл с интерфейсом экспортирует отдельный интерфейс с опциями. Название – имя интерфейса + Options.
export interface ICheckableOptions {
value?: boolean;
}
- Файл с интерфейсом экспортирует сам интерфейс:
export interface ICheckable {...}
- Всю документацию по интерфейсу опишите в файле интерфейса.
- Если интерфейс используется не более чем в одном классе, для него можно не создавать отдельный файл.
- Если интерфейс используется в рамках одной библиотеки, его размещают в директории
interface
внутри приватной папки библиотеки. И такой интерфейс должен экспортироваться библиотекой. - Если интерфейс используется в нескольких библиотеках, его размещают в директории
interface
в корне интерфейсного модуля. И такой интерфейс должен экспортироваться библиотекойinterface
.- Это правило используется в интерфейсном модуле Controls. Папка
interface
необязательная. В корне интерфейсного модуля можно разместить общие для нескольких библиотек интерфейсы.Controls/_toggle/interface/ICheckable.d.ts
- Это правило используется в интерфейсном модуле Controls. Папка
- В файле контрола, реализующего интерфейс – импортируйте интерфейс и пропишите в директиве
implements
.
import ICheckable from './interface/ICheckable';
class BigSeparator extends Control implements ICheckable {...}
- Описание интерфейса в доклетах идет прямо перед его объявлением в коде, а описание опций, методов и событий — в конце файла.
/**
* 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.
*/
- Поле типа
Boolean
с именем интерфейса в квадратных скобках делается по двум причинам:- В TS/ES6 нет нативной проверки на имплементацию инстансом интерфейса (аналога
instanceof
для интерфейсов). Подобное поле с уникальным именем позволит писать проверки вида:if (instance['[Controls/_toggle/interface/ICheckable]'])
- TS ругается на пустой интерфейс.
- В TS/ES6 нет нативной проверки на имплементацию инстансом интерфейса (аналога
Интерфейсы для опций
- Импортируйте опции интерфейса и всех контролов, от которых наследуетесь.
import {ICheckable, ICheckableOptions} from './interface/ICheckable';
- Каждый контрол и интерфейс экспортирует свой набор опций. Для этого объявите интерфейс опций и соберите его из всех родительских интерфейсов-опций. Вот пример для наследника
Control
и интерфейсаICheckable
.
export interface IBigSeparatorOptions extends IControlOptions, ICheckableOptions {...}
- Поле
_options
описано в базовомUI/Base:Control
, и его можно не описывать его в своих контролах. - Интерфейс опций используйте в хуках жизненного цикла, чтоб не ругался
tslint
.
protected _beforeMount(newOptions: IBigSeparatorOptions): void {
this._iconChangedValue(newOptions.value);
}
protected _beforeUpdate(newOptions: IBigSeparatorOptions): void {
this._iconChangedValue(newOptions.value);
}
Классы и методы
- Контрол — это класс. Базовый
Control – Generic class
, который принимает интерфейс опций.
// Хорошо
class BigSeparator extends Control<IBigSeparatorOptions> implements ICheckable {...}
// Плохо
var BigSeparator = Control.extend({...})
- Приватные методы оформляйте синтаксисом
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 ';
}
}
}
- Хуки жизненного цикла —
protected
методы.
protected _beforeMount(newOptions: ICheckableOptions): void {
this._iconChangedValue(newOptions.value);
}
protected _beforeUpdate(newOptions: ICheckableOptions): void {
this._iconChangedValue(newOptions.value);
}
- Обработчики событий – protected методы.
protected _clickHandler(): void {
this._notify('valueChanged', [!this._options.value])
}
Для аргумента типа события event
используется
import {SyntheticEvent} from 'Vdom/Vdom';
defaultProps
– статическое поле,getOptionTypes
– статический метод.
static defaultProps: Partial<IOptions> = {
value: false
};
static getOptionTypes(): object {
return {
value: EntityDescriptor(Boolean)
};
}
- У методов и переменных указывайте корректные модификаторы.
protected _icon: string;
protected _clickHandler(): void {
this._notify('valueChanged', [!this._options.value])
}
- Расставляйте типы у полей, аргументов методов и возвращаемых значений методов.
protected _iconChangedValue(value: boolean): void {
if (value) {
this._icon = 'icon-AccordionArrowUp ';
} else {
this._icon = 'icon-AccordionArrowDown ';
}
}
protected _icon: string;
- Тему и
template
оформляйте следующим образом (TemplateFunction
импортим из библиотекиUI/Base
):
import {Control, IControlOptions, TemplateFunction} from 'UI/Base';
protected _template: TemplateFunction = checkBoxTemplate;
static _theme: string[] = ['Controls/toggle'];
- Контрол по умолчанию экспортирует себя.
// Хорошо.
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);
}
}