Основные принципы будущего решения:
- Точкой входа в компонент является JS-файл.
- Поддержка реактивных примитивов (см. Vue или @preact/signals).
Из главных изменений (по сравнению с текущей версией Endorphin):
- Шаблон указывается внутри фабрики компонента с помощью Tagged Templates. Это решает сразу несколько проблем: бесплатно получаем валидацию, type checking и автокомплит данных, а также используем готовые плагины типа Lit для подсветки синтаксиса внутри такой строки.
- Локальные переменные, объявленные внутри фабрики компонента, автоматически становятся реактивными. То есть вместо
const enabled = ref(false)как во Vue можно писатьlet enabled = falseи компилятор автоматически сделает из него реактивное значение.
import{defineComponent,html,computed,useComponent,onDestory}from'endorphin';importAnotherComponentfrom'./AnotherComponent.ts';// Импорт стилей компонента. Стили так же автоматически преобразуются и скоупятсяimport'./style.css';exportinterfaceProps{enabled: boolean;name: string;}// Общая переменная модуля, как в обычном JSletinstances=0;constitems=['a','b','c'];// Объявляем компонент через вызов `defineComponent()`.// Вернётся класс, который в любом месте можно создать так:// import MyComponent from './component.ts';// const c = new MyComponent(props);exportdefaultfunctionMyComponent({ enabled, name }: Props){// Вызов текущей функции работает как `willMount()` из предыдущей// версии эндорфина// Свободно обращаемся к общим переменным модуляinstances++;// Локальные переменные, которые буду использоваться в шаблоне,// объявляем в JS: получим бесплатную типизацию из JS/TSletitem: string;letinnerValue=1;// Объявляем реактивныем переменные: они будут автоматически пересчитываться,// когда поменяется любое значение внутри коллбэкаconstfullName=computed(()=>enabled ? name : 'Disabled');constonItemClick=(item: string)=>{console.log('Clicked on',item);};// Добавление методов жизненного цикла будет работать через вызов// функции модуляonDestory(()=>{instances--;console.log('component destroyed')});// Тут самое неприятное место — необходимо явно зарегистрировать использование// вложенных компонентов в формате `{имяКомпонента: класс}`, чтобы не было// не использованных переменных и чтобы компилятор знал, как связывать имя// элемента с конструктором компонентаuseComponent({ AnotherComponent });// Шаблон указываем через tagged template literal.// Это даёт следующее:// * нативная валидация, code check и выведение типов, так как тут пишем// обычный JS// * в этом месте подключается компилятор, который заменяет вызов `html`// на скомпилированную функцию рендеринга// * все переменные, которые используются внутри tagged template, автоматически// становятся реактивными: их изменение приводит к ре-рендеру шаблона// * можно использовать плагин для Lit, который подсветит HTML внутри такой строкиreturnhtml`<divclass="${innerValue ? 'foo' : 'bar'}"class:enabled=${enabled}><e:iftest=${enabled}><p@click=${innerValue++}>${fullName}</p><AnotherComponentprop=${name}/></e:if><ul><e:for-eachselect=${items}as=${item}><liclass="item" @click|prevent=${onItemClick(item)}>${item}</li></e:for-each></ul></div>`;}Особенность примера выше: с помощью метода defineComponent() мы подсказываем компилятору, что внутри функции описывается компонент и нужно модифицировать её тело таким образом, чтобы всё правильно обновлялось.
Такой же подсказкой может быть и наличие JSX узла в AST функции. Поэтому с небольшой модификацией парсера можно использовать React-подобный контракт и синтаксис, чтобы переиспользовать инфраструктуру React-инструментов для Endorphin. Вот как выглядит пример выше в JSX-стиле (вроде такой код нормально отрабатывается компилятором):
import{computed,onDestory}from'endorphin';importAnotherComponentfrom'./AnotherComponent.ts';// Импорт стилей компонента. Стили так же автоматически преобразуются и скоупятсяimport'./style.css';exportinterfaceProps{enabled: boolean;name: string;}// Общая переменная модуля, как в обычном JSletinstances=0;constitems=['a','b','c'];// В качестве описания компонента используем обычную функцию с JSX внутриexportdefaultfunctionMyComponent({ enabled, name }: Props){instances++;letitem: string;letinnerValue=1;constfullName=computed(()=>enabled ? name : 'Disabled');constonItemClick=(item: string)=>{console.log('Clicked on',item);};onDestory(()=>{instances--;console.log('component destroyed')});return<divclass={innerValue ? 'foo' : 'bar'}class:enabled={enabled}><iftest={enabled}><ponClick={innerValue++}>{fullName}</p><AnotherComponentprop={name}/></if><ul><forselect={items}as={item}><liclass="item"onClick:prevent={onItemClick(item)}>{item}</li></for></ul></div>;}