All files / bind / mod.ts

100.00% Branches 38/38
100.00% Lines 84/84
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
 
x8
x8
x8
 
 
 
x8
x8
x8
x8
x8
x8
x8
x76
x76
x76
x8
x8
x94
x95
x95
x94
x411
 
 
x94
x94
x97
x97
x97
x504
x97
x98
x98
x97
 
 
x94
x223
x349
 
x460
x237
x237
x711
x258
x267
x258
x280
x280
x237
x237
x237
 
x463
x240
x240
x720
x264
x277
x299
x299
x299
x299
x299
x304
x304
x277
x264
x284
x284
x240
x240
x241
x241
x240
x240
 
x223
x307
x307
 
x223
x237
x239
x239
x239
x249
x223
x223
x94
x8
 
 
x8
 
 
x8
 
 
x8



































































































// Imports
import { type Cache, type Directive, Phase } from "@mizu/internal/engine"
import { toCamelCase } from "@std/text"
import { boolean } from "./boolean.ts"
export type * from "@mizu/internal/engine"

/** `:bind` directive. */
export const _bind = {
  name: /^:(?!:)(?<attribute>)/,
  prefix: ":",
  phase: Phase.ATTRIBUTE,
  multiple: true,
  default: "$<attribute>",
  init(renderer) {
    if (!renderer.cache(this.name)) {
      renderer.cache<Cache<typeof _bind>>(this.name, new WeakMap())
    }
  },
  async execute(renderer, element, { attributes, cache, ...options }) {
    if (!renderer.isHtmlElement(element)) {
      return
    }
    const cached = cache.get(element) ?? cache.set(element, {}).get(element)!
    const parsed = attributes.map((attribute) => renderer.parseAttribute(attribute, null, { prefix: this.prefix }))

    // Handle shorthand attributes binding
    const shorthand = parsed.findIndex(({ name }) => !name.length)
    if (~shorthand) {
      const [attribute] = parsed.splice(shorthand, 1)
      const value = await renderer.evaluate(element, attribute.value, options)
      if (typeof value === "object") {
        parsed.unshift(...Object.entries(value ?? {}).map(([name, value]) => ({ ...attribute, name, value })))
      } else {
        renderer.warn(`[${this.name}] empty shorthand expects an object but got ${typeof value}, ignoring`, element)
      }
    }

    // Bind attributes
    for (const { name, value: expression, attribute } of parsed) {
      const value = attribute.name === ":" ? expression : await renderer.evaluate(element, expression || toCamelCase(name), options)
      switch (true) {
        // Class attributes
        case name === "class": {
          cached.class ??= element.getAttribute("class") ?? ""
          element.setAttribute("class", `${cached.class}`)
          ;[value].flat(Infinity).forEach((classlist) => {
            if ((typeof classlist === "object") && classlist) {
              Object.entries(classlist).forEach(([name, value]) => value && element.classList.add(name))
            } else if (typeof classlist === "string") {
              element.classList.add(...classlist.split(" ").map((name) => name.trim()).filter(Boolean))
            }
          })
          break
        }
        // Style attributes
        case name === "style": {
          cached.style ??= element.getAttribute("style") ?? ""
          element.setAttribute("style", `${cached.style}`)
          ;[value].flat(Infinity).forEach((style) => {
            if ((typeof style === "object") && style) {
              Object.entries(style).forEach(([name, value]) => {
                const property = name.startsWith("--") ? name : name.replace(/([a-z])([A-Z])/g, "$1-$2")
                const [match = "", priority = ""] = `${value}`.match(/\s!(important)[\s;]*$/) ?? []
                const style = `${value}`.replace(match, "")
                element.style.setProperty(property, style, priority)
                if ((typeof value === "number") && (!element.style.getPropertyValue(property))) {
                  element.style.setProperty(property, `${value}px`)
                }
              })
            } else if (typeof style === "string") {
              element.style.cssText += style
            }
          })
          if (!element.style.length) {
            element.removeAttribute("style")
          }
          break
        }
        // Boolean attributes
        case boolean(element.tagName, name):
          element.toggleAttribute(name, Boolean(value))
          break
        // Generic attributes
        default:
          if ((value === undefined) || (value === null)) {
            element.removeAttribute(name)
            break
          }
          element.setAttribute(name, `${value}`)
      }
    }
  },
} as Directive<WeakMap<HTMLElement, { class?: string; style?: string }>>

/** `:class` directive. */
export const _bind_class = _bind as typeof _bind

/** `:style` directive. */
export const _bind_style = _bind as typeof _bind

/** Default exports. */
export default _bind