mantle-lit 0.1.0 → 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,16 +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
- interface CounterProps {
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;
22
24
 
23
- class CounterView extends View<CounterProps> {
25
+ // Internal state - auto-observable
24
26
  count = 0;
25
27
 
26
28
  onCreate() {
27
- this.count = this.props.initial;
29
+ this.count = this.initialCount;
28
30
  }
29
31
 
30
32
  increment() {
@@ -41,30 +43,104 @@ class CounterView extends View<CounterProps> {
41
43
  }
42
44
 
43
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
+ }
44
53
  ```
45
54
 
46
55
  **Usage in HTML (property binding with `.`):**
47
56
  ```html
48
- <x-counter .initial=${5}></x-counter>
57
+ <x-counter .initialCount=${5}></x-counter>
49
58
  ```
50
59
 
51
- **Everything is reactive by default.** All properties become observable, getters become computed, and methods become auto-bound actions. No annotations needed.
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
- > Want explicit control? See [Decorators](#decorators) below to opt into manual annotations.
62
+ ## Defining Props
54
63
 
55
- ## Property Binding
64
+ Use Lit's `@property()` decorator for props. Use `attribute: false` since we pass complex data via property binding:
56
65
 
57
- 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.
66
+ ```ts
67
+ import { View, createView } from 'mantle-lit';
68
+ import { html } from 'lit';
69
+ import { property } from 'lit/decorators.js';
70
+
71
+ interface TodoItem {
72
+ id: number;
73
+ text: string;
74
+ done: boolean;
75
+ }
76
+
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
+ }
97
+ }
98
+ ```
99
+
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.
103
+
104
+ ## Scoped Styles
105
+
106
+ Use Lit's `static styles` for component-scoped CSS:
58
107
 
59
108
  ```ts
60
- // Parent component
61
- render() {
62
- return html`
63
- <x-todo-list
64
- .items=${this.todos}
65
- .onDelete=${this.handleDelete}
66
- ></x-todo-list>
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
+ }
67
123
  `;
124
+
125
+ render() {
126
+ return html`<button>Click me</button>`;
127
+ }
128
+ }
129
+ ```
130
+
131
+ For larger components, extract styles to a separate file:
132
+
133
+ ```ts
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
+ // ...
68
144
  }
69
145
  ```
70
146
 
@@ -99,7 +175,7 @@ render() {
99
175
  ```ts
100
176
  onCreate() {
101
177
  this.watch(
102
- () => this.props.filter,
178
+ () => this.filter,
103
179
  (filter) => this.applyFilter(filter)
104
180
  );
105
181
  }
@@ -136,7 +212,10 @@ this.watch(
136
212
  **Basic example:**
137
213
 
138
214
  ```ts
139
- class SearchView extends View<Props> {
215
+ class SearchView extends View {
216
+ @property({ type: String, attribute: false })
217
+ placeholder = '';
218
+
140
219
  query = '';
141
220
  results: string[] = [];
142
221
 
@@ -158,9 +237,9 @@ class SearchView extends View<Props> {
158
237
 
159
238
  ```ts
160
239
  onCreate() {
161
- this.watch(() => this.props.filter, (filter) => this.applyFilter(filter));
162
- this.watch(() => this.props.sort, (sort) => this.applySort(sort));
163
- 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));
164
243
  }
165
244
  ```
166
245
 
@@ -168,7 +247,7 @@ onCreate() {
168
247
 
169
248
  ```ts
170
249
  onCreate() {
171
- const stop = this.watch(() => this.props.token, (token) => {
250
+ const stop = this.watch(() => this.token, (token) => {
172
251
  this.authenticate(token);
173
252
  stop(); // only needed once
174
253
  });
@@ -177,35 +256,65 @@ onCreate() {
177
256
 
178
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`.
179
258
 
180
- ### Props Reactivity
259
+ ## Mounting Components
260
+
261
+ Use the `mount` helper to imperatively create and mount components:
262
+
263
+ ```ts
264
+ import { mount } from 'mantle-lit';
265
+ import './MyComponent';
266
+
267
+ // Mount with props
268
+ mount('x-my-component', {
269
+ title: 'Hello',
270
+ items: [1, 2, 3],
271
+ onSelect: (item) => console.log(item),
272
+ }, document.body);
273
+
274
+ // Returns the created element
275
+ const el = mount('x-counter', { initialCount: 5 }, container);
276
+ ```
181
277
 
182
- `this.props` is reactive: your component re-renders when accessed props change.
278
+ ## IDE Autocomplete
183
279
 
184
- **Option 1: `this.watch`** the recommended way to react to state changes:
280
+ For IDE autocomplete in Lit templates, add `HTMLElementTagNameMap` declarations:
185
281
 
186
282
  ```ts
187
- onCreate() {
188
- this.watch(
189
- () => this.props.filter,
190
- (filter) => this.applyFilter(filter)
191
- );
283
+ declare global {
284
+ interface HTMLElementTagNameMap {
285
+ 'x-my-component': MyComponentView;
286
+ }
192
287
  }
193
288
  ```
194
289
 
195
- Watchers are automatically disposed on unmount. No cleanup needed.
290
+ Install the [lit-plugin](https://marketplace.visualstudio.com/items?itemName=runem.lit-plugin) VS Code extension for template type checking.
196
291
 
197
- **Option 2: `reaction`** — for advanced MobX patterns (autorun, when, custom schedulers):
292
+ **CLI validation** (works reliably):
293
+ ```bash
294
+ npx lit-analyzer "src/**/*.ts" --strict
295
+ ```
198
296
 
199
- ```ts
200
- onMount() {
201
- return reaction(
202
- () => this.props.filter,
203
- (filter) => this.applyFilter(filter)
204
- );
297
+ Add to your `package.json`:
298
+ ```json
299
+ {
300
+ "scripts": {
301
+ "lint:lit": "lit-analyzer \"src/**/*.ts\" --strict"
302
+ }
205
303
  }
206
304
  ```
207
305
 
208
- Or access props directly in `render()` and MobX handles re-renders when they change.
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
+ }
317
+ ```
209
318
 
210
319
  ## Patterns
211
320
 
@@ -216,6 +325,7 @@ State, logic, and template in one class:
216
325
  ```ts
217
326
  import { View, createView } from 'mantle-lit';
218
327
  import { html } from 'lit';
328
+ import { property } from 'lit/decorators.js';
219
329
 
220
330
  interface TodoItem {
221
331
  id: number;
@@ -224,9 +334,16 @@ interface TodoItem {
224
334
  }
225
335
 
226
336
  class TodoView extends View {
337
+ @property({ type: Array, attribute: false })
338
+ initialTodos: TodoItem[] = [];
339
+
227
340
  todos: TodoItem[] = [];
228
341
  input = '';
229
342
 
343
+ onCreate() {
344
+ this.todos = this.initialTodos;
345
+ }
346
+
230
347
  add() {
231
348
  this.todos.push({ id: Date.now(), text: this.input, done: false });
232
349
  this.input = '';
@@ -248,6 +365,12 @@ class TodoView extends View {
248
365
  }
249
366
 
250
367
  export const Todo = createView(TodoView, { tag: 'x-todo' });
368
+
369
+ declare global {
370
+ interface HTMLElementTagNameMap {
371
+ 'x-todo': TodoView;
372
+ }
373
+ }
251
374
  ```
252
375
 
253
376
  ### Separated
@@ -255,10 +378,10 @@ export const Todo = createView(TodoView, { tag: 'x-todo' });
255
378
  ViewModel and template separate:
256
379
 
257
380
  ```ts
258
- import { ViewModel, createView } from 'mantle-lit';
381
+ import { View, createView } from 'mantle-lit';
259
382
  import { html } from 'lit';
260
383
 
261
- class TodoViewModel extends ViewModel {
384
+ class TodoViewModel extends View {
262
385
  todos: TodoItem[] = [];
263
386
  input = '';
264
387
 
@@ -281,8 +404,6 @@ const template = (vm: TodoViewModel) => html`
281
404
  </div>
282
405
  `;
283
406
 
284
- // Note: For separated templates, extend the View class with a render method
285
- // that calls the template function
286
407
  class TodoView extends TodoViewModel {
287
408
  render() {
288
409
  return template(this);
@@ -299,8 +420,12 @@ For teams that prefer explicit annotations over auto-observable, Mantle provides
299
420
  ```ts
300
421
  import { View, createView, observable, action, computed } from 'mantle-lit';
301
422
  import { html } from 'lit';
423
+ import { property } from 'lit/decorators.js';
302
424
 
303
425
  class TodoView extends View {
426
+ @property({ type: String, attribute: false })
427
+ title = '';
428
+
304
429
  @observable todos: TodoItem[] = [];
305
430
  @observable input = '';
306
431
 
@@ -355,8 +480,6 @@ class TodoView extends View {
355
480
  export const Todo = createView(TodoView, { tag: 'x-todo' });
356
481
  ```
357
482
 
358
- Note: `this.props` is always reactive regardless of decorator mode.
359
-
360
483
  ## Error Handling
361
484
 
362
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.
@@ -531,19 +654,36 @@ configure({ autoObservable: false });
531
654
  | `autoObservable` | `true` | Whether to automatically make View instances observable |
532
655
  | `onError` | `console.error` | Global error handler for lifecycle errors (see [Error Handling](#error-handling)) |
533
656
 
534
- ### `View<P>` / `ViewModel<P>`
657
+ ### `View`
535
658
 
536
- Base class for view components. `ViewModel` is an alias for `View`. Use it when separating the ViewModel from the template for semantic clarity.
659
+ Base class for view components. Extends `LitElement` with MobX integration.
537
660
 
538
661
  | Property/Method | Description |
539
662
  |-----------------|-------------|
540
- | `props` | Current props (reactive) |
541
663
  | `onCreate()` | Called when instance created |
542
664
  | `onMount()` | Called when connected to DOM, return cleanup (optional) |
543
665
  | `onUnmount()` | Called when disconnected from DOM (optional) |
544
666
  | `render()` | Return Lit `TemplateResult` |
545
667
  | `watch(expr, callback, options?)` | Watch reactive expression, auto-disposed on unmount |
546
668
 
669
+ ### `mount(tag, props, container)`
670
+
671
+ Imperatively create and mount a custom element:
672
+
673
+ ```ts
674
+ import { mount } from 'mantle-lit';
675
+
676
+ const element = mount('x-my-component', { title: 'Hello' }, document.body);
677
+ ```
678
+
679
+ | Argument | Type | Description |
680
+ |----------|------|-------------|
681
+ | `tag` | `string` | Custom element tag name |
682
+ | `props` | `object` | Properties to set on the element |
683
+ | `container` | `Element \| string` | Container element or selector |
684
+
685
+ Returns the created element.
686
+
547
687
  ### `Behavior`
548
688
 
549
689
  Base class for behaviors. Extend it and wrap with `createBehavior()`.
@@ -571,7 +711,7 @@ export const withMyBehavior = createBehavior(MyBehavior);
571
711
 
572
712
  ### `createView(ViewClass, options)`
573
713
 
574
- Function that creates and registers a Lit custom element from a View class.
714
+ Function that registers a View class as a custom element.
575
715
 
576
716
  ```ts
577
717
  // Basic