Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(jsx/dom): improve compatibility with React - The 2024 May Update #2756

Merged
merged 7 commits into from
May 24, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions deno_dist/jsx/dom/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
useReducer,
useId,
useDebugValue,
Expand Down Expand Up @@ -79,6 +80,7 @@ export {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
useReducer,
useId,
useDebugValue,
Expand Down Expand Up @@ -114,6 +116,7 @@ export default {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
useReducer,
useId,
useDebugValue,
Expand Down
27 changes: 6 additions & 21 deletions deno_dist/jsx/dom/jsx-dev-runtime.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,16 @@
import type { Props, JSXNode } from '../base.ts'
import { normalizeIntrinsicElementProps } from '../utils.ts'

const JSXNodeCompatPrototype = {
type: {
get(this: { tag: string | Function }): string | Function {
return this.tag
},
},
ref: {
get(this: { props?: { ref: unknown } }): unknown {
return this.props?.ref
},
},
}
import { newJSXNode } from './utils.ts'

export const jsxDEV = (tag: string | Function, props: Props, key?: string): JSXNode => {
if (typeof tag === 'string') {
normalizeIntrinsicElementProps(props)
}
return Object.defineProperties(
{
tag,
props,
key,
},
JSXNodeCompatPrototype
) as JSXNode
return newJSXNode({
tag,
props,
key,
})
}

export const Fragment = (props: Record<string, unknown>): JSXNode => jsxDEV('', props, undefined)
75 changes: 46 additions & 29 deletions deno_dist/jsx/dom/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { EffectData } from '../hooks/index.ts'
import { STASH_EFFECT } from '../hooks/index.ts'
import { styleObjectForEach } from '../utils.ts'
import { createContext } from './context.ts' // import dom-specific versions
import { newJSXNode } from './utils.ts'

const HONO_PORTAL_ELEMENT = '_hp'

Expand Down Expand Up @@ -106,6 +107,14 @@ const getEventSpec = (key: string): [string, boolean] | undefined => {
return undefined
}

const toAttributeName = (element: SupportedElement, key: string): string =>
element instanceof SVGElement &&
/[A-Z]/.test(key) &&
(key in element.style || // Presentation attributes are findable in style object. "clip-path", "font-size", "stroke-width", etc.
key.match(/^(?:o|pai|str|u|ve)/)) // Other un-deprecated kebab-case attributes. "overline-position", "paint-order", "strikethrough-position", etc.
? key.replace(/([A-Z])/g, '-$1').toLowerCase()
: key

const applyProps = (container: SupportedElement, attributes: Props, oldAttributes?: Props) => {
attributes ||= {}
for (const [key, value] of Object.entries(attributes)) {
Expand Down Expand Up @@ -164,14 +173,16 @@ const applyProps = (container: SupportedElement, attributes: Props, oldAttribute
;(container as any)[key] = value
}

const k = toAttributeName(container, key)

if (value === null || value === undefined || value === false) {
container.removeAttribute(key)
container.removeAttribute(k)
} else if (value === true) {
container.setAttribute(key, '')
container.setAttribute(k, '')
} else if (typeof value === 'string' || typeof value === 'number') {
container.setAttribute(key, value as string)
container.setAttribute(k, value as string)
} else {
container.setAttribute(key, value.toString())
container.setAttribute(k, value.toString())
}
}
}
Expand All @@ -189,7 +200,7 @@ const applyProps = (container: SupportedElement, attributes: Props, oldAttribute
value.current = null
}
} else {
container.removeAttribute(key)
container.removeAttribute(toAttributeName(container, key))
}
}
}
Expand Down Expand Up @@ -248,6 +259,8 @@ const getNextChildren = (
const findInsertBefore = (node: Node | undefined): ChildNode | null => {
if (!node) {
return null
} else if (node.tag === HONO_PORTAL_ELEMENT) {
return findInsertBefore(node.nN)
} else if (node.e) {
return node.e
}
Expand Down Expand Up @@ -325,7 +338,7 @@ const applyNodeObject = (node: NodeObject, container: Container) => {
const childNodes = container.childNodes
let offset =
findChildNodeIndex(childNodes, findInsertBefore(node.nN)) ??
findChildNodeIndex(childNodes, next.find((n) => n.e)?.e) ??
findChildNodeIndex(childNodes, next.find((n) => n.tag !== HONO_PORTAL_ELEMENT && n.e)?.e) ??
childNodes.length

for (let i = 0, len = next.length; i < len; i++, offset++) {
Expand All @@ -345,18 +358,17 @@ const applyNodeObject = (node: NodeObject, container: Container) => {
applyProps(el as HTMLElement, child.props, child.pP)
applyNode(child, el as HTMLElement)
}
if (
childNodes[offset] !== el &&
childNodes[offset - 1] !== child.e &&
child.tag !== HONO_PORTAL_ELEMENT
) {
if (child.tag === HONO_PORTAL_ELEMENT) {
offset--
} else if (childNodes[offset] !== el && childNodes[offset - 1] !== child.e) {
container.insertBefore(el, childNodes[offset] || null)
}
}
remove.forEach(removeNode)
callbacks.forEach(([, cb]) => cb?.())
callbacks.forEach(([, , , , cb]) => cb?.()) // invoke useInsertionEffect callbacks
callbacks.forEach(([, cb]) => cb?.()) // invoke useLayoutEffect callbacks
requestAnimationFrame(() => {
callbacks.forEach(([, , , cb]) => cb?.())
callbacks.forEach(([, , , cb]) => cb?.()) // invoke useEffect callbacks
})
}

Expand All @@ -380,7 +392,7 @@ export const build = (
}
const oldVChildren: Node[] = node.vC ? [...node.vC] : []
const vChildren: Node[] = []
const vChildrenToRemove: Node[] = []
node.vR = []
let prevNode: Node | undefined
try {
children.flat().forEach((c: Child) => {
Expand All @@ -401,25 +413,27 @@ export const build = (
}

let oldChild: Node | undefined
const i = oldVChildren.findIndex((c) => c.key === (child as Node).key)
const i = oldVChildren.findIndex(
isNodeString(child)
? (c) => isNodeString(c)
: child.key !== undefined
? (c) => c.key === (child as Node).key
: (c) => c.tag === (child as Node).tag
)
if (i !== -1) {
oldChild = oldVChildren[i]
oldVChildren.splice(i, 1)
}

if (oldChild) {
if (isNodeString(child)) {
if (!isNodeString(oldChild)) {
vChildrenToRemove.push(oldChild)
} else {
if (oldChild.t !== child.t) {
oldChild.t = child.t // update text content
oldChild.d = true
}
child = oldChild
if ((oldChild as NodeString).t !== child.t) {
;(oldChild as NodeString).t = child.t // update text content
;(oldChild as NodeString).d = true
}
child = oldChild
} else if (oldChild.tag !== child.tag) {
vChildrenToRemove.push(oldChild)
node.vR.push(oldChild)
} else {
oldChild.pP = oldChild.props
oldChild.props = child.props
Expand All @@ -442,8 +456,7 @@ export const build = (
}
})
node.vC = vChildren
vChildrenToRemove.push(...oldVChildren)
node.vR = vChildrenToRemove
node.vR.push(...oldVChildren)
} catch (e) {
if (errorHandler) {
const fallbackUpdateFn = () =>
Expand Down Expand Up @@ -481,10 +494,14 @@ const buildNode = (node: Child): Node | undefined => {
} else if (typeof node === 'string' || typeof node === 'number') {
return { t: node.toString(), d: true } as NodeString
} else {
if ('vR' in node) {
node = newJSXNode({
tag: (node as NodeObject).tag,
props: (node as NodeObject).props,
key: (node as NodeObject).key,
})
}
if (typeof (node as JSXNode).tag === 'function') {
if ((node as NodeObject)[DOM_STASH]) {
node = { ...node } as NodeObject
}
;(node as NodeObject)[DOM_STASH] = [0, []]
} else {
const ns = nameSpaceMap[(node as JSXNode).tag as string]
Expand Down
17 changes: 17 additions & 0 deletions deno_dist/jsx/dom/utils.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,24 @@
import type { Props, JSXNode } from '../base.ts'
import { DOM_INTERNAL_TAG } from '../constants.ts'

export const setInternalTagFlag = (fn: Function): Function => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(fn as any)[DOM_INTERNAL_TAG] = true
return fn
}

const JSXNodeCompatPrototype = {
type: {
get(this: { tag: string | Function }): string | Function {
return this.tag
},
},
ref: {
get(this: { props?: { ref: unknown } }): unknown {
return this.props?.ref
},
},
}

export const newJSXNode = (obj: { tag: string | Function; props?: Props; key?: string }): JSXNode =>
Object.defineProperties(obj, JSXNodeCompatPrototype) as JSXNode
9 changes: 7 additions & 2 deletions deno_dist/jsx/hooks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ export type EffectData = [
readonly unknown[] | undefined, // deps
(() => void | (() => void)) | undefined, // layout effect
(() => void) | undefined, // cleanup
(() => void) | undefined // effect
(() => void) | undefined, // effect
(() => void) | undefined // insertion effect
]

const resolvedPromiseValueMap: WeakMap<Promise<unknown>, unknown> = new WeakMap<
Expand Down Expand Up @@ -251,7 +252,7 @@ const useEffectCommon = (
data[index] = undefined // clear this effect in order to avoid calling effect twice
data[2] = effect() as (() => void) | undefined
}
const data: EffectData = [deps, undefined, undefined, undefined]
const data: EffectData = [deps, undefined, undefined, undefined, undefined]
data[index] = runner
effectDepsArray[hookIndex] = data
}
Expand All @@ -262,6 +263,10 @@ export const useLayoutEffect = (
effect: () => void | (() => void),
deps?: readonly unknown[]
): void => useEffectCommon(1, effect, deps)
export const useInsertionEffect = (
effect: () => void | (() => void),
deps?: readonly unknown[]
): void => useEffectCommon(4, effect, deps)

export const useCallback = <T extends (...args: unknown[]) => unknown>(
callback: T,
Expand Down
3 changes: 3 additions & 0 deletions deno_dist/jsx/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
useReducer,
useId,
useDebugValue,
Expand Down Expand Up @@ -51,6 +52,7 @@ export {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
createRef,
forwardRef,
useImperativeHandle,
Expand Down Expand Up @@ -84,6 +86,7 @@ export default {
useViewTransition,
useMemo,
useLayoutEffect,
useInsertionEffect,
createRef,
forwardRef,
useImperativeHandle,
Expand Down
1 change: 1 addition & 0 deletions src/jsx/dom/css.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ describe('Style and css for jsx/dom', () => {
})
global.document = dom.window.document
global.HTMLElement = dom.window.HTMLElement
global.SVGElement = dom.window.SVGElement
global.Text = dom.window.Text
root = document.getElementById('root') as HTMLElement
})
Expand Down
Loading