mantle-lit 0.1.1 → 0.1.2

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/README.md CHANGED
@@ -1,11 +1,11 @@
1
1
  # Mantle Lit
2
2
 
3
- A lightweight library for building Lit web components with a simpler class-based API and MobX reactivity built in.
3
+ A lightweight library for building Lit web components with MobX reactivity. Extends LitElement with automatic observable state, computed getters, and bound actions.
4
4
 
5
5
  ## Installation
6
6
 
7
7
  ```bash
8
- npm install mantle-lit
8
+ npm install mantle-lit lit mobx
9
9
  ```
10
10
 
11
11
  Requires Lit 3+ and MobX 6+.
@@ -15,14 +15,18 @@ Requires Lit 3+ and MobX 6+.
15
15
  ```ts
16
16
  import { View, createView } from 'mantle-lit';
17
17
  import { html } from 'lit';
18
+ import { property } from 'lit/decorators.js';
18
19
 
19
- class CounterView extends View.props({
20
- initial: Number,
21
- }) {
20
+ class CounterView extends View {
21
+ // Props - use @property for Lit reactivity and IDE autocomplete
22
+ @property({ type: Number, attribute: false })
23
+ initialCount = 0;
24
+
25
+ // Internal state - auto-observable
22
26
  count = 0;
23
27
 
24
28
  onCreate() {
25
- this.count = this.props.initial ?? 0;
29
+ this.count = this.initialCount;
26
30
  }
27
31
 
28
32
  increment() {
@@ -39,23 +43,30 @@ class CounterView extends View.props({
39
43
  }
40
44
 
41
45
  export const Counter = createView(CounterView, { tag: 'x-counter' });
46
+
47
+ // Register type for IDE autocomplete in templates
48
+ declare global {
49
+ interface HTMLElementTagNameMap {
50
+ 'x-counter': CounterView;
51
+ }
52
+ }
42
53
  ```
43
54
 
44
55
  **Usage in HTML (property binding with `.`):**
45
56
  ```html
46
- <x-counter .initial=${5}></x-counter>
57
+ <x-counter .initialCount=${5}></x-counter>
47
58
  ```
48
59
 
49
- **Everything is reactive by default.** All properties become observable, getters become computed, and methods become auto-bound actions. No annotations needed.
50
-
51
- > Want explicit control? See [Decorators](#decorators) below to opt into manual annotations.
60
+ **Everything is reactive by default.** Internal state becomes observable, getters become computed, and methods become auto-bound actions. Props use Lit's standard `@property()` decorator.
52
61
 
53
62
  ## Defining Props
54
63
 
55
- Use `View.props()` to define component properties with full TypeScript inference:
64
+ Use Lit's `@property()` decorator for props. Use `attribute: false` since we pass complex data via property binding:
56
65
 
57
66
  ```ts
58
- import { View, createView, PropType } from 'mantle-lit';
67
+ import { View, createView } from 'mantle-lit';
68
+ import { html } from 'lit';
69
+ import { property } from 'lit/decorators.js';
59
70
 
60
71
  interface TodoItem {
61
72
  id: number;
@@ -63,46 +74,73 @@ interface TodoItem {
63
74
  done: boolean;
64
75
  }
65
76
 
66
- class TodoView extends View.props({
67
- title: String,
68
- initialTodos: Array as PropType<TodoItem[]>,
69
- onCountChange: Function as PropType<(count: number) => void>,
70
- }) {
71
- // this.props.title → string
72
- // this.props.initialTodos → TodoItem[]
73
- // this.props.onCountChange (count: number) => void
77
+ class TodoView extends View {
78
+ @property({ type: String, attribute: false })
79
+ title = '';
80
+
81
+ @property({ type: Array, attribute: false })
82
+ initialTodos: TodoItem[] = [];
83
+
84
+ @property({ attribute: false })
85
+ onComplete?: (count: number) => void;
86
+
87
+ // Internal state (auto-observable, no decorator needed)
88
+ todos: TodoItem[] = [];
89
+ }
90
+
91
+ export const Todo = createView(TodoView, { tag: 'x-todo' });
92
+
93
+ declare global {
94
+ interface HTMLElementTagNameMap {
95
+ 'x-todo': TodoView;
96
+ }
74
97
  }
75
98
  ```
76
99
 
77
- **Type constructors:**
78
- - `String` → `string`
79
- - `Number` `number`
80
- - `Boolean` → `boolean`
81
- - `Array` → `any[]` (use `PropType<T[]>` for specific types)
82
- - `Object` → `object` (use `PropType<T>` for specific types)
83
- - `Function` → `Function` (use `PropType<(args) => R>` for specific signatures)
100
+ **Why `attribute: false`?** We use property binding (`.prop=${value}`) to pass complex objects, arrays, and functions. Attribute reflection isn't needed and can cause issues with non-primitive types.
101
+
102
+ **No props?** Just extend `View` directly without any `@property()` decorators.
84
103
 
85
- **No props?** Just extend `View` directly:
104
+ ## Scoped Styles
105
+
106
+ Use Lit's `static styles` for component-scoped CSS:
86
107
 
87
108
  ```ts
88
- class SimpleView extends View {
89
- // no props needed
109
+ import { View, createView } from 'mantle-lit';
110
+ import { html, css } from 'lit';
111
+
112
+ class MyView extends View {
113
+ static styles = css`
114
+ :host {
115
+ display: block;
116
+ padding: 1rem;
117
+ }
118
+
119
+ button {
120
+ background: #6366f1;
121
+ color: white;
122
+ }
123
+ `;
124
+
125
+ render() {
126
+ return html`<button>Click me</button>`;
127
+ }
90
128
  }
91
129
  ```
92
130
 
93
- ## Property Binding
94
-
95
- This library is designed for **property binding** (`.prop=${value}`) rather than attribute binding (`attr="value"`). This allows passing complex objects, arrays, and functions as props.
131
+ For larger components, extract styles to a separate file:
96
132
 
97
133
  ```ts
98
- // Parent component
99
- render() {
100
- return html`
101
- <x-todo-list
102
- .items=${this.todos}
103
- .onDelete=${this.handleDelete}
104
- ></x-todo-list>
105
- `;
134
+ // MyView.styles.ts
135
+ import { css } from 'lit';
136
+ export const styles = css`...`;
137
+
138
+ // MyView.ts
139
+ import { styles } from './MyView.styles';
140
+
141
+ class MyView extends View {
142
+ static styles = styles;
143
+ // ...
106
144
  }
107
145
  ```
108
146
 
@@ -137,7 +175,7 @@ render() {
137
175
  ```ts
138
176
  onCreate() {
139
177
  this.watch(
140
- () => this.props.filter,
178
+ () => this.filter,
141
179
  (filter) => this.applyFilter(filter)
142
180
  );
143
181
  }
@@ -174,9 +212,10 @@ this.watch(
174
212
  **Basic example:**
175
213
 
176
214
  ```ts
177
- class SearchView extends View.props({
178
- placeholder: String,
179
- }) {
215
+ class SearchView extends View {
216
+ @property({ type: String, attribute: false })
217
+ placeholder = '';
218
+
180
219
  query = '';
181
220
  results: string[] = [];
182
221
 
@@ -198,9 +237,9 @@ class SearchView extends View.props({
198
237
 
199
238
  ```ts
200
239
  onCreate() {
201
- this.watch(() => this.props.filter, (filter) => this.applyFilter(filter));
202
- this.watch(() => this.props.sort, (sort) => this.applySort(sort));
203
- this.watch(() => this.props.page, (page) => this.fetchPage(page));
240
+ this.watch(() => this.filter, (filter) => this.applyFilter(filter));
241
+ this.watch(() => this.sort, (sort) => this.applySort(sort));
242
+ this.watch(() => this.page, (page) => this.fetchPage(page));
204
243
  }
205
244
  ```
206
245
 
@@ -208,7 +247,7 @@ onCreate() {
208
247
 
209
248
  ```ts
210
249
  onCreate() {
211
- const stop = this.watch(() => this.props.token, (token) => {
250
+ const stop = this.watch(() => this.token, (token) => {
212
251
  this.authenticate(token);
213
252
  stop(); // only needed once
214
253
  });
@@ -217,36 +256,6 @@ onCreate() {
217
256
 
218
257
  `this.watch` wraps MobX's `reaction` with automatic lifecycle disposal. For advanced MobX patterns (`autorun`, `when`, custom schedulers), use `reaction` directly and return a dispose function from `onMount`.
219
258
 
220
- ### Props Reactivity
221
-
222
- `this.props` is reactive: your component re-renders when accessed props change.
223
-
224
- **Option 1: `this.watch`** — the recommended way to react to state changes:
225
-
226
- ```ts
227
- onCreate() {
228
- this.watch(
229
- () => this.props.filter,
230
- (filter) => this.applyFilter(filter)
231
- );
232
- }
233
- ```
234
-
235
- Watchers are automatically disposed on unmount. No cleanup needed.
236
-
237
- **Option 2: `reaction`** — for advanced MobX patterns (autorun, when, custom schedulers):
238
-
239
- ```ts
240
- onMount() {
241
- return reaction(
242
- () => this.props.filter,
243
- (filter) => this.applyFilter(filter)
244
- );
245
- }
246
- ```
247
-
248
- Or access props directly in `render()` and MobX handles re-renders when they change.
249
-
250
259
  ## Mounting Components
251
260
 
252
261
  Use the `mount` helper to imperatively create and mount components:
@@ -263,7 +272,48 @@ mount('x-my-component', {
263
272
  }, document.body);
264
273
 
265
274
  // Returns the created element
266
- const el = mount('x-counter', { initial: 5 }, container);
275
+ const el = mount('x-counter', { initialCount: 5 }, container);
276
+ ```
277
+
278
+ ## IDE Autocomplete
279
+
280
+ For IDE autocomplete in Lit templates, add `HTMLElementTagNameMap` declarations:
281
+
282
+ ```ts
283
+ declare global {
284
+ interface HTMLElementTagNameMap {
285
+ 'x-my-component': MyComponentView;
286
+ }
287
+ }
288
+ ```
289
+
290
+ Install the [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) VS Code extension for template type checking.
291
+
292
+ **CLI validation** (works reliably):
293
+ ```bash
294
+ npx lit-analyzer "src/**/*.ts" --strict
295
+ ```
296
+
297
+ Add to your `package.json`:
298
+ ```json
299
+ {
300
+ "scripts": {
301
+ "lint:lit": "lit-analyzer \"src/**/*.ts\" --strict"
302
+ }
303
+ }
304
+ ```
305
+
306
+ ## TypeScript Configuration
307
+
308
+ For `@property()` decorators to work correctly:
309
+
310
+ ```json
311
+ {
312
+ "compilerOptions": {
313
+ "experimentalDecorators": true,
314
+ "useDefineForClassFields": false
315
+ }
316
+ }
267
317
  ```
268
318
 
269
319
  ## Patterns
@@ -275,6 +325,7 @@ State, logic, and template in one class:
275
325
  ```ts
276
326
  import { View, createView } from 'mantle-lit';
277
327
  import { html } from 'lit';
328
+ import { property } from 'lit/decorators.js';
278
329
 
279
330
  interface TodoItem {
280
331
  id: number;
@@ -283,9 +334,16 @@ interface TodoItem {
283
334
  }
284
335
 
285
336
  class TodoView extends View {
337
+ @property({ type: Array, attribute: false })
338
+ initialTodos: TodoItem[] = [];
339
+
286
340
  todos: TodoItem[] = [];
287
341
  input = '';
288
342
 
343
+ onCreate() {
344
+ this.todos = this.initialTodos;
345
+ }
346
+
289
347
  add() {
290
348
  this.todos.push({ id: Date.now(), text: this.input, done: false });
291
349
  this.input = '';
@@ -307,6 +365,12 @@ class TodoView extends View {
307
365
  }
308
366
 
309
367
  export const Todo = createView(TodoView, { tag: 'x-todo' });
368
+
369
+ declare global {
370
+ interface HTMLElementTagNameMap {
371
+ 'x-todo': TodoView;
372
+ }
373
+ }
310
374
  ```
311
375
 
312
376
  ### Separated
@@ -314,10 +378,10 @@ export const Todo = createView(TodoView, { tag: 'x-todo' });
314
378
  ViewModel and template separate:
315
379
 
316
380
  ```ts
317
- import { ViewModel, createView } from 'mantle-lit';
381
+ import { View, createView } from 'mantle-lit';
318
382
  import { html } from 'lit';
319
383
 
320
- class TodoViewModel extends ViewModel {
384
+ class TodoViewModel extends View {
321
385
  todos: TodoItem[] = [];
322
386
  input = '';
323
387
 
@@ -340,8 +404,6 @@ const template = (vm: TodoViewModel) => html`
340
404
  </div>
341
405
  `;
342
406
 
343
- // Note: For separated templates, extend the View class with a render method
344
- // that calls the template function
345
407
  class TodoView extends TodoViewModel {
346
408
  render() {
347
409
  return template(this);
@@ -358,8 +420,12 @@ For teams that prefer explicit annotations over auto-observable, Mantle provides
358
420
  ```ts
359
421
  import { View, createView, observable, action, computed } from 'mantle-lit';
360
422
  import { html } from 'lit';
423
+ import { property } from 'lit/decorators.js';
361
424
 
362
425
  class TodoView extends View {
426
+ @property({ type: String, attribute: false })
427
+ title = '';
428
+
363
429
  @observable todos: TodoItem[] = [];
364
430
  @observable input = '';
365
431
 
@@ -414,8 +480,6 @@ class TodoView extends View {
414
480
  export const Todo = createView(TodoView, { tag: 'x-todo' });
415
481
  ```
416
482
 
417
- Note: `this.props` is always reactive regardless of decorator mode.
418
-
419
483
  ## Error Handling
420
484
 
421
485
  Render errors propagate to the browser as usual. Lifecycle errors (`onMount`, `onUnmount`, `watch`) in both Views and Behaviors are caught and routed through a configurable handler.
@@ -590,44 +654,18 @@ configure({ autoObservable: false });
590
654
  | `autoObservable` | `true` | Whether to automatically make View instances observable |
591
655
  | `onError` | `console.error` | Global error handler for lifecycle errors (see [Error Handling](#error-handling)) |
592
656
 
593
- ### `View` / `ViewModel`
657
+ ### `View`
594
658
 
595
- Base class for view components. `ViewModel` is an alias for `View`. Use it when separating the ViewModel from the template for semantic clarity.
596
-
597
- **Defining props:**
598
-
599
- ```ts
600
- class MyView extends View.props({
601
- title: String,
602
- items: Array as PropType<Item[]>,
603
- }) {
604
- // this.props is fully typed
605
- }
606
- ```
659
+ Base class for view components. Extends `LitElement` with MobX integration.
607
660
 
608
661
  | Property/Method | Description |
609
662
  |-----------------|-------------|
610
- | `props` | Current props (reactive, typed based on `View.props()`) |
611
663
  | `onCreate()` | Called when instance created |
612
664
  | `onMount()` | Called when connected to DOM, return cleanup (optional) |
613
665
  | `onUnmount()` | Called when disconnected from DOM (optional) |
614
666
  | `render()` | Return Lit `TemplateResult` |
615
667
  | `watch(expr, callback, options?)` | Watch reactive expression, auto-disposed on unmount |
616
668
 
617
- ### `PropType<T>`
618
-
619
- Type helper for complex prop types:
620
-
621
- ```ts
622
- import { PropType } from 'mantle-lit';
623
-
624
- View.props({
625
- items: Array as PropType<MyItem[]>,
626
- onSelect: Function as PropType<(item: MyItem) => void>,
627
- config: Object as PropType<{ theme: string; debug: boolean }>,
628
- })
629
- ```
630
-
631
669
  ### `mount(tag, props, container)`
632
670
 
633
671
  Imperatively create and mount a custom element:
@@ -642,7 +680,7 @@ const element = mount('x-my-component', { title: 'Hello' }, document.body);
642
680
  |----------|------|-------------|
643
681
  | `tag` | `string` | Custom element tag name |
644
682
  | `props` | `object` | Properties to set on the element |
645
- | `container` | `Element` | Container to append the element to |
683
+ | `container` | `Element \| string` | Container element or selector |
646
684
 
647
685
  Returns the created element.
648
686
 
@@ -673,7 +711,7 @@ export const withMyBehavior = createBehavior(MyBehavior);
673
711
 
674
712
  ### `createView(ViewClass, options)`
675
713
 
676
- Function that creates and registers a Lit custom element from a View class.
714
+ Function that registers a View class as a custom element.
677
715
 
678
716
  ```ts
679
717
  // Basic