@hot-page/fun 0.0.1 → 0.0.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hot-page/fun",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
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,9 @@ 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
+ #effects: EffectEntry[] = []
160
167
  #attributeSignals: Map<string, Signal.State<string | null>> = new Map()
161
168
 
162
169
  static get observedAttributes() {
@@ -180,11 +187,9 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
180
187
 
181
188
  const context = {
182
189
  internals: this.attachInternals(),
183
- effect: (fn: EffectFunction) => {
184
- this.#effects.push(fn)
185
- },
190
+ effect: (fn: EffectFunction) => this.#effects.push({ fn }),
186
191
  styleProps: (props: Record<string, string | number | null>) => {
187
- for (const key in props) {
192
+ Object.keys(props).forEach(key => {
188
193
  const value = props[key]
189
194
  const name = toCustomProperty(key)
190
195
  if (value === null) {
@@ -192,7 +197,7 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
192
197
  } else {
193
198
  this.style.setProperty(name, String(value))
194
199
  }
195
- }
200
+ })
196
201
  }
197
202
  } as ComponentContext<Attrs>
198
203
 
@@ -234,42 +239,112 @@ export function define<Attrs extends string = never>(options: DefineOptions<Attr
234
239
  watcher.watch(signal)
235
240
  })
236
241
 
237
- const templateFn = setup.call(this, context)
242
+ const setupResult = setup.call(this, context)
238
243
 
239
- this.#template = new Signal.Computed(() => templateFn())
244
+ const target = useShadow ? this.shadowRoot! : this
240
245
 
241
- const renderTemplate = () =>
242
- render(this.#template.get(), useShadow ? this.shadowRoot! : this)
246
+ if (typeof setupResult === 'function') {
247
+ this.#template = new Signal.Computed(() => setupResult())
243
248
 
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
- })
249
+ const template = this.#template
250
+ const renderTemplate = () => {
251
+ const result = template.get()
252
+ if (typeof result === 'string') {
253
+ target.innerHTML = result
254
+ } else if (result !== undefined && result !== null) {
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
+ renderTemplate()
267
+ } catch (error) {
268
+ console.error(
269
+ `Error in render function for <${tagName}> fun element: `,
270
+ error,
271
+ )
272
+ }
273
+ this.#watcher.watch()
274
+ })
275
+ })
256
276
 
257
- renderTemplate()
277
+ this.#watcher.watch(this.#template)
278
+
279
+ renderTemplate()
280
+ } else if (setupResult === undefined) {
281
+ } else if (typeof setupResult === 'string') {
282
+ target.innerHTML = setupResult
283
+ } else if (typeof setupResult === 'object' && '_$litType$' in (setupResult as object)) {
284
+ render(setupResult, target)
285
+ } else {
286
+ console.error(
287
+ `Setup function for <${tagName}> returned an unexpected value. ` +
288
+ `Expected a render function, a template (html\`...\`), a string, or nothing. ` +
289
+ `Got: ${typeof setupResult}`
290
+ )
291
+ }
258
292
  }
259
293
 
260
294
  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
- )
295
+ if (this.#watcher && this.#template) {
296
+ this.#watcher.watch(this.#template)
297
+ }
298
+
299
+ this.#effects.forEach(entry => {
300
+ try {
301
+ entry.computed = new Signal.Computed(() => entry.fn())
302
+
303
+ entry.watcher = new Signal.subtle.Watcher(() => {
304
+ queueMicrotask(() => {
305
+ if (typeof entry.cleanup === 'function') {
306
+ entry.cleanup()
307
+ }
308
+
309
+ entry.computed = new Signal.Computed(() => entry.fn())
310
+
311
+ try {
312
+ entry.cleanup = entry.computed.get()
313
+ } catch (error) {
314
+ console.error(
315
+ `Error in effect for <${elementName}> fun element: `,
316
+ error
317
+ )
318
+ }
319
+
320
+ entry.watcher.watch(entry.computed)
321
+ })
322
+ })
323
+
324
+ entry.cleanup = entry.computed.get()
325
+ entry.watcher.watch(entry.computed)
326
+ } catch (error) {
327
+ console.error(
328
+ `Error in effect for <${elementName}> fun element: `,
329
+ error
330
+ )
331
+ }
332
+ })
267
333
  }
268
334
 
269
335
  disconnectedCallback() {
270
- this.#watcher.unwatch(this.#template)
271
- this.#cleanups.forEach(cleanup => cleanup())
272
- this.#cleanups = []
336
+ if (this.#watcher && this.#template) {
337
+ this.#watcher.unwatch(this.#template)
338
+ }
339
+
340
+ // Clean up all effects and stop watching
341
+ this.#effects.forEach(effectEntry => {
342
+ effectEntry.watcher.unwatch(effectEntry.computed)
343
+ if (typeof effectEntry.cleanup === 'function') {
344
+ effectEntry.cleanup()
345
+ }
346
+ effectEntry.cleanup = undefined
347
+ })
273
348
  }
274
349
 
275
350
  attributeChangedCallback(name: string, oldValue: string | null, newValue: string | null) {