Модель элементов UI
Как работает UI-слой в целом: элементы — это обычные объекты, создаваемые глобальными фабричными функциями, компонуемые через массивы детей, настраиваемые цепочкой и обновляемые изменением свойств — без JSX, без virtual DOM, без цикла ре-рендера. Раскладка — флексбокс (Yoga) в логических px, экранное пространство Y вниз. Эта страница про модель; остальная часть раздела — про элементы:
- Стилизация —
.style(), единицы, безопасные зоны,animateTo - Контейнеры —
UIRow,UIColumn,UIScrollable,UISpacer - Элементы контента —
UIText,UIImage,UIVideo - Кнопки и поля ввода —
UIButton,UIInput,UITextArea - Экраны и навигация —
UIScreen,Router - Виджеты — оверлеи и модалки
UIWidget - Виртуализированные списки —
UIVirtualizedList - Шрифты —
registerFont
Обзор
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:
UIColumn(children?) // контейнеры: необязательный массив детей
UIText(text) // элементы контента: сам контент
UIImage(src)Фабрика принимает только контент элемента; всё остальное настраивается цепочкой — .style(),
.onClick(), .append(), .animateTo() — каждый возвращает сам элемент, поэтому конструирование
читается одной цепочкой.
каждая фабрика также принимает объект стиля необязательным первым аргументом
(UIText({ fontSize: 20 }, "Hi")). Он всё ещё работает и встречается в старых проектах, но новый
код задаёт стили через .style().
Элементы несут строку readonly type ("column", "text", …), определяющую, что они такое.
Массивы детей и условный рендер
Контейнеры принимают обычный массив. Запись null, undefined или false пропускается целиком
— ни элемента, ни слота раскладки — это и есть идиома условного рендера:
UIColumn([
header,
isLoading ? spinner : null, // тернарник с null
showFooter && footer, // && с коротким замыканием
])// ✗ пустой контейнер как заглушка (веб-привычка) — он всё равно занимает флекс-слот
UIRow([ isGroup ? button : UIColumn([]) ])
// ✓ null пропускается — без фантомного элемента
UIRow([ isGroup ? button : null ])Захват ссылок
Чтобы держать хэндл на вложенный элемент, используй выражение присваивания внутри массива — оно и задаёт переменную, и добавляет элемент:
let label: UIText
let input: UIInput
UIColumn([
label = UIText("Hello"),
input = UIInput(),
])
label.text = "Updated" // позже
console.log(input.value)let — это оператор, а не выражение — объявляй снаружи, присваивай внутри:
UIRow([ let input = UIInput() ]) // ✗ синтаксическая ошибкаПредпочитай захваченные ссылки индексации container.children — записи children не типизированы
(UINodeChild), поэтому их обратное чтение требует приведения типа.
Свойства контента против стилей
Изменяемый контент живёт прямо на элементе, не в стиле. Его установка сразу перерисовывает, смонтирован он или нет:
text.text = "Updated" // UIText
input.value = "" // UIInput / UITextArea
image.src = newUrl // UIImage
video.player // UIVideo — только чтение, ссылка на его VideoPlayerСтили несут только внешний вид и раскладку. В частности, никогда не клади контент изображения в
bgImage — это фон контейнера. См. content.md.
Императивное обновление детей
Контейнеры (UIRow, UIColumn, UIScrollable, …) предоставляют прямую манипуляцию детьми —
диффинга нет; ты заявляешь изменение:
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 — единственный способ прочитать бокс элемента:
el.onLayout(({ left, top, width, height }) => { ... }) // логические px; top/left относительно родителяОн срабатывает, когда элемент впервые получает раскладку, и снова при каждом изменении его бокса (ресайз, смена контента). Можно зарегистрировать несколько колбэков; каждый вызов чейнится.
Переиспользуемые компоненты
«Компонент» — это просто фабричная функция, возвращающая элемент — чейнься по результату как по
любому элементу. Общие стили — обычные объекты, типизированные глобальным хелпером Style<T> (только
тип — нечего импортировать или инстанцировать):
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(() => { ... })каждый элемент принимает строку name в объекте стиля — семантическую метку, не стиль. Она
извлекается при конструировании и выставляется на узле (el.name) как стабильный селектор для тестов
и инструментов ревью.
Смотрите также
- Стилизация — модель
.style(), которую предполагает эта страница. - События указателя и жесты — объекты событий
onClick/onTouchStart. - Соглашения — логические px, цепочки, правила жизненного цикла.