@madojs/mado 0.6.1 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +82 -30
- package/CHANGELOG.md +150 -3
- package/dist/src/component.d.ts +17 -4
- package/dist/src/component.js +43 -4
- package/dist/src/component.js.map +1 -1
- package/dist/src/forms.js +4 -1
- package/dist/src/forms.js.map +1 -1
- package/dist/src/page.d.ts +12 -0
- package/dist/src/page.js.map +1 -1
- package/dist/src/router/manifest.js +7 -1
- package/dist/src/router/manifest.js.map +1 -1
- package/docs/en/07-llm-pitfalls.md +197 -60
- package/docs/en/08-llm-zero-history-test.md +1 -1
- package/docs/en/17-shadow-dom-forms.md +192 -0
- package/docs/en/README.md +20 -19
- package/docs/fr/07-llm-pitfalls.md +196 -60
- package/docs/fr/17-shadow-dom-forms.md +196 -0
- package/docs/fr/README.md +20 -19
- package/docs/ru/07-llm-pitfalls.md +198 -61
- package/docs/ru/08-llm-zero-history-test.md +39 -38
- package/docs/ru/09-shadow-vs-light-dom.md +97 -81
- package/docs/ru/17-shadow-dom-forms.md +193 -0
- package/docs/ru/README.md +20 -19
- package/docs/uk/07-llm-pitfalls.md +64 -3
- package/docs/uk/17-shadow-dom-forms.md +193 -0
- package/docs/uk/README.md +20 -19
- package/llms.txt +50 -1
- package/package.json +2 -2
- package/scripts/bake.mjs +25 -19
- package/scripts/cli.mjs +22 -33
- package/starters/admin/src/components/x-button.ts +40 -13
- package/starters/admin/src/components/x-input.ts +50 -19
- package/starters/admin/src/lib/api.ts +55 -4
|
@@ -1,56 +1,57 @@
|
|
|
1
|
-
# LLM
|
|
1
|
+
# Тест LLM с нулевой историей
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Этот документ определяет практический валидационный тест для Mado.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-
|
|
5
|
+
Вопрос не в том, «может ли LLM генерировать фронтенд-код?» — может. Вопрос в том:
|
|
6
|
+
может ли свежая LLM написать идиоматичный Mado-код без скатывания в React-подобные
|
|
7
|
+
паттерны?
|
|
7
8
|
|
|
8
|
-
##
|
|
9
|
+
## Допустимый контекст
|
|
9
10
|
|
|
10
|
-
|
|
11
|
+
При первом проходе предоставьте агенту только:
|
|
11
12
|
|
|
12
13
|
- `AGENTS.md`
|
|
13
14
|
- `README.md`
|
|
14
|
-
- `docs/
|
|
15
|
-
- `examples/basic/README.md`
|
|
16
|
-
-
|
|
15
|
+
- `docs/en/07-llm-pitfalls.md`
|
|
16
|
+
- `examples/basic/README.md` если нужен минимальный обзор API
|
|
17
|
+
- конкретные файлы из `examples/showcase/**` только если агент сам попросит паттерн более крупного приложения
|
|
17
18
|
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
Агент может искать целевые API в `src/` когда заблокирован, но не должен
|
|
20
|
+
загружать весь фреймворк в контекст.
|
|
20
21
|
|
|
21
|
-
##
|
|
22
|
+
## Задание
|
|
22
23
|
|
|
23
|
-
|
|
24
|
+
Построить `examples/tickets`: маленький ticket-admin SPA для соло/бекенд-разработчика.
|
|
24
25
|
|
|
25
|
-
|
|
26
|
+
Требуемое поведение:
|
|
26
27
|
|
|
27
|
-
-
|
|
28
|
-
- in-memory mock API
|
|
29
|
-
-
|
|
30
|
-
|
|
31
|
-
-
|
|
32
|
-
-
|
|
33
|
-
- slotted shell, metric
|
|
34
|
-
- smoke
|
|
28
|
+
- маршруты: `/`, `/tickets`, `/tickets/new`, `/tickets/:id`, `*`;
|
|
29
|
+
- in-memory mock API с реалистичными асинхронными задержками;
|
|
30
|
+
- страница списка с `resource()`, `queryParam()` фильтрами поиска/статуса,
|
|
31
|
+
`computed()` и `each()` с ключами по строкам;
|
|
32
|
+
- сценарии создания и редактирования с `useForm()` + `mutation()` + `invalidates`;
|
|
33
|
+
- локальный UI-стейт через `signal()`;
|
|
34
|
+
- slotted shell, metric и badge компоненты для более реалистичного admin UI;
|
|
35
|
+
- smoke-тест, импортирующий собранный пример.
|
|
35
36
|
|
|
36
|
-
##
|
|
37
|
+
## Чеклист ошибок
|
|
37
38
|
|
|
38
|
-
|
|
39
|
+
Ищите следующее после реализации:
|
|
39
40
|
|
|
40
|
-
- JSX, `useState`, `useEffect`, `ref`, `$state
|
|
41
|
-
- `${signal()}`
|
|
42
|
-
- `disabled=${...}`
|
|
43
|
-
-
|
|
44
|
-
-
|
|
45
|
-
- `resource()
|
|
46
|
-
-
|
|
41
|
+
- JSX, `useState`, `useEffect`, `ref`, `$state` или class-style компоненты;
|
|
42
|
+
- `${signal()}` или `${signal() + 1}` где требуется реактивный child thunk;
|
|
43
|
+
- `disabled=${...}` вместо `?disabled=${...}`;
|
|
44
|
+
- динамические списки, отрендеренные через unkeyed `.map()` вместо `each()`;
|
|
45
|
+
- ESM-импорты в браузере без `.js`;
|
|
46
|
+
- `resource()`, созданный вне component setup;
|
|
47
|
+
- новые runtime-зависимости или новые публичные API фреймворка.
|
|
47
48
|
|
|
48
|
-
##
|
|
49
|
+
## Заметки по результатам
|
|
49
50
|
|
|
50
|
-
|
|
51
|
-
runtime
|
|
51
|
+
Текущая реализация `examples/tickets` не потребовала новых публичных API или
|
|
52
|
+
runtime-зависимостей.
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
54
|
+
Основная болевая точка в документации остается lifecycle: старые примеры могут
|
|
55
|
+
создать впечатление, что создание `resource()` прямо в `page.view()` допустимо.
|
|
56
|
+
Пример tickets использует page-level wrapper-компоненты вместо этого, поэтому
|
|
57
|
+
ресурсы регистрируются внутри component setup и очищаются вместе с компонентом.
|
|
@@ -1,10 +1,9 @@
|
|
|
1
1
|
# Shadow DOM vs Light DOM
|
|
2
2
|
|
|
3
|
-
Mado
|
|
4
|
-
|
|
5
|
-
an application.
|
|
3
|
+
Компоненты Mado по умолчанию используют Shadow DOM. Это хороший дефолт для
|
|
4
|
+
самодостаточных виджетов, но не для каждого компонента в приложении.
|
|
6
5
|
|
|
7
|
-
##
|
|
6
|
+
## Правило большого пальца
|
|
8
7
|
|
|
9
8
|
В Mado layout — это тоже component. Если файл описывает видимую переиспользуемую
|
|
10
9
|
часть UI-дерева — app shell, sidebar, modal, table, page section — по умолчанию
|
|
@@ -17,25 +16,25 @@ const money = (value: number) => html`<span>${formatMoney(value)}</span>`;
|
|
|
17
16
|
```
|
|
18
17
|
|
|
19
18
|
Не стоит делать app shell функцией в публичных примерах. Это работает, но
|
|
20
|
-
прячет browser model вместо того, чтобы
|
|
19
|
+
прячет browser model вместо того, чтобы её объяснять.
|
|
21
20
|
|
|
22
|
-
|
|
21
|
+
Используйте **Shadow DOM** для leaf-виджетов:
|
|
23
22
|
|
|
24
|
-
-
|
|
25
|
-
-
|
|
26
|
-
- embed
|
|
27
|
-
-
|
|
23
|
+
- кнопки, бейджи, карточки, метрики;
|
|
24
|
+
- модалы, тосты, маленькие визуальные компоненты;
|
|
25
|
+
- embed-виджеты, которые не должны наследовать CSS приложения случайно;
|
|
26
|
+
- компоненты, стили которых принадлежат самому компоненту.
|
|
28
27
|
|
|
29
|
-
|
|
30
|
-
|
|
28
|
+
Используйте **Light DOM** (`{ shadow: false }`) для структуры приложения,
|
|
29
|
+
которая хочет разделять глобальные CSS-утилиты:
|
|
31
30
|
|
|
32
|
-
- route/page
|
|
33
|
-
- admin
|
|
34
|
-
- data-heavy
|
|
35
|
-
-
|
|
36
|
-
-
|
|
31
|
+
- route/page компоненты;
|
|
32
|
+
- admin-экраны с плотными таблицами/формами;
|
|
33
|
+
- data-heavy экраны с таблицами и формами;
|
|
34
|
+
- компоненты, которые намеренно используют глобальные layout/form/table утилиты;
|
|
35
|
+
- места, где children должны оставаться обычным document DOM.
|
|
37
36
|
|
|
38
|
-
|
|
37
|
+
Используйте **Shadow DOM** для slot-based layouts:
|
|
39
38
|
|
|
40
39
|
- app shells с `<slot>`;
|
|
41
40
|
- sidebar/content wrappers;
|
|
@@ -44,73 +43,89 @@ Use **Shadow DOM** для slot-based layouts:
|
|
|
44
43
|
`<slot>` — это feature Shadow DOM. В компоненте с `shadow: false` тег `<slot>`
|
|
45
44
|
становится обычным DOM-элементом и не переносит children в это место layout.
|
|
46
45
|
|
|
47
|
-
##
|
|
46
|
+
## Подвох
|
|
48
47
|
|
|
49
|
-
|
|
48
|
+
Глобальный CSS не пересекает границу Shadow DOM.
|
|
50
49
|
|
|
51
50
|
```ts
|
|
52
51
|
// global.ts
|
|
53
52
|
export const globalStyles = css`
|
|
54
|
-
.page-head {
|
|
55
|
-
|
|
53
|
+
.page-head {
|
|
54
|
+
display: flex;
|
|
55
|
+
justify-content: space-between;
|
|
56
|
+
}
|
|
57
|
+
.metric-grid {
|
|
58
|
+
display: grid;
|
|
59
|
+
grid-template-columns: repeat(4, 1fr);
|
|
60
|
+
}
|
|
56
61
|
`;
|
|
57
62
|
|
|
58
|
-
// ❌ .page-head
|
|
59
|
-
component(
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
+
// ❌ .page-head и .metric-grid НЕ применятся внутри shadowRoot x-dashboard
|
|
64
|
+
component(
|
|
65
|
+
"x-dashboard",
|
|
66
|
+
() => () => html`
|
|
67
|
+
<header class="page-head">...</header>
|
|
68
|
+
<div class="metric-grid">...</div>
|
|
69
|
+
`,
|
|
70
|
+
);
|
|
63
71
|
```
|
|
64
72
|
|
|
65
|
-
|
|
73
|
+
Решение — сделать route/page компонент Light DOM:
|
|
66
74
|
|
|
67
75
|
```ts
|
|
68
|
-
component(
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
styles: css`
|
|
74
|
-
x-dashboard { display: block; }
|
|
75
|
-
x-dashboard .panel { padding: 1rem; }
|
|
76
|
+
component(
|
|
77
|
+
"x-dashboard",
|
|
78
|
+
() => () => html`
|
|
79
|
+
<header class="page-head">...</header>
|
|
80
|
+
<div class="metric-grid">...</div>
|
|
76
81
|
`,
|
|
77
|
-
|
|
82
|
+
{
|
|
83
|
+
shadow: false,
|
|
84
|
+
styles: css`
|
|
85
|
+
x-dashboard {
|
|
86
|
+
display: block;
|
|
87
|
+
}
|
|
88
|
+
x-dashboard .panel {
|
|
89
|
+
padding: 1rem;
|
|
90
|
+
}
|
|
91
|
+
`,
|
|
92
|
+
},
|
|
93
|
+
);
|
|
78
94
|
```
|
|
79
95
|
|
|
80
|
-
|
|
96
|
+
Теперь глобальные утилиты и локальные scoped-стили работают вместе.
|
|
81
97
|
|
|
82
|
-
##
|
|
98
|
+
## Как ведут себя стили
|
|
83
99
|
|
|
84
|
-
- `styles: css\`\``
|
|
85
|
-
- `styles: css
|
|
86
|
-
|
|
87
|
-
- CSS custom properties (`--accent`, `--bg
|
|
88
|
-
-
|
|
89
|
-
Shadow DOM
|
|
90
|
-
- Slotted
|
|
91
|
-
|
|
92
|
-
- `<slot>`
|
|
93
|
-
|
|
94
|
-
place in your layout.
|
|
100
|
+
- `styles: css\`\`` в Shadow DOM адоптируется в shadowRoot компонента.
|
|
101
|
+
- `styles: css\`\``с`shadow: false` скоупится по имени тега и адоптируется
|
|
102
|
+
глобально.
|
|
103
|
+
- CSS custom properties (`--accent`, `--bg` и т.д.) пересекают границу Shadow DOM.
|
|
104
|
+
- Селекторы по классу (`.btn`, `.form-grid`, `.page-head`) **не** пересекают
|
|
105
|
+
границу Shadow DOM.
|
|
106
|
+
- Slotted-children сохраняют свои стили из документа; shadow-компонент может
|
|
107
|
+
таргетировать их только через `::slotted(...)`.
|
|
108
|
+
- `<slot>` проецирует children только в Shadow DOM. В компоненте с `shadow: false`
|
|
109
|
+
это обычный `<slot>` элемент, который не перемещает children в своё место.
|
|
95
110
|
|
|
96
|
-
##
|
|
111
|
+
## Рекомендованная архитектура
|
|
97
112
|
|
|
98
113
|
```ts
|
|
99
|
-
// root
|
|
114
|
+
// root и pages: Light DOM
|
|
100
115
|
component("x-app", setup, { shadow: false });
|
|
101
116
|
component("x-users-page", setup, { shadow: false });
|
|
102
117
|
|
|
103
|
-
// slot-based layout: Shadow DOM
|
|
118
|
+
// slot-based layout: Shadow DOM по умолчанию, потому что владеет shell grid
|
|
104
119
|
component("x-app-layout", setup);
|
|
105
120
|
|
|
106
|
-
// leaf widgets: Shadow DOM
|
|
121
|
+
// leaf widgets: Shadow DOM по умолчанию
|
|
107
122
|
component("x-status-badge", setup);
|
|
108
123
|
component("x-stat-card", setup);
|
|
109
124
|
component("x-toast-stack", setup);
|
|
110
125
|
```
|
|
111
126
|
|
|
112
|
-
|
|
113
|
-
|
|
127
|
+
Это даёт backend-admin экранам предсказуемый CSS, сохраняя инкапсуляцию
|
|
128
|
+
для переиспользуемых виджетов и slot-based shells.
|
|
114
129
|
|
|
115
130
|
Import model специально browser-native:
|
|
116
131
|
|
|
@@ -121,30 +136,31 @@ render(html`<x-app-layout>${router.view}</x-app-layout>`, app);
|
|
|
121
136
|
```
|
|
122
137
|
|
|
123
138
|
Import регистрирует custom element через `customElements.define()`. Template
|
|
124
|
-
|
|
125
|
-
компонента. Тут нет React-style component value, который
|
|
139
|
+
создаёт `<x-app-layout>` элемент. Дальше браузер сам связывает тег с классом
|
|
140
|
+
компонента. Тут нет React-style component value, который передаётся как функция.
|
|
126
141
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
142
|
+
Если layout не нуждается в slot projection и должен стилизоваться полностью
|
|
143
|
+
глобальным CSS, `shadow: false` — хороший выбор. Если он содержит `<slot>`,
|
|
144
|
+
оставьте Shadow DOM и поместите стили shell в этот компонент.
|
|
130
145
|
|
|
131
|
-
##
|
|
146
|
+
## Маршрутизация и ссылки
|
|
132
147
|
|
|
133
|
-
`data-link`
|
|
134
|
-
|
|
148
|
+
`data-link` работает внутри Shadow DOM. Роутер использует `event.composedPath()`,
|
|
149
|
+
поэтому перехват кликов и hover-prefetch видят ссылки из open shadow roots.
|
|
135
150
|
|
|
136
151
|
```ts
|
|
137
|
-
component(
|
|
138
|
-
|
|
139
|
-
`
|
|
152
|
+
component(
|
|
153
|
+
"x-card-link",
|
|
154
|
+
() => () => html` <a href="/app/accounts" data-link>Accounts</a> `,
|
|
155
|
+
);
|
|
140
156
|
```
|
|
141
157
|
|
|
142
|
-
|
|
158
|
+
Ссылка может быть внутри Shadow DOM — навигация всё равно остаётся SPA.
|
|
143
159
|
|
|
144
160
|
## Где импортировать компоненты
|
|
145
161
|
|
|
146
|
-
Custom elements становятся глобальными после регистрации, но регистрация
|
|
147
|
-
равно
|
|
162
|
+
Custom elements становятся глобальными после регистрации, но регистрация всё
|
|
163
|
+
равно остаётся явным JavaScript import.
|
|
148
164
|
|
|
149
165
|
```ts
|
|
150
166
|
// main.ts: global app frame
|
|
@@ -158,7 +174,7 @@ import "../components/ticket-list.js";
|
|
|
158
174
|
`<ticket-list>`. Файл должен быть где-то импортирован. После import он вызывает
|
|
159
175
|
`customElements.define(...)`, и тег становится известен текущему document.
|
|
160
176
|
|
|
161
|
-
Не стоит bulk-import всех компонентов в `main.ts`
|
|
177
|
+
Не стоит bulk-import всех компонентов в `main.ts` «на всякий случай». Для маленьких
|
|
162
178
|
demo это работает, но прячет ownership и ломает lazy route loading. Лучше:
|
|
163
179
|
|
|
164
180
|
- global app shell/providers импортировать в `main.ts`;
|
|
@@ -167,16 +183,16 @@ demo это работает, но прячет ownership и ломает lazy r
|
|
|
167
183
|
- truly global leaf components импортировать в `main.ts` только если они реально
|
|
168
184
|
используются везде.
|
|
169
185
|
|
|
170
|
-
##
|
|
186
|
+
## Урок из showcase
|
|
171
187
|
|
|
172
|
-
`examples/showcase`
|
|
188
|
+
`examples/showcase` использует это разделение намеренно:
|
|
173
189
|
|
|
174
|
-
- `x-app`
|
|
175
|
-
- `x-app-layout`
|
|
176
|
-
shell;
|
|
177
|
-
- table/form/page
|
|
178
|
-
- leaf
|
|
179
|
-
`x-toast-stack`
|
|
190
|
+
- `x-app` и CRM route pages — Light DOM;
|
|
191
|
+
- `x-app-layout` остаётся в Shadow DOM, потому что владеет slot-based
|
|
192
|
+
sidebar/content shell;
|
|
193
|
+
- table/form/page утилиты живут в `styles/global.ts`;
|
|
194
|
+
- leaf-компоненты типа `x-stat-card`, `x-status-badge`, `x-modal` и
|
|
195
|
+
`x-toast-stack` остаются в Shadow DOM.
|
|
180
196
|
|
|
181
|
-
|
|
182
|
-
Shadow DOM
|
|
197
|
+
Если страница внезапно выглядит без стилей, проверьте — не используете ли вы
|
|
198
|
+
глобальные классы внутри Shadow DOM компонента. Обычно проблема именно в этом.
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
# Shadow DOM + формы
|
|
2
|
+
|
|
3
|
+
Использование `useForm()` с кастомными input-компонентами в Shadow DOM требует
|
|
4
|
+
знания двух поведений на уровне браузера:
|
|
5
|
+
|
|
6
|
+
1. **Ретаргетинг событий** — события, всплывающие из Shadow DOM, получают
|
|
7
|
+
`e.target`, перенаправленный на host-элемент. `useForm().onInput` читает
|
|
8
|
+
`e.target.name` и `e.target.value`, но host-элемент `<x-input>`
|
|
9
|
+
не имеет этих свойств нативно.
|
|
10
|
+
|
|
11
|
+
2. **Ассоциация с формой** — `<button type="submit">` внутри Shadow Root
|
|
12
|
+
НЕ участвует в алгоритме form-owner для `<form>` в Light DOM. Клик по ней
|
|
13
|
+
не триггерит submit формы.
|
|
14
|
+
|
|
15
|
+
Оба ограничения — на уровне спецификации, не баги Mado. Но фреймворк предоставляет
|
|
16
|
+
паттерны, которые делают их безболезненными.
|
|
17
|
+
|
|
18
|
+
## Паттерн: Proxy-свойства на input-компонентах
|
|
19
|
+
|
|
20
|
+
Оборачивая `<input>` в Shadow DOM компонент, экспонируйте `name` и `value`
|
|
21
|
+
как DOM-свойства на host, чтобы `useForm().onInput` работал после ретаргетинга:
|
|
22
|
+
|
|
23
|
+
```ts
|
|
24
|
+
import { component, css, html } from "@madojs/mado";
|
|
25
|
+
|
|
26
|
+
component("x-input", ({ host, attr }) => {
|
|
27
|
+
const name = attr("name", "");
|
|
28
|
+
const type = attr("type", "text");
|
|
29
|
+
const value = attr("value", "");
|
|
30
|
+
|
|
31
|
+
// Proxy-свойства для совместимости с useForm().
|
|
32
|
+
// После ретаргетинга Shadow DOM e.target из <input> → <x-input>,
|
|
33
|
+
// useForm читает e.target.name / e.target.value — эти геттеры наводят мост.
|
|
34
|
+
Object.defineProperty(host, "name", {
|
|
35
|
+
get: () => host.getAttribute("name") ?? "",
|
|
36
|
+
configurable: true,
|
|
37
|
+
});
|
|
38
|
+
Object.defineProperty(host, "value", {
|
|
39
|
+
get: () => host.shadowRoot?.querySelector("input")?.value ?? "",
|
|
40
|
+
set: (v: string) => {
|
|
41
|
+
const input = host.shadowRoot?.querySelector("input");
|
|
42
|
+
if (input) input.value = v;
|
|
43
|
+
},
|
|
44
|
+
configurable: true,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
return () => html`<input name=${name} type=${type} .value=${value} />`;
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Событие `input` от внутреннего `<input>` имеет `composed: true` по умолчанию,
|
|
52
|
+
поэтому оно всплывает через границу shadow. После ретаргетинга `e.target` —
|
|
53
|
+
это `<x-input>`, но теперь у него есть геттеры `.name` и `.value` → `useForm`
|
|
54
|
+
работает.
|
|
55
|
+
|
|
56
|
+
## Паттерн: Submit формы из Shadow DOM кнопок
|
|
57
|
+
|
|
58
|
+
`<button type="submit">` внутри Shadow DOM не может триггерить submit `<form>`
|
|
59
|
+
в Light DOM. Мост через `requestSubmit()`:
|
|
60
|
+
|
|
61
|
+
```ts
|
|
62
|
+
import { component, css, html } from "@madojs/mado";
|
|
63
|
+
|
|
64
|
+
component("x-button", ({ host, attr }) => {
|
|
65
|
+
const disabled = attr("disabled");
|
|
66
|
+
|
|
67
|
+
const handleClick = () => {
|
|
68
|
+
const typeAttr = host.getAttribute("type");
|
|
69
|
+
if (typeAttr === "button" || typeAttr === "reset") return;
|
|
70
|
+
const form = host.closest("form");
|
|
71
|
+
if (form && !host.hasAttribute("disabled")) form.requestSubmit();
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
return () => html`
|
|
75
|
+
<button ?disabled=${() => disabled() !== ""} @click=${handleClick}>
|
|
76
|
+
<slot></slot>
|
|
77
|
+
</button>
|
|
78
|
+
`;
|
|
79
|
+
});
|
|
80
|
+
```
|
|
81
|
+
|
|
82
|
+
`host.closest("form")` работает, потому что сам host-элемент живёт в Light DOM
|
|
83
|
+
(только его внутренности в тени). `requestSubmit()` триггерит валидацию и
|
|
84
|
+
событие `submit` точно так, как если бы пользователь кликнул нативную submit-кнопку
|
|
85
|
+
внутри формы.
|
|
86
|
+
|
|
87
|
+
## Паттерн: Реактивные атрибуты через ctx.attr()
|
|
88
|
+
|
|
89
|
+
С версии 0.7 `ctx.attr(name, defaultValue?)` возвращает `Signal<string>`,
|
|
90
|
+
который автоматически обновляется при изменении атрибута на host. Никакого
|
|
91
|
+
`MutationObserver` бойлерплейта:
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
component("x-badge", ({ attr }) => {
|
|
95
|
+
const variant = attr("variant", "default"); // Signal<string>
|
|
96
|
+
|
|
97
|
+
return () =>
|
|
98
|
+
html`<span class=${() => `badge badge-${variant()}`}>
|
|
99
|
+
<slot></slot>
|
|
100
|
+
</span>`;
|
|
101
|
+
});
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
Родитель может использовать `?disabled=${() => !form.isValid()}` (boolean атрибут)
|
|
105
|
+
или `.variant=${"danger"}` — компонент перерендеривается реактивно в любом случае.
|
|
106
|
+
|
|
107
|
+
## Полный пример формы
|
|
108
|
+
|
|
109
|
+
```ts
|
|
110
|
+
import { page, html, useForm, navigate } from "@madojs/mado";
|
|
111
|
+
import "../components/x-input.js";
|
|
112
|
+
import "../components/x-button.js";
|
|
113
|
+
|
|
114
|
+
export default page({
|
|
115
|
+
title: "Вход",
|
|
116
|
+
view: () => {
|
|
117
|
+
const form = useForm({
|
|
118
|
+
email: { required: true, type: "email" },
|
|
119
|
+
password: { required: true, minLength: 6 },
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
const handleLogin = async (values) => {
|
|
123
|
+
await api("/auth/login", { method: "POST", json: values });
|
|
124
|
+
navigate("/admin");
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
return html`
|
|
128
|
+
<form @submit=${form.onSubmit(handleLogin)}>
|
|
129
|
+
<x-input
|
|
130
|
+
name="email"
|
|
131
|
+
type="email"
|
|
132
|
+
label="Email"
|
|
133
|
+
required
|
|
134
|
+
@input=${form.onInput}
|
|
135
|
+
@blur=${form.onBlur}
|
|
136
|
+
></x-input>
|
|
137
|
+
${() =>
|
|
138
|
+
form.errors().email
|
|
139
|
+
? html`<small class="err">${form.errors().email}</small>`
|
|
140
|
+
: null}
|
|
141
|
+
|
|
142
|
+
<x-input
|
|
143
|
+
name="password"
|
|
144
|
+
type="password"
|
|
145
|
+
label="Пароль"
|
|
146
|
+
required
|
|
147
|
+
@input=${form.onInput}
|
|
148
|
+
@blur=${form.onBlur}
|
|
149
|
+
></x-input>
|
|
150
|
+
|
|
151
|
+
<x-button type="submit" ?disabled=${() => !form.isValid()}>
|
|
152
|
+
Войти
|
|
153
|
+
</x-button>
|
|
154
|
+
</form>
|
|
155
|
+
`;
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
## Когда использовать Light DOM
|
|
161
|
+
|
|
162
|
+
Если ваш input-компонент — это просто стилизованная обёртка без нужды в
|
|
163
|
+
инкапсуляции, `shadow: false` избегает обеих проблем (ретаргетинг и
|
|
164
|
+
form-association):
|
|
165
|
+
|
|
166
|
+
```ts
|
|
167
|
+
component(
|
|
168
|
+
"x-field",
|
|
169
|
+
({ attr }) => {
|
|
170
|
+
const label = attr("label", "");
|
|
171
|
+
return () => html`
|
|
172
|
+
<label>
|
|
173
|
+
<span>${label}</span>
|
|
174
|
+
<slot></slot>
|
|
175
|
+
</label>
|
|
176
|
+
`;
|
|
177
|
+
},
|
|
178
|
+
{ shadow: false },
|
|
179
|
+
);
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
С Light DOM нативный `<input>` — часть дерева документа, события не
|
|
183
|
+
ретаргетируются, и submit формы работает нативно. Компромисс: стили не
|
|
184
|
+
инкапсулированы (нужно скоупить самостоятельно).
|
|
185
|
+
|
|
186
|
+
## Итого
|
|
187
|
+
|
|
188
|
+
| Задача | Решение Shadow DOM | Альтернатива Light DOM |
|
|
189
|
+
| --------------------------- | -------------------------------------- | --------------------------- |
|
|
190
|
+
| `useForm` + кастомный input | Proxy `name`/`value` на host | Нативный `<input>` в slot |
|
|
191
|
+
| Submit формы | `form.requestSubmit()` в click handler | Нативная кнопка работает |
|
|
192
|
+
| Реактивные атрибуты | `ctx.attr()` → авто-сигнал | `ctx.attr()` работает везде |
|
|
193
|
+
| Инкапсуляция стилей | Да (автоматически) | Ручной `@scope` или BEM |
|
package/docs/ru/README.md
CHANGED
|
@@ -1,21 +1,22 @@
|
|
|
1
1
|
# Mado docs — Русский
|
|
2
2
|
|
|
3
|
-
| Раздел
|
|
4
|
-
|
|
5
|
-
| Принципы
|
|
6
|
-
|
|
|
7
|
-
|
|
|
8
|
-
|
|
|
9
|
-
| IDE
|
|
10
|
-
|
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
| LLM
|
|
14
|
-
| Shadow DOM vs Light DOM
|
|
15
|
-
| Архитектура приложения
|
|
16
|
-
|
|
|
17
|
-
|
|
|
18
|
-
|
|
|
19
|
-
| Тестирование
|
|
20
|
-
| Обработка ошибок
|
|
21
|
-
|
|
|
3
|
+
| Раздел | Файл |
|
|
4
|
+
| --------------------------- | ------------------------------------------------------------ |
|
|
5
|
+
| Принципы | [00-the-mado-way.md](./00-the-mado-way.md) |
|
|
6
|
+
| Маршрутизация | [01-routing.md](./01-routing.md) |
|
|
7
|
+
| Структура проекта | [02-project-layout.md](./02-project-layout.md) |
|
|
8
|
+
| Статический bake и SEO | [03-static-bake.md](./03-static-bake.md) |
|
|
9
|
+
| Настройка IDE | [04-ide-setup.md](./04-ide-setup.md) |
|
|
10
|
+
| Почему Mado | [05-why-mado.md](./05-why-mado.md) |
|
|
11
|
+
| Для бекендеров | [06-for-backenders.md](./06-for-backenders.md) |
|
|
12
|
+
| Типичные ошибки LLM | [07-llm-pitfalls.md](./07-llm-pitfalls.md) |
|
|
13
|
+
| Тест LLM с нулевой историей | [08-llm-zero-history-test.md](./08-llm-zero-history-test.md) |
|
|
14
|
+
| Shadow DOM vs Light DOM | [09-shadow-vs-light-dom.md](./09-shadow-vs-light-dom.md) |
|
|
15
|
+
| Архитектура приложения | [10-app-architecture.md](./10-app-architecture.md) |
|
|
16
|
+
| Макеты (layouts) | [11-layouts.md](./11-layouts.md) |
|
|
17
|
+
| Авторизация и API | [12-auth-and-api.md](./12-auth-and-api.md) |
|
|
18
|
+
| Развёртывание | [13-deployment.md](./13-deployment.md) |
|
|
19
|
+
| Тестирование | [14-testing.md](./14-testing.md) |
|
|
20
|
+
| Обработка ошибок | [15-error-handling.md](./15-error-handling.md) |
|
|
21
|
+
| Рецепты bake | [16-bake-cookbook.md](./16-bake-cookbook.md) |
|
|
22
|
+
| Shadow DOM + формы | [17-shadow-dom-forms.md](./17-shadow-dom-forms.md) |
|