LeCodesdocs

Модель элементов UI

Как работает UI-слой в целом: элементы — это обычные объекты, создаваемые глобальными фабричными функциями, компонуемые через массивы детей, настраиваемые цепочкой и обновляемые изменением свойств — без JSX, без virtual DOM, без цикла ре-рендера. Раскладка — флексбокс (Yoga) в логических px, экранное пространство Y вниз. Эта страница про модель; остальная часть раздела — про элементы:

Обзор

TypeScript
let counter = 0
let label: UIText

const screen = UIScreen([
  label = UIText("Taps: 0").style({ color: "white", fontSize: 24, fontWeight: 700 }),
  counter > 0 ? UIText("already tapped") : null,        // null-дети пропускаются
  UIButton([ UIText("Tap").style({ color: "white" }) ])
    .style({ bgColor: "#FF4032", borderRadius: 12, p: 16, alignSelf: "flex-start" })
    .onClick(() => { label.text = `Taps: ${++counter}` }),   // меняем контент напрямую
]).style({ bgColor: "black", p: 20, pt: "max(safe-top, 24px)", gap: 16 })

screen.open()

Фабрики, не конструкторы

Каждый элемент создаётся вызовом глобальной функции — никогда не new:

TypeScript
UIColumn(children?)              // контейнеры: необязательный массив детей
UIText(text)                     // элементы контента: сам контент
UIImage(src)

Фабрика принимает только контент элемента; всё остальное настраивается цепочкой — .style(), .onClick(), .append(), .animateTo() — каждый возвращает сам элемент, поэтому конструирование читается одной цепочкой.

Legacy

каждая фабрика также принимает объект стиля необязательным первым аргументом (UIText({ fontSize: 20 }, "Hi")). Он всё ещё работает и встречается в старых проектах, но новый код задаёт стили через .style().

Элементы несут строку readonly type ("column", "text", …), определяющую, что они такое.

Массивы детей и условный рендер

Контейнеры принимают обычный массив. Запись null, undefined или false пропускается целиком — ни элемента, ни слота раскладки — это и есть идиома условного рендера:

TypeScript
UIColumn([
  header,
  isLoading ? spinner : null,     // тернарник с null
  showFooter && footer,           // && с коротким замыканием
])
TypeScript
// ✗ пустой контейнер как заглушка (веб-привычка) — он всё равно занимает флекс-слот
UIRow([ isGroup ? button : UIColumn([]) ])
// ✓ null пропускается — без фантомного элемента
UIRow([ isGroup ? button : null ])

Захват ссылок

Чтобы держать хэндл на вложенный элемент, используй выражение присваивания внутри массива — оно и задаёт переменную, и добавляет элемент:

TypeScript
let label: UIText
let input: UIInput

UIColumn([
  label = UIText("Hello"),
  input = UIInput(),
])

label.text = "Updated"       // позже
console.log(input.value)

let — это оператор, а не выражение — объявляй снаружи, присваивай внутри:

TypeScript
UIRow([ let input = UIInput() ])   // ✗ синтаксическая ошибка

Предпочитай захваченные ссылки индексации container.children — записи children не типизированы (UINodeChild), поэтому их обратное чтение требует приведения типа.

Свойства контента против стилей

Изменяемый контент живёт прямо на элементе, не в стиле. Его установка сразу перерисовывает, смонтирован он или нет:

TypeScript
text.text = "Updated"        // UIText
input.value = ""             // UIInput / UITextArea
image.src = newUrl           // UIImage
video.player                 // UIVideo — только чтение, ссылка на его VideoPlayer

Стили несут только внешний вид и раскладку. В частности, никогда не клади контент изображения в bgImage — это фон контейнера. См. content.md.

Императивное обновление детей

Контейнеры (UIRow, UIColumn, UIScrollable, …) предоставляют прямую манипуляцию детьми — диффинга нет; ты заявляешь изменение:

TypeScript
list.setContent(items.map(Row))       // заменить всех детей
list.append(row1, row2)               // добавить в конец
list.insert(0, banner)                // добавить по индексу (в .children)
list.remove(row1)                     // удалить по идентичности
list.children                         // текущий массив (только чтение)

Все четыре работают и до, и после появления элемента на экране. Для длинных или неограниченных данных используй UIVirtualizedList вместо setContent по большому массиву.

Чтение измеренного размера — onLayout

Размеры существуют только после раскладки. Синхронного геттера нет (нет el.width, нет getBoundingClientRect) — onLayout — единственный способ прочитать бокс элемента:

TypeScript
el.onLayout(({ left, top, width, height }) => { ... })   // логические px; top/left относительно родителя

Он срабатывает, когда элемент впервые получает раскладку, и снова при каждом изменении его бокса (ресайз, смена контента). Можно зарегистрировать несколько колбэков; каждый вызов чейнится.

Переиспользуемые компоненты

«Компонент» — это просто фабричная функция, возвращающая элемент — чейнься по результату как по любому элементу. Общие стили — обычные объекты, типизированные глобальным хелпером Style<T> (только тип — нечего импортировать или инстанцировать):

TypeScript
const heading: Style<UIText> = { color: "white", fontSize: 24, fontWeight: 700 }

const Card = (title: string, subtitle: string) => UIButton([
  UIText(title).style({ fontWeight: 700, color: "white" }),
  UIText(subtitle).style({ color: "#888", fontSize: 13 }),
]).style({ px: 16, py: 12, gap: 4, flexDirection: "column", alignItems: "flex-start" })

Card("Title", "Subtitle").style({ bgColor: "#111" }).onClick(() => { ... })
Note

каждый элемент принимает строку name в объекте стиля — семантическую метку, не стиль. Она извлекается при конструировании и выставляется на узле (el.name) как стабильный селектор для тестов и инструментов ревью.

Смотрите также