@nativele/ts-dom 0.4.6 → 0.5.1

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.
@@ -0,0 +1,279 @@
1
+ ---
2
+ name: nativele-vue
3
+ description: "Use Nativele web components inside a Vue 3 app: telling the template compiler which tags are custom elements, passing props, listening to events, replacing v-model, children and slots, lifecycle with v-for, v-if and KeepAlive, and SFC template types. Use when a project depends on vue and @nativele/ts-dom."
4
+ metadata:
5
+ tier: official
6
+ nativele: ">=0.5.1 <0.6.0"
7
+ verified: "2026-11-05"
8
+ tools: "vue@3.5.44 vite@8.3.6 @vitejs/plugin-vue@6.0.10 vue-tsc@3.3.14 typescript@6.0.3 @vue/tsconfig@0.9.1"
9
+ author: "Nativele maintainers"
10
+ detects: "vue"
11
+ ---
12
+
13
+ # Nativele + Vue
14
+
15
+ ## When to use
16
+
17
+ - The project renders with Vue 3 single-file components and wants to use components written with Nativele (`@nativele/ts-dom`), for example a design-system layer or widgets shared with non-Vue pages.
18
+ - Signals: `vue` and `@nativele/ts-dom` in `package.json`, `.vue` templates that render Nativele tags (`<nv-info-card>`).
19
+ - The starting point is a working Vue app, such as the one `npm create vite@latest -- --template vue-ts` makes. This Skill does not set Vue or Vite up.
20
+ - Not for Vue 2, and not for Vue's own `defineCustomElement` (that turns Vue components into custom elements; this Skill goes the other way). Not for writing the Nativele components themselves: use `nativele-core` (`npx ts-dom skills add nativele-core`).
21
+
22
+ Nativele components are native custom elements. Vue renders them like any other element; nothing wraps them.
23
+
24
+ ## Versions
25
+
26
+ | Tool | Verified version |
27
+ |---|---|
28
+ | @nativele/ts-dom | 0.5.1 |
29
+ | vue | 3.5.44 |
30
+ | vite | 8.3.6 |
31
+ | @vitejs/plugin-vue | 6.0.10 |
32
+ | vue-tsc | 3.3.14 |
33
+ | typescript | 6.0.3 |
34
+ | @vue/tsconfig | 0.9.1 |
35
+
36
+ Verified on 2026-11-05 in Google Chrome 148, starting from `create-vite` 9.3.4 with the `vue-ts` template, with the example project in the Nativele repository (`skills/nativele-vue/example`), which CI runs again when the example or the library changes and once a month. With a different major version of Vue, treat every step as unverified.
37
+
38
+ ## Set-up
39
+
40
+ 1. Keep the Nativele components in their own folder, for example `src/nv/`, each as `x.html` plus `x.ts`, written with the core Skill (`npx ts-dom skills add nativele-core`). Give every Nativele tag one prefix that no Vue component uses, for example `nv-` (`define("nv-info-card", InfoCard)`); step 4 depends on it.
41
+ 2. Install: `npm i @nativele/ts-dom`.
42
+ 3. Keep `"strict": true` for that folder. In the `vue-ts` template, `tsconfig.app.json` extends `@vue/tsconfig/tsconfig.dom.json`, which already sets it; if the project turned it off, turn it on there. Nativele's type checks depend on it.
43
+ 4. Add the Nativele Vite plugin, and tell Vue's template compiler that the tags with the prefix are custom elements:
44
+
45
+ ```ts
46
+ // vite.config.ts
47
+ import vue from "@vitejs/plugin-vue";
48
+ import { defineConfig } from "vite";
49
+ import { tsDom } from "@nativele/ts-dom/vite";
50
+
51
+ export default defineConfig({
52
+ plugins: [tsDom({ dirs: ["src/nv"] }), vue({ template: { compilerOptions: { isCustomElement: (tag) => tag.startsWith("nv-") } } })],
53
+ });
54
+ ```
55
+
56
+ Match the prefix only, never every hyphenated tag (limitation 1). Pass the same folder to every `ts-dom` command (`npx ts-dom check src/nv`), so there is one `ts-dom-tags.ts`, in `src/nv`.
57
+
58
+ 5. Add `*.html.ts` and `ts-dom-tags.ts` to `.gitignore`: `ts-dom` writes them, and they are not committed.
59
+ 6. Import every Nativele component module in `src/main.ts`, before `createApp(...).mount(...)`:
60
+
61
+ ```ts
62
+ // src/main.ts
63
+ import "./nv/info-card.js";
64
+ import "./nv/text-field.js";
65
+ import { createApp } from "vue";
66
+ import App from "./App.vue";
67
+
68
+ createApp(App).mount("#app");
69
+ ```
70
+
71
+ Vue sets a binding as a property only when the element already has that property, which is true only after `define()` has run (limitation 2).
72
+
73
+ 7. Add the template type declaration below as `src/nativele-vue.d.ts` (the template's `tsconfig.app.json` already includes `src/**/*.ts`). It reads the tag map that `ts-dom gen` writes (`ts-dom-tags.ts`), so every exported component gets typed props and events in SFC templates, and new components need no edit:
74
+
75
+ ```ts
76
+ // src/nativele-vue.d.ts
77
+ import type { HTMLAttributes } from "vue";
78
+
79
+ type NativeleTag = Extract<keyof HTMLElementTagNameMap, `${string}-${string}`>;
80
+ type Camelize<S extends string> = S extends `${infer A}-${infer B}` ? `${A}${Capitalize<Camelize<B>>}` : S;
81
+ type NativeleProps<E> = E extends { readonly props: infer P } ? { [K in keyof P & keyof E]?: E[K] } : unknown;
82
+ type NativeleEvents<E> = E extends { readonly "~emits"?: infer M }
83
+ ? { [K in keyof NonNullable<M> & string as `on${Capitalize<Camelize<K>>}`]?: (event: CustomEvent<NonNullable<M>[K]>) => void }
84
+ : unknown;
85
+ type NativeleComponents = {
86
+ [K in NativeleTag]: new () => {
87
+ $props: HTMLAttributes & NativeleProps<HTMLElementTagNameMap[K]> & NativeleEvents<HTMLElementTagNameMap[K]>;
88
+ };
89
+ };
90
+
91
+ declare module "vue" {
92
+ interface GlobalComponents extends NativeleComponents {}
93
+ }
94
+ ```
95
+
96
+ The component class must be exported and passed to `define()` in a folder that `ts-dom gen` scans, so it appears in `ts-dom-tags.ts`. A template ref is typed from the same map: `useTemplateRef<HTMLElementTagNameMap["nv-info-card"]>("card")`.
97
+
98
+ The generated files are not committed, so on a clean checkout the type check in `npm run build` runs before anything has written them and fails with `TS2307: Cannot find module './x.html.js'`. Generate them first: `"build": "ts-dom gen src/nv && vue-tsc -b && vite build"` (the template's script with `ts-dom gen src/nv &&` in front).
99
+
100
+ ## Example
101
+
102
+ A Nativele component with props and an event:
103
+
104
+ ```html
105
+ <!-- src/nv/info-card.html -->
106
+ <span>{{ label }}</span>
107
+ <b>{{ count }}</b>
108
+ <i data-ref="itemName"></i>
109
+ <button type="button" data-on-click="pick">pick</button>
110
+ ```
111
+
112
+ ```ts
113
+ // src/nv/info-card.ts
114
+ import { Component, define, event } from "@nativele/ts-dom";
115
+ import tpl from "./info-card.html.js";
116
+
117
+ export interface Item {
118
+ id: number;
119
+ name: string;
120
+ }
121
+
122
+ export class InfoCard extends Component(tpl, {
123
+ props: { label: "", count: 0, item: null as Item | null },
124
+ emits: { "item-select": event<{ id: number }>() },
125
+ }) {
126
+ pick(): void {
127
+ const item = this.props.item;
128
+ if (item) this.emit("item-select", { id: item.id });
129
+ }
130
+ override render(view: { item: Item | null }): void {
131
+ this.refs.itemName.textContent = view.item ? view.item.name : "-";
132
+ }
133
+ }
134
+ define("nv-info-card", InfoCard);
135
+ ```
136
+
137
+ A field that takes the place of `v-model`: a `value` prop down, a `value-change` event up.
138
+
139
+ ```html
140
+ <!-- src/nv/text-field.html -->
141
+ <input data-ref="input" data-on-input="onInput" />
142
+ ```
143
+
144
+ ```ts
145
+ // src/nv/text-field.ts
146
+ import { Component, define, event } from "@nativele/ts-dom";
147
+ import tpl from "./text-field.html.js";
148
+
149
+ export class TextField extends Component(tpl, {
150
+ props: { value: "" },
151
+ emits: { "value-change": event<{ value: string }>() },
152
+ }) {
153
+ onInput(): void {
154
+ this.emit("value-change", { value: this.refs.input.value });
155
+ }
156
+ override render(view: { value: string }): void {
157
+ if (this.refs.input.value !== view.value) this.refs.input.value = view.value;
158
+ }
159
+ }
160
+ define("nv-text-field", TextField);
161
+ ```
162
+
163
+ Used from a Vue component:
164
+
165
+ ```vue
166
+ <script setup lang="ts">
167
+ import { ref } from "vue";
168
+ import type { Item } from "./nv/info-card.js";
169
+
170
+ const count = ref(0);
171
+ const item = ref<Item>({ id: 1, name: "apple" });
172
+ const picked = ref<number[]>([]);
173
+ const text = ref("");
174
+ </script>
175
+
176
+ <template>
177
+ <nv-info-card label="Fruit" :count="count" :item="item" @item-select="(e) => picked.push(e.detail.id)" />
178
+ <nv-text-field :value="text" @value-change="(e) => (text = e.detail.value)" />
179
+ </template>
180
+ ```
181
+
182
+ - `label`, `count` and `item` arrive as properties; the object is the same object Vue passed, and the element has no `item` attribute. Changing them from Vue state re-renders the Nativele component.
183
+ - `@item-select` uses the exact event name; `e.detail` is typed from the component's `emits`.
184
+
185
+ Children and slots: a `shadow: true` component takes Vue children directly; a Light DOM component (the default) takes one stable element per slot, with Vue changing only what is inside it.
186
+
187
+ ```html
188
+ <!-- src/nv/shadow-card.html (and the same in src/nv/light-card.html) -->
189
+ <h2><slot name="title">Untitled</slot></h2>
190
+ <div><slot></slot></div>
191
+ ```
192
+
193
+ ```ts
194
+ // src/nv/shadow-card.ts
195
+ export class ShadowCard extends Component(tpl, { shadow: true }) {}
196
+ define("nv-shadow-card", ShadowCard);
197
+
198
+ // src/nv/light-card.ts
199
+ export class LightCard extends Component(tpl) {}
200
+ define("nv-light-card", LightCard);
201
+ ```
202
+
203
+ ```vue
204
+ <nv-shadow-card>
205
+ <span v-if="showTitle" slot="title">{{ title }}</span>
206
+ <p v-for="r in rows" :key="r.id">{{ r.text }}</p>
207
+ </nv-shadow-card>
208
+
209
+ <nv-light-card>
210
+ <div slot="title"><span v-if="showTitle">{{ title }}</span></div>
211
+ <div><p v-for="r in rows" :key="r.id">{{ r.text }}</p></div>
212
+ </nv-light-card>
213
+ ```
214
+
215
+ ## Known limitations
216
+
217
+ 1. **`isCustomElement` must match only the Nativele tags.** With `(tag) => tag.includes("-")`, every kebab-case Vue component (an SFC used as `<hello-box>`, `<router-view>`) stops resolving: it renders as an empty unknown element, with no warning. Use the prefix from step 1. Without `isCustomElement` at all, Nativele tags still render and work, but Vue logs `[Vue warn]: Failed to resolve component: nv-info-card` for each tag in development.
218
+ 2. **Define before mount.** Import every Nativele module before `mount()` (step 6). A `:item` binding on an element that is not defined yet becomes the attribute `item="[object Object]"`, and the component gets `null` after `define()` runs. When a module really is loaded later (a dynamic `import()`), bind object props with the property modifier, `.item="item"` (or `:item.prop="item"`): the property is kept and the component takes it over when it is defined.
219
+ 3. **camelCase prop names in templates.** Write Nativele props as they are declared: `:selectedItem="item"`, `:maxCount="3"`. Vue sets a kebab-case binding (`:selected-item`) as an attribute, because the element has no property of that name: a number or string still converts from the attribute, an object or array arrives as `"[object Object]"` and the prop stays `null`. `vue-tsc` does not report this (limitation 8).
220
+ 4. **Exact event names.** On a custom element, Vue listens to the event name exactly as written after `@`: `@item-select` hears `item-select`, `@pickedOne` hears `pickedOne`, and `@itemSelect` never hears `item-select`. Name events in kebab-case and write that name. Nativele events do not bubble by default, so the listener goes on the element that emits.
221
+ 5. **`v-model` does not work on a Nativele component.** It compiles, `vue-tsc` accepts it and Vue logs no warning; it writes the first value into the element, but typing never updates the Vue state. Use a prop and an event, as `nv-text-field` above: `:value="text" @value-change="(e) => (text = e.detail.value)"`. In the component, compare before writing the input's value, so the cursor does not jump (see "A Small Reminder About Form Inputs" in the components guide).
222
+ 6. **Light DOM slots and Vue children do not mix.** In the default Light DOM mode, Nativele moves the host's children into its slot positions. When Vue later inserts, removes or reorders those children (`v-if`, `v-for`), the update throws (`Cannot read properties of null (reading 'insertBefore')`, `Cannot set properties of null (setting '__vnode')`) and the slots show the wrong children. Use `shadow: true`, or one stable element per slot as in the example. A stable but empty wrapper still fills its slot, so the slot's fallback content does not show; when the fallback matters, use `shadow: true`.
223
+ 7. **Lifecycle follows the DOM.** When a keyed `v-for` reorders Nativele elements, Vue moves the same element with `insertBefore`: it runs `disconnected()` then `connected()`, keeps its props and its Light DOM slot children, and `bind` subscriptions end and start again. `<KeepAlive>` deactivation takes the element out of the document (`disconnected()`, no store updates while inactive); activation puts the same element back (`connected()`, and `bind` calls back at once with the current value). `v-if` removal runs `disconnected()` and ends the subscriptions. So subscribe in `connected()`, as the lists guide says for moved items.
224
+ 8. **What `vue-tsc` checks.** With the declaration from step 7, it reports a wrong prop type (`count="1"` on a number prop), a wrong object shape and a wrong use of `e.detail`. It does not report an unknown prop name, an unknown tag with the prefix, a kebab-case prop name, `v-model`, or an event name whose case differs from the declared one. In a template, mark an expected error with `<!-- @vue-expect-error -->`; `// @ts-expect-error` is rendered as text and suppresses nothing.
225
+ 9. Not verified: server-side rendering and hydration, Nuxt, `<Transition>` around Nativele elements, and Vue versions other than 3.5.
226
+
227
+ ## Verify
228
+
229
+ Run in the project:
230
+
231
+ ```sh
232
+ npx ts-dom check src/nv # expect: ts-dom check: no issues
233
+ npx ts-dom doctor src/nv # expect: ts-dom doctor: the set-up looks right (info lines are suggestions)
234
+ npx vue-tsc -b # expect: no output
235
+ npx vite build # expect: built
236
+ ```
237
+
238
+ `ts-dom check` runs `vue-tsc` instead of `tsc` when `vue-tsc` is installed, so it also reports type errors in `.vue` files.
239
+
240
+ Prove that the declaration types props: keep a `.vue` file that the app never imports (the example uses `src/TypesCheck.vue`) with one wrong case per kind under `<!-- @vue-expect-error -->`:
241
+
242
+ ```vue
243
+ <template>
244
+ <!-- @vue-expect-error count is a number -->
245
+ <nv-info-card count="1" />
246
+ <!-- @vue-expect-error item has the wrong shape -->
247
+ <nv-info-card :item="{ id: '1' }" />
248
+ <!-- @vue-expect-error detail is { id: number } -->
249
+ <nv-info-card @item-select="(e) => e.detail.name" />
250
+ </template>
251
+ ```
252
+
253
+ `vue-tsc -b` passes only while each of these is an error; if the declaration stops working, it reports `TS2578: Unused '@ts-expect-error' directive`.
254
+
255
+ In the browser, against the dev server (production builds drop Vue warnings), check that an element with an object prop has no attribute for it (`el.getAttributeNames()`) and that `el.item` is the object, that the listener runs once per event, that a kebab-case Vue component still renders, and that there are no errors or `[Vue warn]` lines in the console. The example project's [`verify.mjs`](https://github.com/Pixmi/nativele/blob/main/skills/nativele-vue/example/verify.mjs) does these checks with Playwright, plus the `v-model` replacement, a component defined after mount, children that Vue reorders, adds and removes in both slot modes, and the lifecycle under `v-for`, `<KeepAlive>` and `v-if`. A Light DOM component has no `<slot>` elements once rendered; read a slot with `el.assignedNodes(name)`, where `""` is the default slot, and compare elements only, because Vue leaves comment and empty text nodes among the children.
256
+
257
+ ## Troubleshooting
258
+
259
+ | Symptom | Cause | Fix |
260
+ |---|---|---|
261
+ | `TS2307: Cannot find module './x.html.js'` from `npm run build` on a clean checkout | `vue-tsc -b` runs before the generated files exist | start the script with `ts-dom gen src/nv &&` |
262
+ | `[Vue warn]: Failed to resolve component: nv-info-card` | `isCustomElement` is missing, or does not match the tag | set it as in step 4, with the prefix your tags use |
263
+ | A kebab-case Vue component (`<hello-box>`, `<router-view>`) renders as an empty element, with no warning | `isCustomElement` matches every hyphenated tag | match only the Nativele prefix (`tag.startsWith("nv-")`) |
264
+ | An object prop shows `[object Object]`, or the element has an attribute holding it | the element was not defined when Vue rendered it, or the prop is written in kebab-case | import the module before `mount()`; write `:selectedItem`, not `:selected-item`; for a module loaded later, use `.item` |
265
+ | The event listener never runs | the name after `@` differs from the emitted name (`@itemSelect` for `item-select`) | write the exact event name |
266
+ | The Vue value bound with `v-model` never changes | `v-model` does not work on custom elements like this | a prop plus an event (limitation 5) |
267
+ | `Cannot read properties of null (reading 'insertBefore')` or `(setting '__vnode')`, children in the wrong slot | Vue changed children of a Light DOM component with slots | `shadow: true`, or a stable element per slot |
268
+ | `TS7006: Parameter 'e' implicitly has an 'any' type` in an `@item-select` handler, wrong props not reported | the declaration is missing, or the component class is not exported | add `src/nativele-vue.d.ts`, export the class, run `ts-dom gen` |
269
+ | `TS1117: An object literal cannot have multiple properties with the same name` | two spellings of one event on the same element (`@picked-one` and `@pickedOne` are both `onPickedOne` in the types) | keep one, with the exact event name |
270
+ | `ts-dom check` reports `TS2307: Cannot find module './App.vue'` | `vue-tsc` is not installed, so `ts-dom check` runs plain `tsc` | keep `vue-tsc` in `devDependencies` |
271
+ | `ts-dom check` reports `unused-ref` | a `data-ref` the class never reads | use it in the class or remove the attribute |
272
+
273
+ ## See also
274
+
275
+ - Components, props and events: https://github.com/Pixmi/nativele/blob/main/docs/components.md#data-flow-props-down-events-up
276
+ - Form inputs written from `render`: https://github.com/Pixmi/nativele/blob/main/docs/components.md#a-small-reminder-about-form-inputs
277
+ - Slots, Light DOM and Shadow DOM: https://github.com/Pixmi/nativele/blob/main/docs/components.md#light-default-and-shadow
278
+ - Moved elements, connecting and subscriptions: https://github.com/Pixmi/nativele/blob/main/docs/lists.md#moving-items-connecting-and-subscriptions
279
+ - Setup, the Vite plugin and checks: https://github.com/Pixmi/nativele/blob/main/docs/tooling.md