a post-component library for building user-interfaces on the web.

split child_part update into multiple smaller functions

tombl.dev baf2d04d 8e8f2cd8

verified
+118 -109
+113 -104
src/client/parts.ts
··· 1 1 import type { Displayable, Renderable } from 'dhtml' 2 - import { assert, is_html, is_iterable, is_renderable, single_part_template } from '../shared.ts' 2 + import { assert, is_html, is_iterable, is_renderable, single_part_template, type ToString } from '../shared.ts' 3 3 import { delete_controller, get_controller, get_key } from './controller.ts' 4 4 import { create_root, create_root_after, type Root } from './root.ts' 5 5 import { create_span, delete_contents, extract_contents, insert_node, type Span } from './span.ts' ··· 53 53 span = create_span(child) 54 54 } 55 55 56 - return function update(value) { 56 + function render_renderable(renderable: Renderable): Displayable { 57 + const controller = get_controller(renderable) 58 + 59 + controller._invalidate ??= () => { 60 + assert(current_renderable === renderable, 'could not invalidate an outdated renderable') 61 + update(renderable) 62 + } 63 + controller._parent_node = span._parent 64 + 65 + try { 66 + return renderable.render() 67 + } catch (thrown) { 68 + if (is_html(thrown)) { 69 + return thrown 70 + } else { 71 + throw thrown 72 + } 73 + } 74 + } 75 + 76 + function handle_iterable(values: Iterable<Displayable>) { 77 + if (!roots) { 78 + // we previously rendered a single value, so we need to clear it. 79 + disconnect_root() 80 + delete_contents(span) 81 + 82 + roots = [] 83 + } 84 + 85 + // create or update a root for every item. 86 + let i = 0 87 + let end = span._start 88 + for (const item of values) { 89 + const key = get_key(item) 90 + let root = (roots[i] ??= create_root_after(end)) 91 + 92 + if (key !== undefined && root._key !== key) { 93 + const j = roots.findIndex(r => r._key === key) 94 + root._key = key 95 + if (j !== -1) { 96 + const root1 = root 97 + const root2 = roots[j] 98 + 99 + // swap the contents of the spans 100 + const tmp_content = extract_contents(root1._span) 101 + insert_node(root1._span, extract_contents(root2._span)) 102 + insert_node(root2._span, tmp_content) 103 + 104 + // swap the spans back 105 + const tmp_span = root1._span 106 + root1._span = root2._span 107 + root2._span = tmp_span 108 + 109 + // swap the roots 110 + roots[j] = root1 111 + root = roots[i] = root2 112 + } 113 + } 114 + 115 + root.render(item) 116 + end = root._span._end 117 + i++ 118 + } 119 + 120 + // and now remove excess roots if the iterable has shrunk. 121 + while (roots.length > i) { 122 + const root = roots.pop() 123 + assert(root) 124 + root.detach() 125 + delete_contents(root._span) 126 + } 127 + 128 + span._end = end 129 + } 130 + 131 + function handle_single_value(value: Node | ToString | null) { 132 + if (old_value != null && value !== null && !(old_value instanceof Node) && !(value instanceof Node)) { 133 + // we previously rendered a string, and we're rendering a string again. 134 + assert(span._start === span._end && span._start instanceof Text) 135 + span._start.data = '' + value 136 + } else { 137 + delete_contents(span) 138 + if (value !== null) insert_node(span, value instanceof Node ? value : new Text(value satisfies ToString as string)) 139 + } 140 + } 141 + 142 + function update(value_: unknown) { 143 + let value = value_ as Displayable 144 + 57 145 assert(span) 58 146 const ends_were_equal = span._parent === parent_span._parent && span._end === parent_span._end 59 147 60 148 if (is_renderable(value)) { 61 149 switch_renderable(value) 62 150 63 - const renderable = value 64 - const controller = get_controller(renderable) 65 - 66 - controller._invalidate ??= () => { 67 - assert(current_renderable === renderable, 'could not invalidate an outdated renderable') 68 - update(renderable) 69 - } 70 - controller._parent_node = span._parent 71 - 72 - try { 73 - value = renderable.render() 74 - } catch (thrown) { 75 - if (is_html(thrown)) { 76 - value = thrown 77 - } else { 78 - throw thrown 79 - } 80 - } 151 + value = render_renderable(value) 81 152 82 153 // if render returned another renderable, we want to track/cache both renderables individually. 83 154 // wrap it in a nested ChildPart so that each can be tracked without ChildPart having to handle multiple renderables. 84 155 if (is_renderable(value)) value = single_part_template(value) 85 - } else switch_renderable(null) 156 + } else { 157 + switch_renderable(null) 158 + } 86 159 87 160 // if it's undefined, swap the value for null. 88 161 // this means if the initial value is undefined, ··· 93 166 // NOTE: we're explicitly not caching/diffing the value when it's an iterable, 94 167 // given it can yield different values but have the same identity. (e.g. arrays) 95 168 if (is_iterable(value)) { 96 - if (!roots) { 97 - // we previously rendered a single value, so we need to clear it. 98 - disconnect_root() 99 - delete_contents(span) 100 - 101 - roots = [] 169 + handle_iterable(value as Iterable<Displayable>) 170 + } else { 171 + if (roots) { 172 + for (const root of roots) root.detach() 173 + roots = undefined 102 174 } 103 175 104 - // create or update a root for every item. 105 - let i = 0 106 - let end = span._start 107 - for (const item of value) { 108 - const key = get_key(item) 109 - let root = (roots[i] ??= create_root_after(end)) 110 - 111 - if (key !== undefined && root._key !== key) { 112 - const j = roots.findIndex(r => r._key === key) 113 - root._key = key 114 - if (j !== -1) { 115 - const root1 = root 116 - const root2 = roots[j] 176 + // only continue if the value hasn't changed. 177 + if (!Object.is(old_value, value)) { 178 + if (is_html(value)) { 179 + root ??= create_root(span) 180 + root.render(value) // root.render will detach the previous tree if the template has changed. 181 + } else { 182 + // if we previously rendered a tree that might contain renderables, 183 + // and the template has changed (or we're not even rendering a template anymore), 184 + // we need to clear the old renderables. 185 + disconnect_root() 117 186 118 - // swap the contents of the spans 119 - const tmp_content = extract_contents(root1._span) 120 - insert_node(root1._span, extract_contents(root2._span)) 121 - insert_node(root2._span, tmp_content) 122 - 123 - // swap the spans back 124 - const tmp_span = root1._span 125 - root1._span = root2._span 126 - root2._span = tmp_span 127 - 128 - // swap the roots 129 - roots[j] = root1 130 - root = roots[i] = root2 131 - } 187 + handle_single_value(value) 132 188 } 133 189 134 - root.render(item as Displayable) 135 - end = root._span._end 136 - i++ 137 - } 138 - 139 - // and now remove excess roots if the iterable has shrunk. 140 - while (roots.length > i) { 141 - const root = roots.pop() 142 - assert(root) 143 - root.detach() 144 - delete_contents(root._span) 145 - } 146 - 147 - span._end = end 148 - 149 - if (current_renderable) { 150 - const controller = get_controller(current_renderable) 151 - controller._mount_callbacks?.forEach(callback => controller._unmount_callbacks.add(callback?.())) 152 - delete controller._mount_callbacks 153 - } 154 - 155 - if (ends_were_equal) parent_span._end = span._end 156 - 157 - return 158 - } else if (roots) { 159 - for (const root of roots) root.detach() 160 - roots = undefined 161 - } 162 - 163 - // now early return if the value hasn't changed. 164 - if (Object.is(old_value, value)) return 165 - 166 - if (is_html(value)) { 167 - root ??= create_root(span) 168 - root.render(value) // root.render will detach the previous tree if the template has changed. 169 - } else { 170 - // if we previously rendered a tree that might contain renderables, 171 - // and the template has changed (or we're not even rendering a template anymore), 172 - // we need to clear the old renderables. 173 - disconnect_root() 174 - 175 - if (old_value != null && value !== null && !(old_value instanceof Node) && !(value instanceof Node)) { 176 - // we previously rendered a string, and we're rendering a string again. 177 - assert(span._start === span._end && span._start instanceof Text) 178 - span._start.data = '' + value 179 - } else { 180 - delete_contents(span) 181 - if (value !== null) insert_node(span, value instanceof Node ? value : new Text('' + value)) 190 + old_value = value 182 191 } 183 192 } 184 - 185 - old_value = value 186 193 187 194 if (current_renderable) { 188 195 const controller = get_controller(current_renderable) ··· 192 199 193 200 if (ends_were_equal) parent_span._end = span._end 194 201 } 202 + 203 + return update 195 204 } 196 205 197 206 export function create_property_part(node: Node, name: string): Part {
+1 -5
src/index.ts
··· 1 - import { html_tag } from './shared.ts' 2 - 3 - interface ToString { 4 - toString(): string 5 - } 1 + import { html_tag, type ToString } from './shared.ts' 6 2 7 3 export type Displayable = null | undefined | ToString | Node | Renderable | Iterable<Displayable> | HTML 8 4 export interface Renderable {
+4
src/shared.ts
··· 25 25 export function single_part_template(part: Displayable): HTML { 26 26 return html`${part}` 27 27 } 28 + 29 + export interface ToString { 30 + toString(): string 31 + }