-
-
Notifications
You must be signed in to change notification settings - Fork 552
/
block.ts
128 lines (114 loc) · 4.18 KB
/
block.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
import { createContext, createElement, Fragment, useCallback, useContext, useMemo, useRef } from 'react';
import type { ComponentType, Ref, Dispatch, SetStateAction } from 'react';
import {
block as createBlock,
mount$,
patch as patchBlock,
remove$ as removeBlock
} from '../million/block';
import { MapSet$, MapHas$ } from '../million/constants';
import type { Options, MillionProps, MillionPortal } from '../types';
import { processProps, unwrap } from './utils';
import { Effect, RENDER_SCOPE, REGISTRY, SVG_RENDER_SCOPE } from './constants';
import { experimental_options } from '../million/experimental';
import { useContainer, useNearestParent } from './its-fine';
import { cloneNode$ } from '../million/dom';
experimental_options.noSlot = true;
export const mountContext = createContext<Dispatch<SetStateAction<boolean>> | null>(null)
export const block = <P extends MillionProps>(
fn: ComponentType<P> | null,
options: Options<P> | null | undefined = {}
) => {
const noSlot = options?.experimental_noSlot ?? experimental_options.noSlot
let blockTarget: ReturnType<typeof createBlock> | null = options?.block;
const defaultType = options?.svg ? SVG_RENDER_SCOPE : RENDER_SCOPE;
if (fn) {
blockTarget = createBlock(
fn as any,
unwrap as any,
options?.shouldUpdate as Parameters<typeof createBlock>[2],
options?.svg
);
}
const MillionBlock = <P extends MillionProps>(
props: P,
forwardedRef: Ref<any>
) => {
const mount = useContext(mountContext)
const container = useContainer<HTMLElement>(); // usable when there's no parent other than the root element
const parentRef = useNearestParent<HTMLElement>();
const hmrTimestamp = props._hmr;
const ref = useRef<HTMLElement | null>(null);
const patch = useRef<((props: P) => void) | null>(null);
const portalRef = useRef<MillionPortal[]>([]);
props = processProps(props, forwardedRef, portalRef.current);
patch.current?.(props);
const effect = useCallback(() => {
if (!ref.current && !noSlot) return;
const currentBlock = blockTarget!(props, props.key);
if (hmrTimestamp && ref.current && ref.current.textContent) {
ref.current.textContent = '';
}
if (noSlot) {
ref.current = (parentRef.current ?? container.current) as HTMLElement;
if (props.scoped) {
// in portals, parentRef is not the proper parent
ref.current = container.current!
}
if (ref.current.childNodes.length) {
console.error(new Error(`\`experimental_options.noSlot\` does not support having siblings at the moment.
The block element should be the only child of the \`${
(cloneNode$.call(ref.current) as HTMLElement).outerHTML
}\` element.
To avoid this error, \`experimental_options.noSlot\` should be false`));
}
}
if (patch.current === null || hmrTimestamp) {
mount$.call(currentBlock, ref.current!, null);
patch.current = (props: P) => {
patchBlock(
currentBlock,
blockTarget!(
props,
props.key,
options?.shouldUpdate as Parameters<typeof createBlock>[2]
)
);
};
mount?.(true)
}
return () => {
removeBlock.call(currentBlock)
}
}, []);
const marker = useMemo(() => {
if (noSlot) {
return null
}
return createElement(options?.as ?? defaultType, { ref });
}, []);
const childrenSize = portalRef.current.length;
const children = new Array(childrenSize);
children[0] = marker;
children[1] = createElement(Effect, {
effect,
deps: hmrTimestamp ? [hmrTimestamp] : [],
});
for (let i = 0; i < childrenSize; ++i) {
children[i + 2] = portalRef.current[i]?.portal;
}
const vnode = createElement(Fragment, { children });
return vnode;
};
if (!MapHas$.call(REGISTRY, MillionBlock)) {
MapSet$.call(REGISTRY, MillionBlock, block);
}
// TODO add dev guard
if (options?.name) {
if (fn) {
fn.displayName = `Million(Render(${options.name}))`;
}
MillionBlock.displayName = `Million(Block(${options.name}))`;
}
return MillionBlock<P>;
};