@pfern/elements 0.1.7 → 0.1.8

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/elements.js CHANGED
@@ -24,9 +24,10 @@ const svgNS = 'http://www.w3.org/2000/svg'
24
24
  */
25
25
  const rootMap = new WeakMap()
26
26
 
27
- const isNodeEnv = typeof document === 'undefined'
27
+ const isNodeEnv = () => typeof document === 'undefined'
28
28
 
29
29
  let componentUpdateDepth = 0
30
+ let currentEventRoot = null
30
31
 
31
32
  /**
32
33
  * Determines whether two nodes have changed enough to require replacement.
@@ -99,6 +100,8 @@ const assignProperties = (el, props) =>
99
100
  while (target && !target.__root) target = target.parentNode
100
101
  if (!target) return
101
102
 
103
+ const prevEventRoot = currentEventRoot
104
+ currentEventRoot = target
102
105
  try {
103
106
  const event = args[0]
104
107
  const isFormEvent = /^(oninput|onsubmit|onchange)$/.test(key)
@@ -142,6 +145,8 @@ const assignProperties = (el, props) =>
142
145
  }
143
146
  } catch (error) {
144
147
  console.error(error)
148
+ } finally {
149
+ currentEventRoot = prevEventRoot
145
150
  }
146
151
  }
147
152
  } else if (key === 'style' && typeof value === 'object') {
@@ -174,7 +179,7 @@ const assignProperties = (el, props) =>
174
179
  */
175
180
  const renderTree = (node, isRoot = true) => {
176
181
  if (typeof node === 'string' || typeof node === 'number') {
177
- return isNodeEnv ? node : document.createTextNode(node)
182
+ return isNodeEnv() ? node : document.createTextNode(node)
178
183
  }
179
184
 
180
185
  if (!node || node.length === 0) {
@@ -297,6 +302,34 @@ export const render = (vtree, container = null) => {
297
302
  rootMap.set(vtree, target)
298
303
  }
299
304
 
305
+ /**
306
+ * Updates the browser URL via the History API (no full page load).
307
+ * No-ops outside the browser.
308
+ *
309
+ * @param {string} to - The target URL (path/search/hash).
310
+ * @param {Object} [options]
311
+ * @param {boolean} [options.replace=false] - Use replaceState instead of pushState.
312
+ * @param {boolean} [options.force=false] - Update even if URL is already `to`.
313
+ * @param {any} [options.state={}] - History state.
314
+ * @param {string} [options.title=''] - History title (mostly ignored by browsers).
315
+ */
316
+ export const navigate = (to, {
317
+ replace = false,
318
+ force = false,
319
+ state = {},
320
+ title = ''
321
+ } = {}) => {
322
+ if (typeof window === 'undefined') return
323
+ if (typeof to !== 'string' || !to.length) return
324
+
325
+ const { pathname, search, hash } = window.location
326
+ const current = `${pathname}${search}${hash}`
327
+ if (!force && current === to) return
328
+
329
+ const method = replace ? 'replaceState' : 'pushState'
330
+ window.history[method](state, title, to)
331
+ }
332
+
300
333
  /**
301
334
  * Wraps a function component so that it participates in reconciliation.
302
335
  *
@@ -308,11 +341,18 @@ export const component = fn => {
308
341
  return (...args) => {
309
342
  try {
310
343
  const prevEl = rootMap.get(instance)
311
- const canUpdateInPlace = !!prevEl?.parentNode && componentUpdateDepth === 0
344
+ const canUpdateInPlace =
345
+ !!prevEl?.parentNode
346
+ && componentUpdateDepth === 0
347
+ && !currentEventRoot
312
348
 
313
349
  componentUpdateDepth++
314
- const vnode = fn(...args)
315
- componentUpdateDepth--
350
+ let vnode
351
+ try {
352
+ vnode = fn(...args)
353
+ } finally {
354
+ componentUpdateDepth--
355
+ }
316
356
 
317
357
  if (canUpdateInPlace) {
318
358
  const replacement = renderTree(['wrap', { __instance: instance }, vnode], true)
@@ -322,7 +362,6 @@ export const component = fn => {
322
362
 
323
363
  return ['wrap', { __instance: instance }, vnode]
324
364
  } catch (err) {
325
- componentUpdateDepth = Math.max(0, componentUpdateDepth - 1)
326
365
  console.error('Component error:', err)
327
366
  return ['div', {}, `Error: ${err.message}`]
328
367
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pfern/elements",
3
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
4
  "description": "A minimalist, pure functional declarative UI toolkit.",
5
5
  "type": "module",
6
6
  "main": "elements.js",
@@ -12,6 +12,15 @@
12
12
  export const DEBUG: boolean;
13
13
  export function render(vtree: any, container?: any): void;
14
14
  export function component(fn: (...args: any[]) => any): (...args: any[]) => any;
15
+ export function navigate(
16
+ to: string,
17
+ options?: {
18
+ replace?: boolean;
19
+ force?: boolean;
20
+ state?: any;
21
+ title?: string;
22
+ }
23
+ ): void;
15
24
  /**
16
25
  * @typedef {Record<string, any>} Props
17
26
  * @typedef {any[] | string | number | boolean | null | undefined | Node} Child