a post-component library for building user-interfaces on the web.
at main 328 lines 9.8 kB view raw
1import { 2 assert, 3 is_html, 4 is_iterable, 5 is_keyed, 6 is_renderable, 7 single_part_template, 8 type Displayable, 9 type Key, 10 type Renderable, 11} from '../shared.ts' 12import { 13 compile_template, 14 PART_ATTRIBUTE, 15 PART_CHILD, 16 PART_DIRECTIVE, 17 PART_PROPERTY, 18 type CompiledTemplate, 19} from './compiler.ts' 20import { controllers, get_controller } from './controller.ts' 21import { create_span_after, delete_contents, extract_contents, insert_node, type Span } from './span.ts' 22import type { Cleanup } from './util.ts' 23 24export type Part = (value: unknown) => void 25 26export function create_child_part( 27 span: Span, 28 29 // for when we're rendering a renderable: 30 needs_revalidate = true, 31 current_renderable?: Renderable, 32 33 // for when we're rendering a template: 34 old_template?: CompiledTemplate, 35 template_parts?: [number, Part][], 36 37 // for when we're rendering multiple values: 38 entries?: Array<{ _span: Span; _part: Part; _key: Key }>, 39 40 // for when we're rendering a string/single dom node: 41 // undefined means no previous value, because a user-specified undefined is remapped to null 42 old_value?: unknown, 43): Part { 44 function switch_renderable(next: Renderable | undefined) { 45 if (current_renderable && current_renderable !== next) { 46 const controller = controllers.get(current_renderable) 47 if (controller) { 48 controller._invalidate.delete(switch_renderable) 49 50 // If this was the last instance, call unmount callbacks 51 if (!controller._invalidate.size) { 52 controller._unmount_callbacks.forEach(callback => callback?.()) 53 controller._unmount_callbacks.length = 0 54 } 55 } 56 } 57 current_renderable = next 58 } 59 60 function disconnect_root() { 61 if (template_parts !== undefined) { 62 for (const [, part] of template_parts) part(null) 63 old_template = undefined 64 template_parts = undefined 65 } 66 } 67 68 return function update(value) { 69 if (is_renderable(value)) { 70 if (!needs_revalidate && value === current_renderable) return 71 needs_revalidate = false 72 73 switch_renderable(value) 74 75 const renderable = value 76 const controller = get_controller(renderable) 77 // If this is the first mounted instance, call mount callbacks 78 if (!controller._invalidate.size) { 79 controller._unmount_callbacks = controller._mount_callbacks.map(callback => callback()) 80 } 81 controller._invalidate.set(switch_renderable, () => { 82 assert(renderable === current_renderable) 83 needs_revalidate = true 84 update(renderable) 85 }) 86 87 try { 88 value = renderable.render() 89 } catch (thrown) { 90 if (is_html(thrown)) { 91 value = thrown 92 } else { 93 throw thrown 94 } 95 } 96 97 // if render returned another renderable, we want to track/cache both renderables individually. 98 // wrap it in a nested ChildPart so that each can be tracked without ChildPart having to handle multiple renderables. 99 if (is_renderable(value)) value = single_part_template(value) 100 } else switch_renderable(undefined) 101 102 // if it's undefined, swap the value for null. 103 // this means if the initial value is undefined, 104 // it won't conflict with old_value's default of undefined, 105 // so it'll still render. 106 if (value === undefined) value = null 107 108 // NOTE: we're explicitly not caching/diffing the value when it's an iterable, 109 // given it can yield different values but have the same identity. (e.g. arrays) 110 if (is_iterable(value)) { 111 if (!entries) { 112 // we previously rendered a single value, so we need to clear it. 113 disconnect_root() 114 delete_contents(span) 115 entries = [] 116 } 117 118 // create or update a root for every item. 119 let i = 0 120 let end = span._start 121 for (const item of value) { 122 const key = is_keyed(item) ? item._key : (item as Key) 123 if (entries.length <= i) { 124 const span = create_span_after(end) 125 entries[i] = { _span: span, _part: create_child_part(span), _key: key } 126 } 127 128 if (key !== undefined && entries[i]._key !== key) { 129 for (let j = i + 1; j < entries.length; j++) { 130 const entry1 = entries[i] 131 const entry2 = entries[j] 132 133 if (entry2._key === key) { 134 // swap the contents of the spans 135 const tmp_content = extract_contents(entry1._span) 136 insert_node(entry1._span, extract_contents(entry2._span)) 137 insert_node(entry2._span, tmp_content) 138 139 // swap the spans back 140 const tmp_span = { ...entry1._span } 141 Object.assign(entry1._span, entry2._span) 142 Object.assign(entry2._span, tmp_span) 143 144 // swap the roots 145 entries[j] = entry1 146 entries[i] = entry2 147 148 break 149 } 150 } 151 152 entries[i]._key = key 153 } 154 155 entries[i]._part(item as Displayable) 156 end = entries[i]._span._end 157 i++ 158 } 159 160 // and now remove excess parts if the iterable has shrunk. 161 while (entries.length > i) { 162 const entry = entries.pop() 163 assert(entry) 164 entry._part(null) 165 } 166 167 old_value = undefined 168 return 169 } else if (entries) { 170 for (const entry of entries) entry._part(null) 171 entries = undefined 172 } 173 174 if (is_html(value)) { 175 const { _dynamics: dynamics, _statics: statics } = value 176 const template = compile_template(statics) 177 178 assert( 179 template._parts.length === dynamics.length, 180 'expected the same number of dynamics as parts. do you have a ${...} in an unsupported place?', 181 ) 182 183 if (old_template !== template) { 184 if (template_parts !== undefined) { 185 // scan through all the parts of the previous tree, and clear any renderables. 186 for (const [_idx, part] of template_parts) part(null) 187 template_parts = undefined 188 } 189 190 old_template = template 191 192 const doc = old_template._content.cloneNode(true) as DocumentFragment 193 194 const node_by_part: Array<Node | Span> = [] 195 196 for (const node of doc.querySelectorAll('[data-dynparts]')) { 197 const parts = node.getAttribute('data-dynparts') 198 assert(parts) 199 node.removeAttribute('data-dynparts') 200 for (const part of parts.split(' ')) node_by_part[+part] = node 201 } 202 203 for (const part of old_template._root_parts) node_by_part[part] = span 204 205 // the fragment must be inserted before the parts are constructed, 206 // because they need to know their final location. 207 // this also ensures that custom elements are upgraded before we do things 208 // to them, like setting properties or attributes. 209 delete_contents(span) 210 insert_node(span, doc) 211 212 template_parts = template._parts.map(([dynamic_index, [type, data]], element_index): [number, Part] => { 213 const node = node_by_part[element_index] 214 switch (type) { 215 case PART_CHILD: 216 let child: ChildNode | null 217 218 if (node instanceof Node) { 219 child = node.childNodes[data] 220 assert(child) 221 } else { 222 child = node._start.nextSibling 223 assert(child) 224 for (let i = 0; i < data; i++) { 225 child = child.nextSibling 226 assert(child !== null, 'expected more siblings') 227 assert(child !== node._end, 'ran out of siblings before the end') 228 } 229 } 230 231 assert(child.parentNode && child.previousSibling && child.nextSibling) 232 233 return [ 234 dynamic_index, 235 create_child_part({ 236 _start: child.previousSibling, 237 _end: child.nextSibling, 238 }), 239 ] 240 case PART_DIRECTIVE: 241 assert(node instanceof Node) 242 return [dynamic_index, create_directive_part(node)] 243 case PART_ATTRIBUTE: 244 assert(node instanceof Element) 245 return [dynamic_index, create_attribute_part(node, data)] 246 case PART_PROPERTY: 247 assert(node instanceof Node) 248 return [dynamic_index, create_property_part(node, data)] 249 } 250 }) 251 } 252 253 assert(template_parts) 254 for (const [idx, part] of template_parts) part(dynamics[idx]) 255 256 old_value = undefined 257 return 258 } 259 260 if (!Object.is(old_value, value)) { 261 // if we previously rendered a tree that might contain renderables, 262 // and the template has changed (or we're not even rendering a template anymore), 263 // we need to clear the old renderables. 264 disconnect_root() 265 266 if (old_value != null && value !== null && !(old_value instanceof Node) && !(value instanceof Node)) { 267 // we previously rendered a string, and we're rendering a string again. 268 assert(span._start.nextSibling?.nextSibling === span._end && span._start.nextSibling instanceof Text) 269 span._start.nextSibling.data = '' + value 270 } else { 271 delete_contents(span) 272 if (value !== null) insert_node(span, value instanceof Node ? value : new Text('' + value)) 273 } 274 275 old_value = value 276 } 277 } 278} 279 280export function create_property_part(node: Node, name: string): Part { 281 return value => { 282 // @ts-expect-error 283 node[name] = value 284 } 285} 286 287export function create_attribute_part(node: Element, name: string): Part { 288 return value => set_attr(node, name, value) 289} 290 291export type Directive = (el: Element) => Cleanup 292 293export function create_directive_part(node: Node): Part { 294 let cleanup: Cleanup 295 let prev_fn: unknown 296 return fn => { 297 if (prev_fn === fn) return 298 assert(typeof fn === 'function' || fn == null) 299 cleanup?.() 300 cleanup = fn?.(node) 301 prev_fn = fn 302 } 303} 304 305function set_attr(el: Element, name: string, value: unknown) { 306 if (typeof value === 'boolean') el.toggleAttribute(name, value) 307 else if (value == null) el.removeAttribute(name) 308 // the cast is fine because setAttribute implicitly casts the value to a string 309 else el.setAttribute(name, value as string) 310} 311 312export function attr_directive(name: string, value: string | boolean | null | undefined): Directive { 313 return el => { 314 set_attr(el, name, value) 315 return () => set_attr(el, name, null) 316 } 317} 318 319export function on_directive( 320 type: string, 321 listener: EventListenerOrEventListenerObject, 322 options?: boolean | AddEventListenerOptions, 323): Directive { 324 return el => { 325 el.addEventListener(type, listener, options) 326 return () => el.removeEventListener(type, listener, options) 327 } 328}