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): more react staff #2132

Merged
merged 10 commits into from
Feb 3, 2024
4 changes: 2 additions & 2 deletions deno_dist/helper/css/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { raw } from '../../helper/html/index.ts'
import { DOM_RENDERER } from '../../jsx/constants.ts'
import { createCssJsxDomObjects } from '../../jsx/dom/css.ts'
import { RENDER_TO_DOM } from '../../jsx/dom/render.ts'
import type { HtmlEscapedCallback, HtmlEscapedString } from '../../utils/html.ts'
import type { CssClassName as CssClassNameCommon, CssVariableType } from './common.ts'
import {
Expand Down Expand Up @@ -138,7 +138,7 @@ export const createCssContext = ({ id }: { id: Readonly<string> }) => {
? raw(`<style id="${id}">${(children as unknown as CssClassName)[STYLE_STRING]}</style>`)
: raw(`<style id="${id}"></style>`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(Style as any)[RENDER_TO_DOM] = StyleRenderToDom
;(Style as any)[DOM_RENDERER] = StyleRenderToDom

return {
css,
Expand Down
4 changes: 2 additions & 2 deletions deno_dist/jsx/components.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { raw } from '../helper/html/index.ts'
import type { HtmlEscapedString, HtmlEscapedCallback } from '../utils/html.ts'
import { HtmlEscapedCallbackPhase, resolveCallback } from '../utils/html.ts'
import { DOM_RENDERER } from './constants.ts'
import { ErrorBoundary as ErrorBoundaryDomRenderer } from './dom/components.ts'
import type { HasRenderToDom } from './dom/render.ts'
import { RENDER_TO_DOM } from './dom/render.ts'
import type { FC, Child } from './index.ts'

let errorBoundaryCounter = 0
Expand Down Expand Up @@ -180,4 +180,4 @@ d.remove()
return raw(resArray.join(''))
}
}
;(ErrorBoundary as HasRenderToDom)[RENDER_TO_DOM] = ErrorBoundaryDomRenderer
;(ErrorBoundary as HasRenderToDom)[DOM_RENDERER] = ErrorBoundaryDomRenderer
3 changes: 3 additions & 0 deletions deno_dist/jsx/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export const DOM_RENDERER = Symbol('RENDERER')
export const DOM_ERROR_HANDLER = Symbol('ERROR_HANDLER')
export const DOM_STASH = Symbol('STASH')
37 changes: 21 additions & 16 deletions deno_dist/jsx/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { raw } from '../helper/html/index.ts'
import type { HtmlEscapedString } from '../utils/html.ts'
import { DOM_RENDERER } from './constants.ts'
import { createContextProviderFunction } from './dom/context.ts'
import { RENDER_TO_DOM } from './dom/render.ts'
import { JSXFragmentNode } from './index.ts'
import type { FC } from './index.ts'

Expand All @@ -10,35 +10,40 @@ export interface Context<T> {
Provider: FC<{ value: T }>
}

export const globalContexts: Context<unknown>[] = []

export const createContext = <T>(defaultValue: T): Context<T> => {
const values = [defaultValue]
const context: Context<T> = {
values,
Provider(props): HtmlEscapedString | Promise<HtmlEscapedString> {
values.push(props.value)
const string = props.children
? (Array.isArray(props.children)
? new JSXFragmentNode('', {}, props.children)
: props.children
).toString()
: ''
values.pop()
let string
try {
string = props.children
? (Array.isArray(props.children)
? new JSXFragmentNode('', {}, props.children)
: props.children
).toString()
: ''
} finally {
values.pop()
}

if (string instanceof Promise) {
return Promise.resolve().then<HtmlEscapedString>(async () => {
values.push(props.value)
const awaited = await string
const promiseRes = raw(awaited, (awaited as HtmlEscapedString).callbacks)
values.pop()
return promiseRes
})
return string.then((resString) =>
raw(resString, (resString as HtmlEscapedString).callbacks)
)
} else {
return raw(string)
}
},
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(context.Provider as any)[RENDER_TO_DOM] = createContextProviderFunction(values)
;(context.Provider as any)[DOM_RENDERER] = createContextProviderFunction(values)

globalContexts.push(context as Context<unknown>)

return context
}

Expand Down
6 changes: 3 additions & 3 deletions deno_dist/jsx/dom/components.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { FC, Child } from '../index.ts'
import type { FallbackRender, ErrorHandler } from '../components.ts'
import { DOM_ERROR_HANDLER } from '../constants.ts'
import { Fragment } from './jsx-runtime.ts'
import { ERROR_HANDLER } from './render.ts'

/* eslint-disable @typescript-eslint/no-explicit-any */
export const ErrorBoundary: FC<{
Expand All @@ -10,7 +10,7 @@ export const ErrorBoundary: FC<{
onError?: ErrorHandler
}> = (({ children, fallback, fallbackRender, onError }: any) => {
const res = Fragment({ children })
;(res as any)[ERROR_HANDLER] = (err: any) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: any) => {
if (err instanceof Promise) {
throw err
}
Expand All @@ -22,7 +22,7 @@ export const ErrorBoundary: FC<{

export const Suspense: FC<{ fallback: any }> = (({ children, fallback }: any) => {
const res = Fragment({ children })
;(res as any)[ERROR_HANDLER] = (err: any, retry: () => void) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: any, retry: () => void) => {
if (!(err instanceof Promise)) {
throw err
}
Expand Down
9 changes: 6 additions & 3 deletions deno_dist/jsx/dom/context.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Child } from '../index.ts'
import { DOM_ERROR_HANDLER } from '../constants.ts'
import type { Context } from '../context.ts'
import { globalContexts } from '../context.ts'
import { Fragment } from './jsx-runtime.ts'
import { ERROR_HANDLER } from './render.ts'

export const createContextProviderFunction =
<T>(values: T[]) =>
Expand All @@ -22,7 +23,7 @@ export const createContextProviderFunction =
],
})
// eslint-disable-next-line @typescript-eslint/no-explicit-any
;(res as any)[ERROR_HANDLER] = (err: unknown) => {
;(res as any)[DOM_ERROR_HANDLER] = (err: unknown) => {
values.pop()
throw err
}
Expand All @@ -31,9 +32,11 @@ export const createContextProviderFunction =

export const createContext = <T>(defaultValue: T): Context<T> => {
const values = [defaultValue]
return {
const context = {
values,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
Provider: createContextProviderFunction(values) as any,
}
globalContexts.push(context)
return context
}
33 changes: 33 additions & 0 deletions deno_dist/jsx/dom/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,38 @@
export {
useState,
useEffect,
useRef,
useCallback,
use,
startTransition,
useTransition,
useDeferredValue,
startViewTransition,
useViewTransition,
useMemo,
useLayoutEffect,
} from '../hooks/index.ts'
export { render } from './render.ts'
export { Suspense, ErrorBoundary } from './components.ts'
export { useContext } from '../context.ts'
export type { Context } from '../context.ts'
export { createContext } from './context.ts'
export { memo, isValidElement } from '../index.ts'

import type { Props, Child, JSXNode } from '../index.ts'
import { jsx } from './jsx-runtime.ts'
export const cloneElement = <T extends JSXNode | JSX.Element>(
element: T,
props: Props,
...children: Child[]
): T => {
return jsx(
(element as JSXNode).tag,
{
...(element as JSXNode).props,
...props,
children: children.length ? children : (element as JSXNode).children,
},
(element as JSXNode).key
) as T
}
51 changes: 36 additions & 15 deletions deno_dist/jsx/dom/render.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import type { FC, Child, Props } from '../index.ts'
import type { JSXNode } from '../index.ts'
import { DOM_RENDERER, DOM_ERROR_HANDLER, DOM_STASH } from '../constants.ts'
import type { Context as JSXContext } from '../context.ts'
import { globalContexts as globalJSXContexts } from '../context.ts'
import type { EffectData } from '../hooks/index.ts'
import { STASH_EFFECT } from '../hooks/index.ts'

const eventAliasMap: Record<string, string> = {
change: 'input',
}

export const RENDER_TO_DOM = Symbol()
export const ERROR_HANDLER = Symbol()
export const STASH = Symbol()
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type HasRenderToDom = FC<any> & { [RENDER_TO_DOM]: FC<any> }
export type HasRenderToDom = FC<any> & { [DOM_RENDERER]: FC<any> }
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export type ErrorHandler = (error: any, retry: () => void) => Child | undefined

type Container = HTMLElement | DocumentFragment
type LocalJSXContexts = [JSXContext<unknown>, unknown][] | undefined

export type NodeObject = {
pP: Props | undefined // previous props
Expand All @@ -25,11 +26,18 @@ export type NodeObject = {
s?: Node[] // shadow virtual dom children
c: Container | undefined // container
e: HTMLElement | Text | undefined // rendered element
[STASH]: [
number, // current hook index
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][] // stash for hooks
]
[DOM_STASH]:
| [
number, // current hook index
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][], // stash for hooks
LocalJSXContexts // context
]
| [
number,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
any[][]
]
} & JSXNode
type NodeString = [string] & {
e?: Text
Expand Down Expand Up @@ -133,9 +141,9 @@ const invokeTag = (context: Context, node: NodeObject): Child[] => {
return res
}

node[STASH][0] = 0
node[DOM_STASH][0] = 0
buildDataStack.push([context, node])
const func = (node.tag as HasRenderToDom)[RENDER_TO_DOM] || node.tag
const func = (node.tag as HasRenderToDom)[DOM_RENDERER] || node.tag
try {
return [
func.call(null, {
Expand All @@ -157,7 +165,7 @@ const getNextChildren = (
) => {
childrenToRemove.push(...node.vR)
if (typeof node.tag === 'function') {
node[STASH][1][STASH_EFFECT]?.forEach((data: EffectData) => callbacks.push(data))
node[DOM_STASH][1][STASH_EFFECT]?.forEach((data: EffectData) => callbacks.push(data))
}
node.vC.forEach((child) => {
if (isNodeString(child)) {
Expand Down Expand Up @@ -195,7 +203,7 @@ const findInsertBefore = (node: Node | undefined): ChildNode | null => {

const removeNode = (node: Node) => {
if (!isNodeString(node)) {
node[STASH]?.[1][STASH_EFFECT]?.forEach((data: EffectData) => data[2]?.())
node[DOM_STASH]?.[1][STASH_EFFECT]?.forEach((data: EffectData) => data[2]?.())
node.vC?.forEach(removeNode)
}
node.e?.remove()
Expand Down Expand Up @@ -255,6 +263,9 @@ const applyNodeObject = (node: NodeObject, container: Container) => {
}
remove.forEach(removeNode)
callbacks.forEach(([, cb]) => cb?.())
requestAnimationFrame(() => {
callbacks.forEach(([, , , cb]) => cb?.())
})
}

export const build = (
Expand All @@ -271,7 +282,7 @@ export const build = (
children ||= typeof node.tag == 'function' ? invokeTag(context, node) : node.children
if ((children[0] as JSXNode)?.tag === '') {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
errorHandler = (children[0] as any)[ERROR_HANDLER] as ErrorHandler
errorHandler = (children[0] as any)[DOM_ERROR_HANDLER] as ErrorHandler
topLevelErrorHandlerNode ||= node
}
const oldVChildren: Node[] = node.vC ? [...node.vC] : []
Expand All @@ -287,6 +298,10 @@ export const build = (
}
prevNode = child

if (typeof child.tag === 'function' && globalJSXContexts.length > 0) {
child[DOM_STASH][2] = globalJSXContexts.map((c) => [c, c.values.at(-1)])
}

let oldChild: Node | undefined
const i = oldVChildren.findIndex((c) => c.key === (child as Node).key)
if (i !== -1) {
Expand Down Expand Up @@ -346,7 +361,7 @@ const buildNode = (node: Child): Node | undefined => {
return [node.toString()] as NodeString
} else {
if (typeof (node as JSXNode).tag === 'function') {
;(node as NodeObject)[STASH] = [0, []]
;(node as NodeObject)[DOM_STASH] = [0, []]
}
return node as NodeObject
}
Expand All @@ -360,7 +375,13 @@ const replaceContainer = (node: NodeObject, from: DocumentFragment, to: Containe
}

const updateSync = (context: Context, node: NodeObject) => {
node[DOM_STASH][2]?.forEach(([c, v]) => {
c.values.push(v)
})
build(context, node, undefined)
node[DOM_STASH][2]?.forEach(([c]) => {
c.values.pop()
})
if (context[0] !== 1 || !context[1]) {
apply(node, node.c as Container)
}
Expand Down