typed-dom
Advanced tools
Comparing version 0.0.278 to 0.0.279
{ | ||
"name": "typed-dom", | ||
"version": "0.0.278", | ||
"version": "0.0.279", | ||
"description": "A value-level and type-level DOM builder.", | ||
@@ -5,0 +5,0 @@ "private": false, |
209
src/proxy.ts
@@ -5,2 +5,3 @@ import { Event } from 'spica/global'; | ||
import { identity } from './util/identity'; | ||
import { splice } from 'spica/array'; | ||
@@ -65,14 +66,3 @@ declare global { | ||
namespace privates { | ||
export const events = Symbol.for('typed-dom::events'); | ||
export const id = Symbol('id'); | ||
export const id_ = Symbol('id_'); | ||
export const query = Symbol('query'); | ||
export const query_ = Symbol('query_'); | ||
export const scope = Symbol('scope'); | ||
export const observe = Symbol('observe'); | ||
export const type = Symbol('type'); | ||
export const container = Symbol('container'); | ||
export const children = Symbol('children'); | ||
export const isInit = Symbol('isInit'); | ||
export const isObserverUpdate = Symbol('isObserverUpdate'); | ||
export const listeners = Symbol.for('typed-dom::listeners'); | ||
} | ||
@@ -94,21 +84,21 @@ | ||
) { | ||
const events = this[privates.events]; | ||
const listeners = this[privates.listeners]; | ||
assert('' != null); | ||
events.mutate = 'onmutate' in element && element['onmutate'] != null; | ||
events.connect = 'onconnect' in element && element['onconnect'] != null; | ||
events.disconnect = 'ondisconnect' in element && element['ondisconnect'] != null; | ||
this[privates.children] = children; | ||
this[privates.container] = container; | ||
listeners.mutate = 'onmutate' in element && element['onmutate'] != null; | ||
listeners.connect = 'onconnect' in element && element['onconnect'] != null; | ||
listeners.disconnect = 'ondisconnect' in element && element['ondisconnect'] != null; | ||
this.children_ = children; | ||
this.container = container; | ||
switch (true) { | ||
case children === void 0: | ||
this[privates.type] = ElChildType.Void; | ||
this.type = ElChildType.Void; | ||
break; | ||
case typeof children === 'string': | ||
this[privates.type] = ElChildType.Text | ||
this.type = ElChildType.Text | ||
break; | ||
case isArray(children): | ||
this[privates.type] = ElChildType.Array; | ||
this.type = ElChildType.Array; | ||
break; | ||
case children && typeof children === 'object': | ||
this[privates.type] = ElChildType.Struct; | ||
this.type = ElChildType.Struct; | ||
break; | ||
@@ -120,19 +110,19 @@ default: | ||
this.element[proxy] = this; | ||
switch (this[privates.type]) { | ||
switch (this.type) { | ||
case ElChildType.Void: | ||
this[privates.isInit] = false; | ||
this.isInit = false; | ||
return; | ||
case ElChildType.Text: | ||
this.children = children as El.Setter<C>; | ||
this[privates.isInit] = false; | ||
this.isInit = false; | ||
return; | ||
case ElChildType.Array: | ||
this[privates.children] = [] as El.Children.Array as C; | ||
this.children_ = [] as El.Children.Array as C; | ||
this.children = children as El.Setter<C>; | ||
this[privates.isInit] = false; | ||
this.isInit = false; | ||
return; | ||
case ElChildType.Struct: | ||
this[privates.children] = this[privates.observe](children as El.Children.Struct) as C; | ||
this.children_ = this.observe(children as El.Children.Struct) as C; | ||
this.children = children as El.Setter<C>; | ||
this[privates.isInit] = false; | ||
this.isInit = false; | ||
return; | ||
@@ -143,12 +133,20 @@ default: | ||
} | ||
private readonly [privates.events] = { | ||
private [privates.listeners] = { | ||
mutate: false, | ||
connect: false, | ||
disconnect: false, | ||
values: [] as El[], | ||
add(child: El): void { | ||
this.values.push(child); | ||
}, | ||
delete(child: El): void { | ||
assert(this.values.indexOf(child) > -1); | ||
splice(this.values, this.values.indexOf(child), 1); | ||
}, | ||
}; | ||
private [privates.id_] = ''; | ||
private get [privates.id](): string { | ||
if (this[privates.id_]) return this[privates.id_]; | ||
this[privates.id_] = this.element.id; | ||
if (/^[a-z][\w-]*$/i.test(this[privates.id_])) return this[privates.id_]; | ||
private id_ = ''; | ||
private get id(): string { | ||
if (this.id_) return this.id_; | ||
this.id_ = this.element.id; | ||
if (/^[a-z][\w-]*$/i.test(this.id_)) return this.id_; | ||
if (counter === 999) { | ||
@@ -158,20 +156,20 @@ id = identity(); | ||
} | ||
this[privates.id_] = `rnd-${id}-${++counter}`; | ||
assert(!this.element.classList.contains(this[privates.id_])); | ||
this.element.classList.add(this[privates.id_]); | ||
return this[privates.id_]; | ||
this.id_ = `rnd-${id}-${++counter}`; | ||
assert(!this.element.classList.contains(this.id_)); | ||
this.element.classList.add(this.id_); | ||
return this.id_; | ||
} | ||
private [privates.query_] = ''; | ||
private get [privates.query](): string { | ||
if (this[privates.query_]) return this[privates.query_]; | ||
private query_ = ''; | ||
private get query(): string { | ||
if (this.query_) return this.query_; | ||
switch (true) { | ||
case this.element !== this[privates.container]: | ||
return this[privates.query_] = ':host'; | ||
case this[privates.id] === this.element.id: | ||
return this[privates.query_] = `#${this[privates.id]}`; | ||
case this.element !== this.container: | ||
return this.query_ = ':host'; | ||
case this.id === this.element.id: | ||
return this.query_ = `#${this.id}`; | ||
default: | ||
return this[privates.query_] = `.${this[privates.id]}`; | ||
return this.query_ = `.${this.id}`; | ||
} | ||
} | ||
private [privates.scope](child: El): void { | ||
private scope(child: El): void { | ||
if (child.tag.toUpperCase() !== 'STYLE') return; | ||
@@ -181,12 +179,12 @@ const source = child.element.innerHTML; | ||
const scope = /(^|[>~+,}/])(\s*)\:scope(?!\w)(?=\s*[A-Za-z#.:[>~+,{/])/g; | ||
const style = source.replace(scope, (...$) => `${$[1]}${$[2]}${this[privates.query]}`); | ||
assert(!this[privates.query_] || style !== source); | ||
const style = source.replace(scope, (...$) => `${$[1]}${$[2]}${this.query}`); | ||
assert(!this.query_ || style !== source); | ||
if (style === source) return; | ||
child.element.innerHTML = style; | ||
assert(/^[:#.][\w-]+$/.test(this[privates.query])); | ||
assert(/^[:#.][\w-]+$/.test(this.query)); | ||
assert(child.element.children.length === 0); | ||
child.element.firstElementChild && child.element.replaceChildren(); | ||
} | ||
private [privates.isObserverUpdate] = false; | ||
private [privates.observe](children: El.Children.Struct): El.Children.Struct { | ||
private isObserverUpdate = false; | ||
private observe(children: El.Children.Struct): El.Children.Struct { | ||
return ObjectDefineProperties(children, ObjectKeys(children).reduce((obj, name) => { | ||
@@ -202,7 +200,7 @@ if (name in {}) throw new Error(`TypedDOM: Child names conflicted with the object property names.`); | ||
set: (newChild: El) => { | ||
if (!this[privates.isObserverUpdate]) { | ||
if (!this.isObserverUpdate) { | ||
this.children = { [name]: newChild } as El.Setter<C>; | ||
} | ||
else { | ||
this[privates.isObserverUpdate] = false; | ||
this.isObserverUpdate = false; | ||
} | ||
@@ -215,25 +213,25 @@ child = newChild; | ||
} | ||
private readonly [privates.type]: ElChildType; | ||
private readonly [privates.container]: Element | ShadowRoot; | ||
private [privates.isInit] = true; | ||
private [privates.children]: C; | ||
private readonly type: ElChildType; | ||
private readonly container: Element | ShadowRoot; | ||
private isInit = true; | ||
private children_: C; | ||
public get children(): El.Getter<C> { | ||
switch (this[privates.type]) { | ||
switch (this.type) { | ||
case ElChildType.Text: | ||
return this[privates.container].textContent as El.Getter<C>; | ||
return this.container.textContent as El.Getter<C>; | ||
default: | ||
return this[privates.children] as El.Getter<C>; | ||
return this.children_ as El.Getter<C>; | ||
} | ||
} | ||
public set children(children: El.Setter<C>) { | ||
assert(!this[privates.isObserverUpdate]); | ||
const container = this[privates.container]; | ||
assert(!this.isObserverUpdate); | ||
const container = this.container; | ||
const removedChildren: El[] = []; | ||
const addedChildren: El[] = []; | ||
let isMutated = false; | ||
switch (this[privates.type]) { | ||
switch (this.type) { | ||
case ElChildType.Void: | ||
return; | ||
case ElChildType.Text: { | ||
if (this[privates.isInit] || !this[privates.events].mutate) { | ||
if (this.isInit || !this[privates.listeners].mutate) { | ||
container.textContent = children as El.Children.Text; | ||
@@ -252,3 +250,3 @@ isMutated = true; | ||
const sourceChildren = children as El.Children.Array; | ||
const targetChildren = this[privates.children] as El.Children.Array; | ||
const targetChildren = this.children_ as El.Children.Array; | ||
isMutated ||= sourceChildren.length !== targetChildren.length; | ||
@@ -258,8 +256,8 @@ for (let i = 0; i < sourceChildren.length; ++i) { | ||
const oldChild = targetChildren[i]; | ||
throwErrorIfNotUsable(newChild, this[privates.container]); | ||
throwErrorIfNotUsable(newChild, this.container); | ||
isMutated ||= newChild.element !== oldChild.element; | ||
if (newChild.element.parentNode !== this.element) { | ||
this[privates.scope](newChild); | ||
this.scope(newChild); | ||
assert(!addedChildren.includes(newChild)); | ||
events(newChild)?.connect && addedChildren.push(newChild); | ||
hasListener(newChild) && addedChildren.push(newChild) && this[privates.listeners].add(newChild); | ||
} | ||
@@ -275,3 +273,3 @@ } | ||
} | ||
this[privates.children] = sourceChildren as C; | ||
this.children_ = sourceChildren as C; | ||
for (let i = 0; i < targetChildren.length; ++i) { | ||
@@ -281,3 +279,3 @@ const oldChild = targetChildren[i]; | ||
assert(!removedChildren.includes(oldChild)); | ||
events(oldChild)?.disconnect && removedChildren.push(oldChild); | ||
hasListener(oldChild) && removedChildren.push(oldChild) && this[privates.listeners].delete(oldChild); | ||
assert(isMutated); | ||
@@ -291,3 +289,3 @@ } | ||
case ElChildType.Struct: { | ||
if (this[privates.isInit]) { | ||
if (this.isInit) { | ||
container.firstChild && container.replaceChildren(); | ||
@@ -298,7 +296,7 @@ const sourceChildren = children as El.Children.Struct; | ||
const newChild = sourceChildren[name]; | ||
throwErrorIfNotUsable(newChild, this[privates.container]); | ||
this[privates.scope](newChild); | ||
throwErrorIfNotUsable(newChild, this.container); | ||
this.scope(newChild); | ||
container.appendChild(newChild.element); | ||
assert(!addedChildren.includes(newChild)); | ||
events(newChild)?.connect && addedChildren.push(newChild); | ||
hasListener(newChild) && addedChildren.push(newChild) && this[privates.listeners].add(newChild); | ||
isMutated = true; | ||
@@ -309,3 +307,3 @@ } | ||
const sourceChildren = children as El.Children.Struct; | ||
const targetChildren = this[privates.children] as El.Children.Struct; | ||
const targetChildren = this.children_ as El.Children.Struct; | ||
if (sourceChildren === targetChildren) break; | ||
@@ -318,11 +316,11 @@ for (const name in sourceChildren) { | ||
if (newChild === oldChild) continue; | ||
throwErrorIfNotUsable(newChild, this[privates.container]); | ||
throwErrorIfNotUsable(newChild, this.container); | ||
if (newChild !== oldChild && newChild.element.parentNode !== oldChild.element.parentNode) { | ||
this[privates.scope](newChild); | ||
this.scope(newChild); | ||
container.replaceChild(newChild.element, oldChild.element); | ||
assert(!oldChild.element.parentNode); | ||
assert(!addedChildren.includes(newChild)); | ||
events(newChild)?.connect && addedChildren.push(newChild); | ||
hasListener(newChild) && addedChildren.push(newChild) && this[privates.listeners].add(newChild); | ||
assert(!removedChildren.includes(oldChild)); | ||
events(oldChild)?.disconnect && removedChildren.push(oldChild); | ||
hasListener(oldChild) && removedChildren.push(oldChild) && this[privates.listeners].delete(oldChild); | ||
} | ||
@@ -336,6 +334,6 @@ else { | ||
isMutated = true; | ||
this[privates.isObserverUpdate] = true; | ||
this.isObserverUpdate = true; | ||
targetChildren[name] = newChild; | ||
assert(!this[privates.isObserverUpdate]); | ||
this[privates.isObserverUpdate] = false; | ||
assert(!this.isObserverUpdate); | ||
this.isObserverUpdate = false; | ||
} | ||
@@ -345,18 +343,43 @@ break; | ||
} | ||
for (let i = 0; i < removedChildren.length; ++i) { | ||
removedChildren[i].element.dispatchEvent(new Event('disconnect', { bubbles: false, cancelable: true })); | ||
} | ||
for (let i = 0; i < addedChildren.length; ++i) { | ||
addedChildren[i].element.dispatchEvent(new Event('connect', { bubbles: false, cancelable: true })); | ||
} | ||
this.dispatchDisconnectionEvent(removedChildren); | ||
this.dispatchConnectionEvent(addedChildren); | ||
assert(isMutated || removedChildren.length + addedChildren.length === 0); | ||
if (isMutated && this[privates.events].mutate) { | ||
if (isMutated && this[privates.listeners].mutate) { | ||
this.element.dispatchEvent(new Event('mutate', { bubbles: false, cancelable: true })); | ||
} | ||
} | ||
private get isConnected(): boolean { | ||
return !!this.element.parentNode && this.element.isConnected; | ||
} | ||
private dispatchConnectionEvent( | ||
listeners: El[] | undefined = this[privates.listeners].values, | ||
isConnected = listeners.length !== 0 && this.isConnected, | ||
): void { | ||
if (listeners.length === 0) return; | ||
if (listeners !== this[privates.listeners].values && !isConnected) return; | ||
for (const listener of listeners) { | ||
listener.element[proxy].dispatchConnectionEvent(void 0, isConnected); | ||
getListeners(listener)?.connect && listener.element.dispatchEvent(new Event('connect', { bubbles: false, cancelable: true })); | ||
} | ||
} | ||
private dispatchDisconnectionEvent( | ||
listeners: El[] | undefined = this[privates.listeners].values, | ||
isConnected = listeners.length !== 0 && this.isConnected, | ||
): void { | ||
if (listeners.length === 0) return; | ||
if (listeners !== this[privates.listeners].values && !isConnected) return; | ||
for (const listener of listeners) { | ||
listener.element[proxy].dispatchDisconnectionEvent(void 0, isConnected); | ||
getListeners(listener)?.disconnect && listener.element.dispatchEvent(new Event('disconnect', { bubbles: false, cancelable: true })); | ||
} | ||
} | ||
} | ||
function events(child: El): ElementProxy[typeof privates.events] | undefined { | ||
return child[privates.events] ?? child.element[proxy]?.[privates.events]; | ||
function hasListener(child: El): boolean { | ||
const ls = getListeners(child); | ||
return ls?.connect || ls?.disconnect || ls?.values.length! > 0; | ||
} | ||
function getListeners(child: El): ElementProxy[typeof privates.listeners] | undefined { | ||
return child[privates.listeners] ?? child.element[proxy]?.[privates.listeners]; | ||
} | ||
@@ -363,0 +386,0 @@ function throwErrorIfNotUsable(child: El, newParent?: ParentNode): void { |
@@ -16,2 +16,5 @@ import { Shadow, HTML, SVG, El, Attrs, shadow, html } from '../..'; | ||
const doc = Shadow.section([]); | ||
document.body.appendChild(doc.element); | ||
describe('Integration: Typed DOM', function () { | ||
@@ -380,5 +383,5 @@ describe('spec', function () { | ||
onconnect: ({ currentTarget: el }) => | ||
el.textContent += el.textContent!.toUpperCase(), | ||
el.textContent += el.textContent![0].toUpperCase(), | ||
ondisconnect: ({ currentTarget: el }) => | ||
el.textContent += el.textContent!, | ||
el.textContent += el.textContent![0].toLowerCase(), | ||
}; | ||
@@ -392,2 +395,9 @@ const dom = HTML.ul([ | ||
[ | ||
'a', | ||
'b', | ||
]); | ||
doc.children = [dom]; | ||
assert.deepStrictEqual( | ||
dom.children.map(child => child.children), | ||
[ | ||
'aA', | ||
@@ -409,2 +419,9 @@ 'bB', | ||
[...dom.element.children]); | ||
doc.children = []; | ||
assert.deepStrictEqual( | ||
dom.children.map(child => child.children), | ||
[ | ||
'bBb', | ||
'cCc', | ||
]); | ||
}); | ||
@@ -429,2 +446,12 @@ | ||
[ | ||
['a', 'a'], | ||
['b', 'b'], | ||
['c', 'c'], | ||
['d', 'd'], | ||
['e', 'e'], | ||
]); | ||
doc.children = [Shadow.section([dom])]; | ||
assert.deepStrictEqual( | ||
Object.entries(dom.children).map(([k, v]) => [k, v.children]), | ||
[ | ||
['a', 'aA'], | ||
@@ -431,0 +458,0 @@ ['b', 'bB'], |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
685070
16121