jq79 0.4.1 → 0.4.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": "jq79",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Mini reactive component library: single-file components, Svelte-style setup scripts, fine-grained proxy reactivity. Single-file build, zero dependencies.",
5
5
  "keywords": [
6
6
  "reactive",
package/src/jq79.ts CHANGED
@@ -91,7 +91,7 @@ const interpolate = (template: string, scope: Record<string, any>): string =>
91
91
  template.replace(/{{\s*([\s\S]+?)\s*}}/g, (_, expr) => evalExpr(expr, scope) ?? "")
92
92
 
93
93
 
94
- const CONTROL_ATTRS = new Set([":attrs", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html"])
94
+ const CONTROL_ATTRS = new Set([":attrs", ":class", ":if", ":elseif", ":else", ":each", ":key", ":with", ":text", ":html"])
95
95
  // `item in items`, `item, i in items`, `(value, key) in props` - the second
96
96
  // binding is the array index or the object key, parens optional (Vue-style).
97
97
  // The list expression can span lines, so it matches [\s\S] rather than `.`
@@ -294,6 +294,20 @@ const createWithScope = (expr: string, scope: Record<string, any>): Record<strin
294
294
  })
295
295
  }
296
296
 
297
+ // what :class accepts, flattened to single class tokens: a string of
298
+ // space-separated names, an array (entries normalized recursively), or an
299
+ // object whose truthy-valued keys are the names (a key may itself hold
300
+ // several). Everything else - null, false, numbers - contributes nothing, so
301
+ // `cond && 'active'` reads naturally. The object form reads each value, so a
302
+ // store-backed flag is tracked per key
303
+ const classNames = (value: any): string[] => {
304
+ if (typeof value === "string") return value.split(/\s+/).filter(Boolean)
305
+ if (Array.isArray(value)) return value.flatMap(classNames)
306
+ if (value !== null && typeof value === "object")
307
+ return Object.entries(value).flatMap(([name, on]) => (on ? classNames(name) : []))
308
+ return []
309
+ }
310
+
297
311
  // renders a single element node: static attrs, @event listeners, a reactive
298
312
  // :attrs object, and its content - :text/:html override the element's own
299
313
  // children with a reactive textContent/innerHTML, otherwise children render
@@ -353,6 +367,25 @@ const renderNode = (node: TemplateNode, outerScope: Record<string, any>, fx: Eff
353
367
  })
354
368
  }
355
369
 
370
+ // :class="expr" adds classes on top of the static `class` attribute. Only
371
+ // classes this binding added are ever removed: the static list survives
372
+ // every re-run, even when the expression names one of its classes and then
373
+ // drops it (class="btn" :class="{ btn: cond }" keeps btn on false)
374
+ const classExpr = node.attrs[":class"]
375
+ if (classExpr !== undefined) {
376
+ const staticClasses = new Set(classNames(node.attrs.class ?? ""))
377
+ let bound: string[] = []
378
+
379
+ fx.effect(() => {
380
+ const next = classNames(evalExpr(classExpr, scope))
381
+ bound.forEach(name => {
382
+ if (!next.includes(name) && !staticClasses.has(name)) el.classList.remove(name)
383
+ })
384
+ el.classList.add(...next)
385
+ bound = next
386
+ })
387
+ }
388
+
356
389
  // :text="expr" sets textContent reactively, replacing any children.
357
390
  // :html="expr" sets innerHTML reactively, sanitizing the value first so
358
391
  // untrusted content can't inject scripts/attributes (see sanitizeHTML in