a post-component library for building user-interfaces on the web.
1import { html } from 'dhtml'
2import { attr, on, type Directive } from 'dhtml/client'
3import { assert, assert_deep_eq, assert_eq, test } from '../../../scripts/test/test.ts'
4import { setup } from './setup.ts'
5
6test('directive functions work correctly', () => {
7 const { root, el } = setup()
8
9 const redifier: Directive = node => {
10 if (!(node instanceof HTMLElement)) throw new Error('expected HTMLElement')
11 node.style.color = 'red'
12 return () => {
13 node.style.color = ''
14 }
15 }
16 const flipper: Directive = node => {
17 if (!(node instanceof HTMLElement)) throw new Error('expected HTMLElement')
18 node.style.transform = 'scaleX(-1)'
19 return () => {
20 node.style.transform = ''
21 }
22 }
23
24 const template = (d: Directive | null) => html`<div ${d}>Hello, world!</div>`
25
26 root.render(template(redifier))
27 const div = el.querySelector('div')
28 assert(div)
29 assert_eq(div.style.cssText, 'color: red;')
30
31 root.render(template(flipper))
32 assert_eq(div.style.cssText, 'transform: scaleX(-1);')
33
34 root.render(template(null))
35 assert_eq(div.style.cssText, '')
36
37 root.render(null)
38})
39
40test('directive functions with values work correctly', () => {
41 const { root, el } = setup()
42
43 function classes(value: string[]): Directive {
44 const values = value.filter(Boolean)
45 return node => {
46 node.classList.add(...values)
47 return () => {
48 node.classList.remove(...values)
49 }
50 }
51 }
52
53 const template = (c: string[]) => html`<div class="foo" ${classes(c)}>Hello, world!</div>`
54
55 root.render(template(['a', 'b']))
56 const div = el.querySelector('div')
57 assert(div)
58 assert_eq(div.className, 'foo a b')
59
60 root.render(template(['c', 'd']))
61 assert_eq(div.className, 'foo c d')
62
63 root.render(template([]))
64 assert_eq(div.className, 'foo')
65})
66
67test('attr directive works correctly', () => {
68 const { root, el } = setup()
69
70 const template = (value: string | null) => html`
71 <input id="attr-works-input"></input>
72 <label ${attr('for', value)}>Hello, world!</label>
73 `
74
75 root.render(template('attr-works-input'))
76 assert_eq(el.querySelector('label')!.htmlFor, 'attr-works-input')
77
78 root.render(template('updated'))
79 assert_eq(el.querySelector('label')!.htmlFor, 'updated')
80
81 root.render(template(null))
82 assert_eq(el.querySelector('label')!.htmlFor, '')
83})
84
85test('attr directive supports booleans', () => {
86 const { root, el } = setup()
87
88 const template = (value: boolean) => html`<input ${attr('disabled', value)} />`
89
90 root.render(template(true))
91 assert_eq(el.querySelector('input')!.disabled, true)
92
93 root.render(template(false))
94 assert_eq(el.querySelector('input')!.disabled, false)
95})
96
97test('on directive works correctly', () => {
98 const { root, el } = setup()
99 let count = 0
100 let event: Event
101
102 const template = (handler: EventListener | null) => html`
103 <button ${handler ? on('click', handler) : null}>Click me</button>
104 `
105
106 root.render(
107 template(e => {
108 count++
109 event = e
110 }),
111 )
112 const button = el.querySelector('button')!
113
114 button.click()
115 assert_eq(count, 1)
116 assert(event! instanceof Event)
117 assert_eq(event.type, 'click')
118
119 button.click()
120 assert_eq(count, 2)
121
122 root.render(template(null))
123 button.click()
124 assert_eq(count, 2)
125})
126
127test('on directive handles event listener updates', () => {
128 const { root, el } = setup()
129 let count1 = 0
130 let count2 = 0
131
132 const template = (handler: EventListener) => html`<button ${on('click', handler)}>Click me</button>`
133
134 root.render(
135 template(() => {
136 count1++
137 }),
138 )
139 const button = el.querySelector('button')!
140
141 button.click()
142 assert_eq(count1, 1)
143 assert_eq(count2, 0)
144
145 root.render(
146 template(() => {
147 count2++
148 }),
149 )
150 button.click()
151 assert_eq(count1, 1)
152 assert_eq(count2, 1)
153})
154
155test('on directive supports different event types', () => {
156 const { root, el } = setup()
157 let enter_count = 0
158 let leave_count = 0
159
160 const template = () => html`
161 <div
162 ${on('mouseenter', () => {
163 enter_count++
164 })}
165 ${on('mouseleave', () => {
166 leave_count++
167 })}
168 >
169 Hover me
170 </div>
171 `
172
173 root.render(template())
174 const div = el.querySelector('div')!
175
176 div.dispatchEvent(new MouseEvent('mouseenter'))
177 assert_eq(enter_count, 1)
178 assert_eq(leave_count, 0)
179
180 div.dispatchEvent(new MouseEvent('mouseleave'))
181 assert_eq(enter_count, 1)
182 assert_eq(leave_count, 1)
183})
184
185test('on directive supports event listener options', () => {
186 const { root, el } = setup()
187 let count = 0
188
189 const template = (options?: AddEventListenerOptions) => html`
190 <button
191 ${on(
192 'click',
193 () => {
194 count++
195 },
196 options,
197 )}
198 >
199 Click me
200 </button>
201 `
202
203 root.render(template({ once: true }))
204 const button = el.querySelector('button')!
205
206 button.click()
207 assert_eq(count, 1)
208
209 button.click()
210 assert_eq(count, 1)
211
212 root.render(template())
213 button.click()
214 assert_eq(count, 2)
215
216 button.click()
217 assert_eq(count, 3)
218})
219
220test('same directive function is not re-invoked or cleaned up', () => {
221 const { root } = setup()
222
223 const sequence: string[] = []
224 const stable = () => {
225 sequence.push('stable create')
226 return () => sequence.push('stable cleanup')
227 }
228 const unstable = () => () => {
229 sequence.push('unstable create')
230 return () => sequence.push('unstable cleanup')
231 }
232
233 const template = (d1: Directive | null, d2: Directive | null) => html`<div ${d1} ${d2}></div>`
234
235 root.render(template(stable, unstable()))
236 assert_deep_eq(sequence, ['stable create', 'unstable create'])
237 sequence.length = 0
238
239 root.render(template(stable, unstable()))
240 assert_deep_eq(sequence, ['unstable cleanup', 'unstable create'])
241 sequence.length = 0
242
243 root.render(template(null, null))
244 assert_deep_eq(sequence, ['stable cleanup', 'unstable cleanup'])
245})