@hot-page/fun 0.0.1 → 0.0.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-page/fun",
3
- "version": "0.0.1",
3
+ "version": "0.0.3",
4
4
  "description": "Simple define helper for functional web components",
5
5
  "repository": {
6
6
  "type": "git",
package/src/index.ts CHANGED
@@ -10,6 +10,13 @@ export type StylePropsFunction = (
10
10
  props: Record<string, string | number | null>
11
11
  ) => void
12
12
 
13
+ interface EffectEntry {
14
+ fn: EffectFunction
15
+ computed?: Signal.Computed<CleanupFunction | void>
16
+ watcher?: any
17
+ cleanup?: CleanupFunction | void
18
+ }
19
+
13
20
  type AttributeSignals<Attrs extends string> = {
14
21
  [K in Attrs]: Signal.State<string | null>
15
22
  }
@@ -103,8 +110,9 @@ function resolveOverload<Attrs extends string>(
103
110
  }
104
111
 
105
112
  const state = <T>(value: T) => new Signal.State(value)
113
+ const computed = <T>(fn: () => T) => new Signal.Computed(fn)
106
114
 
107
- export { state, lightElement, shadowElement }
115
+ export { state, computed, lightElement, shadowElement }
108
116
 
109
117
  export function define<Attrs extends string = never>(options: DefineOptions<Attrs>) {
110
118
  const {
@@ -129,11 +137,11 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
129
137
  throw new Error(`Custom element with name ${elementName} already defined`)
130
138
  }
131
139
 
132
- for (const attr of attributes) {
140
+ attributes.forEach(attr => {
133
141
  if (RESERVED_KEYS.has(attr)) {
134
142
  throw new Error(`Attribute name "${attr}" conflicts with a reserved context property.`)
135
143
  }
136
- }
144
+ })
137
145
 
138
146
  // One constructed stylesheet per element type, shared across all instances.
139
147
  // For shadow DOM, it's adopted into each shadowRoot.
@@ -153,10 +161,10 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
153
161
  }
154
162
 
155
163
  customElements.define(elementName, class extends HTMLElement {
156
- #template!: Signal.Computed<ReturnType<typeof html>>
157
- #watcher!: any
158
- #effects: EffectFunction[] = []
159
- #cleanups: CleanupFunction[] = []
164
+ #template: Signal.Computed<ReturnType<typeof html>> | undefined
165
+ #watcher: any
166
+ #renderTemplate: (() => void) | undefined
167
+ #effects: EffectEntry[] = []
160
168
  #attributeSignals: Map<string, Signal.State<string | null>> = new Map()
161
169
 
162
170
  static get observedAttributes() {
@@ -180,11 +188,9 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
180
188
 
181
189
  const context = {
182
190
  internals: this.attachInternals(),
183
- effect: (fn: EffectFunction) => {
184
- this.#effects.push(fn)
185
- },
191
+ effect: (fn: EffectFunction) => this.#effects.push({ fn }),
186
192
  styleProps: (props: Record<string, string | number | null>) => {
187
- for (const key in props) {
193
+ Object.keys(props).forEach(key => {
188
194
  const value = props[key]
189
195
  const name = toCustomProperty(key)
190
196
  if (value === null) {
@@ -192,7 +198,7 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
192
198
  } else {
193
199
  this.style.setProperty(name, String(value))
194
200
  }
195
- }
201
+ })
196
202
  }
197
203
  } as ComponentContext<Attrs>
198
204
 
@@ -234,42 +240,110 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
234
240
  watcher.watch(signal)
235
241
  })
236
242
 
237
- const templateFn = setup.call(this, context)
243
+ const setupResult = setup.call(this, context)
238
244
 
239
- this.#template = new Signal.Computed(() => templateFn())
245
+ const target = useShadow ? this.shadowRoot! : this
240
246
 
241
- const renderTemplate = () =>
242
- render(this.#template.get(), useShadow ? this.shadowRoot! : this)
247
+ if (typeof setupResult === 'function') {
248
+ this.#template = new Signal.Computed(() => setupResult())
243
249
 
244
- let renderPending = false
245
- this.#watcher = new Signal.subtle.Watcher(() => {
246
- if (renderPending) return
247
- renderPending = true
248
- queueMicrotask(() => {
249
- renderPending = false
250
- renderTemplate()
251
- this.#watcher.watch()
252
- })
253
- })
250
+ this.#renderTemplate = () => {
251
+ const result = this.#template!.get()
252
+ if (typeof result === 'string') {
253
+ target.innerHTML = result
254
+ } else {
255
+ render(result, target)
256
+ }
257
+ }
254
258
 
255
- this.#watcher.watch(this.#template)
259
+ let renderPending = false
260
+ this.#watcher = new Signal.subtle.Watcher(() => {
261
+ if (renderPending) return
262
+ renderPending = true
263
+ queueMicrotask(() => {
264
+ renderPending = false
265
+ try {
266
+ this.#renderTemplate!()
267
+ } catch (error) {
268
+ console.error(
269
+ `Error in render function for <${elementName}> fun element: `,
270
+ error,
271
+ )
272
+ }
273
+ this.#watcher.watch()
274
+ })
275
+ })
256
276
 
257
- renderTemplate()
277
+ } else if (typeof setupResult === 'string') {
278
+ target.innerHTML = setupResult
279
+ } else if (typeof setupResult === 'object' && '_$litType$' in (setupResult as object)) {
280
+ render(setupResult, target)
281
+ } else if (setupResult === undefined) {
282
+ return
283
+ } else {
284
+ console.error(
285
+ `Setup function for <${elementName}> returned an unexpected value. ` +
286
+ `Expected a render function, a template (html\`...\`), a string, or nothing. ` +
287
+ `Got: ${typeof setupResult}`
288
+ )
289
+ }
258
290
  }
259
291
 
260
292
  connectedCallback() {
261
- this.#watcher.watch(this.#template)
262
- this.#cleanups = this.#effects
263
- .map(effect => effect())
264
- .filter((cleanup): cleanup is CleanupFunction =>
265
- typeof cleanup === 'function'
266
- )
293
+ if (this.#watcher && this.#template) {
294
+ this.#watcher.watch(this.#template)
295
+ this.#renderTemplate?.()
296
+ }
297
+
298
+ this.#effects.forEach(entry => {
299
+ try {
300
+ entry.computed = new Signal.Computed(() => entry.fn())
301
+
302
+ entry.watcher = new Signal.subtle.Watcher(() => {
303
+ queueMicrotask(() => {
304
+ if (typeof entry.cleanup === 'function') {
305
+ entry.cleanup()
306
+ }
307
+
308
+ entry.computed = new Signal.Computed(() => entry.fn())
309
+
310
+ try {
311
+ entry.cleanup = entry.computed.get()
312
+ } catch (error) {
313
+ console.error(
314
+ `Error in effect for <${elementName}> fun element: `,
315
+ error
316
+ )
317
+ }
318
+
319
+ entry.watcher.watch(entry.computed)
320
+ })
321
+ })
322
+
323
+ entry.cleanup = entry.computed.get()
324
+ entry.watcher.watch(entry.computed)
325
+ } catch (error) {
326
+ console.error(
327
+ `Error in effect for <${elementName}> fun element: `,
328
+ error
329
+ )
330
+ }
331
+ })
267
332
  }
268
333
 
269
334
  disconnectedCallback() {
270
- this.#watcher.unwatch(this.#template)
271
- this.#cleanups.forEach(cleanup => cleanup())
272
- this.#cleanups = []
335
+ if (this.#watcher && this.#template) {
336
+ this.#watcher.unwatch(this.#template)
337
+ }
338
+
339
+ // Clean up all effects and stop watching
340
+ this.#effects.forEach(effectEntry => {
341
+ effectEntry.watcher.unwatch(effectEntry.computed)
342
+ if (typeof effectEntry.cleanup === 'function') {
343
+ effectEntry.cleanup()
344
+ }
345
+ effectEntry.cleanup = undefined
346
+ })
273
347
  }
274
348
 
275
349
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {