Skip to content

Commit

Permalink
control element
Browse files Browse the repository at this point in the history
  • Loading branch information
MrWangJustToDo committed Jan 12, 2024
1 parent 2d0e958 commit ecded6f
Show file tree
Hide file tree
Showing 8 changed files with 325 additions and 37 deletions.
8 changes: 4 additions & 4 deletions __tests__/testAntd.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../bundle/babel.min.js"></script>
<script src="../bundle/react.development.js"></script>
<script src="../bundle/react-dom.development.js"></script>
<!-- <script src="../packages/myreact/dist/umd/index.development.js"></script> -->
<!-- <script src="../packages/myreact-dom/dist/umd/index.development.js"></script> -->
<!-- <script src="../bundle/react.development.js"></script> -->
<!-- <script src="../bundle/react-dom.development.js"></script> -->
<script src="../packages/myreact/dist/umd/index.development.js"></script>
<script src="../packages/myreact-dom/dist/umd/index.development.js"></script>

<!-- <script src="https://unpkg.com/preact@latest/dist/preact.umd.js"></script> -->
<!-- <script src="https://unpkg.com/preact@latest/hooks/dist/hooks.umd.js"></script> -->
Expand Down
74 changes: 46 additions & 28 deletions __tests__/testInput.html
Original file line number Diff line number Diff line change
@@ -1,33 +1,51 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../bundle/babel.min.js"></script>
<!-- <script src="../bundle/react.development.js"></script> -->
<!-- <script src="../bundle/react-dom.development.js"></script> -->
<script src="../packages/myreact/dist/umd/index.development.js"></script>
<script src="../packages/myreact-dom/dist/umd/index.development.js"></script>
</head>
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;

const Input = () => {
const [str, setStr] = useState("");
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../bundle/babel.min.js"></script>
<!-- <script src="../bundle/react.development.js"></script> -->
<!-- <script src="../bundle/react-dom.development.js"></script> -->
<script src="../packages/myreact/dist/umd/index.development.js"></script>
<script src="../packages/myreact-dom/dist/umd/index.development.js"></script>
</head>

return (
<>
<input type="text" value={str} onChange={(e) => setStr(e.target.value)} />
<input value={str} />
</>
);
};
<body>
<div id="root"></div>
<script type="text/babel">
const { useState } = React;

ReactDOM.render(<Input />, root);
</script>
</body>
</html>
const Input = () => {
const [str, setStr] = useState("");

return (
<>
<input type="text" value={str} onChange={(e) => setStr(e.target.value)} />
<input value={str} />
<input type='checkbox' defaultChecked='123' />
<label>
Pick a fruit:
<select name="selectedFruit" value='banana'>
<option value="apple">Apple</option>
<option value="banana">Banana</option>
<option value="orange">Orange</option>
</select>
</label>
<progress value={0} />
<progress value={0.5} />
<progress value={0.7} />
<progress value={75} max={100} />
<progress value={1} />
<progress value={null} />
</>
);
};

ReactDOM.render(<Input />, root);
</script>
</body>

</html>
14 changes: 14 additions & 0 deletions packages/myreact-dom/src/client/api/helper/control/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { generateInputOnChangeFun, hasControlledInputProps, isControlledInputElement, isReadonlyInputElement, updateControlInputElement } from "./input";
import { generateSelectOnChangeFun, hasControlledSelectProps, isControlledSelectElement, isReadonlySelectElement, updateControlSelectElement } from "./select";
import {
generateTextAreaOnChangeFun,
hasControlledTextAreaProps,
Expand All @@ -14,6 +15,7 @@ import type { MyReactFiberNode } from "@my-react/react-reconciler";
*/
export const controlElementTag: Record<string, boolean> = {
input: true,
select: true,
textarea: true,
};

Expand All @@ -26,6 +28,8 @@ export const updateControlElement = (fiber: MyReactFiberNode) => {
switch (elementType) {
case "input":
return updateControlInputElement(fiber);
case "select":
return updateControlSelectElement(fiber);
case "textarea":
return updateControlTextAreaElement(fiber);
}
Expand All @@ -40,6 +44,8 @@ export const generateOnChangeFun = (fiber: MyReactFiberNode) => {
switch (elementType) {
case "input":
return generateInputOnChangeFun(fiber);
case "select":
return generateSelectOnChangeFun(fiber);
case "textarea":
return generateTextAreaOnChangeFun(fiber);
}
Expand All @@ -54,6 +60,8 @@ export const hasControlledProps = (fiber: MyReactFiberNode) => {
switch (elementType) {
case "input":
return hasControlledInputProps(fiber);
case "select":
return hasControlledSelectProps(fiber);
case "textarea":
return hasControlledTextAreaProps(fiber);
}
Expand All @@ -67,6 +75,8 @@ export const isControlledElement = (fiber: MyReactFiberNode) => {
switch (elementType) {
case "input":
return isControlledInputElement(fiber);
case "select":
return isControlledSelectElement(fiber);
case "textarea":
return isControlledTextAreaElement(fiber);
}
Expand All @@ -80,7 +90,11 @@ export const isReadonlyElement = (fiber: MyReactFiberNode) => {
switch (elementType) {
case "input":
return isReadonlyInputElement(fiber);
case "select":
return isReadonlySelectElement(fiber);
case "textarea":
return isReadonlyTextAreaElement(fiber);
}
};

export { initSelect, updateSelect } from "./select";
168 changes: 168 additions & 0 deletions packages/myreact-dom/src/client/api/helper/control/select.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
import { type MyReactFiberNode } from "@my-react/react-reconciler";

import { log } from "@my-react-dom-shared";

type ControlledElement = HTMLSelectElement;

/**
* @internal
*/
export const isReadonlySelectElement = (fiber: MyReactFiberNode) => hasControlledSelectProps(fiber) && !fiber.pendingProps.onChange;

/**
* @internal
*/
export const isControlledSelectElement = (fiber: MyReactFiberNode) => hasControlledSelectProps(fiber) && typeof fiber.pendingProps.onChange === "function";

const generateEmptyChangeFun = (fiber: MyReactFiberNode) => {
return () => {
if (__DEV__) {
log(fiber, "warn", `current controlled element is a readonly element, please provider a 'onChange' props to make the value update`);
}
};
};

/**
* @internal
*/
export const generateSelectOnChangeFun = (fiber: MyReactFiberNode) => {
const onChange = (...args) => {
const originalOnChange = fiber.pendingProps.onChange;

const targetOnChange =
typeof originalOnChange !== "function"
? generateEmptyChangeFun(fiber)
: (...args) => {
originalOnChange?.call?.(null, ...args);
};

targetOnChange?.call?.(null, ...args);

requestAnimationFrame(() => {
const dom = fiber.nativeNode;

const props = fiber.pendingProps;

const typedDom = dom as ControlledElement;

const key = "value";

if (key in props) {
(typedDom as any)[key] = props[key];
}
});
};

return onChange;
};

/**
* @internal
*/
export const hasControlledSelectProps = (fiber: MyReactFiberNode) => {
const props = fiber.pendingProps;

const key = "value";

return props[key] !== undefined;
};

/**
* @internal
*/
export const updateControlSelectElement = (fiber: MyReactFiberNode) => {
const pendingProps = fiber.pendingProps;

const memoizedProps = fiber.memoizedProps;

const key = "value";

if (__DEV__) {
if (pendingProps[key] !== undefined) {
if (!(key in memoizedProps) || memoizedProps[key] === undefined) {
log(fiber, "warn", `current component change from 'unControlled' to 'controlled', this may case some bug`);
}
} else {
if (memoizedProps[key] !== undefined) {
log(fiber, "warn", `current component change from 'controlled' to 'unControlled', this may case some bug`);
}
}
}
};

function updateOptions(node: HTMLSelectElement, multiple: boolean, propValue: string | string[], setDefaultSelected: boolean) {
const options: HTMLOptionsCollection = node.options;

if (multiple) {
const selectedValues = propValue as Array<string>;
const selectedValue: { [key: string]: boolean } = {};
for (let i = 0; i < selectedValues.length; i++) {
// Prefix to avoid chaos with special keys.
selectedValue["$" + selectedValues[i]] = true;
}
for (let i = 0; i < options.length; i++) {
const selected = Object.prototype.hasOwnProperty.call(selectedValue, "$" + options[i].value);
if (options[i].selected !== selected) {
options[i].selected = selected;
}
if (selected && setDefaultSelected) {
options[i].defaultSelected = true;
}
}
} else {
// Do not set `select.value` as exact behavior isn't consistent across all
// browsers for all cases.
const selectedValue = String(propValue);
let defaultSelected = null;
for (let i = 0; i < options.length; i++) {
if (options[i].value === selectedValue) {
options[i].selected = true;
if (setDefaultSelected) {
options[i].defaultSelected = true;
}
return;
}
if (defaultSelected === null && !options[i].disabled) {
defaultSelected = options[i];
}
}
if (defaultSelected !== null) {
defaultSelected.selected = true;
}
}
}

export const initSelect = (fiber: MyReactFiberNode) => {
const element = fiber.nativeNode as HTMLSelectElement;
const multiple = fiber.pendingProps.multiple;
const value = fiber.pendingProps.value;
const defaultValue = fiber.pendingProps.defaultValue;
const node = element;
node.multiple = !!multiple;
if (value != null) {
updateOptions(node, !!multiple, value, false);
} else if (defaultValue != null) {
updateOptions(node, !!multiple, defaultValue, true);
}
};

export const updateSelect = (fiber: MyReactFiberNode) => {
const element = fiber.nativeNode as HTMLSelectElement;
const multiple = fiber.pendingProps.multiple;
const value = fiber.pendingProps.value;
const defaultValue = fiber.pendingProps.defaultValue;
const wasMultiple = fiber.memoizedProps.multiple;
const node = element;

if (value != null) {
updateOptions(node, !!multiple, value, false);
} else if (!!wasMultiple !== !!multiple) {
// For simplicity, reapply `defaultValue` if `multiple` is toggled.
if (defaultValue != null) {
updateOptions(node, !!multiple, defaultValue, true);
} else {
// Revert the select back to its default unselected state.
updateOptions(node, !!multiple, multiple ? [] : "", false);
}
}
};
5 changes: 4 additions & 1 deletion packages/myreact-dom/src/client/api/update/hydrateUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
log,
} from "@my-react-dom-shared";

import { XLINK_NS, XML_NS, X_CHAR, addEventListener, controlElementTag, setStyle } from "../helper";
import { XLINK_NS, XML_NS, X_CHAR, addEventListener, controlElementTag, initSelect, setStyle } from "../helper";

import { mountControl } from "./control";
import { isNoProps, isSameInnerHTML } from "./tool";
Expand Down Expand Up @@ -199,6 +199,9 @@ export const hydrateUpdate = (fiber: MyReactFiberNode, renderDispatch: ClientDom

if (enableEventSystem.current && enableControlComponent.current && controlElementTag[fiber.elementType as string]) {
mountControl(fiber, renderDispatch);
if (fiber.elementType === "select") {
requestAnimationFrame(() => initSelect(fiber));
}
}

domInnerHTMLHydrate(fiber);
Expand Down
12 changes: 10 additions & 2 deletions packages/myreact-dom/src/client/api/update/nativeUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { include } from "@my-react/react-shared";

import { enableControlComponent, enableEventSystem, isEvent, isProperty, isStyle } from "@my-react-dom-shared";

import { addEventListener, controlElementTag, removeEventListener, setAttribute, setStyle } from "../helper";
import { addEventListener, controlElementTag, initSelect, removeEventListener, setAttribute, setStyle, updateSelect } from "../helper";

import { mountControl, updateControl } from "./control";
import { getAllKeys } from "./tool";
Expand All @@ -12,6 +12,8 @@ import type { MyReactFiberNode } from "@my-react/react-reconciler";
import type { ClientDomDispatch } from "@my-react-dom-client/renderDispatch";
import type { DomElement, DomNode } from "@my-react-dom-shared";

const isFalse = (v: any) => v === null || v === undefined;

/**
* @internal
*/
Expand All @@ -36,7 +38,7 @@ export const nativeUpdate = (fiber: MyReactFiberNode, renderDispatch: ClientDomD
allKeys.forEach((key) => {
const oldValue = oldProps[key];
const newValue = newProps[key];
if (!Object.is(oldValue, newValue)) {
if (!Object.is(oldValue, newValue) && !(isFalse(newValue) && isFalse(oldValue))) {
if (isEvent(key)) {
removeEventListener(fiber, renderDispatch.runtimeMap.eventMap, node as DomElement, key);
addEventListener(fiber, renderDispatch.runtimeMap.eventMap, node as DomElement, key);
Expand All @@ -57,8 +59,14 @@ export const nativeUpdate = (fiber: MyReactFiberNode, renderDispatch: ClientDomD
if (enableEventSystem.current && enableControlComponent.current && controlElementTag[fiber.elementType as string]) {
if (isMount) {
mountControl(fiber, renderDispatch);
if (fiber.elementType === "select") {
requestAnimationFrame(() => initSelect(fiber));
}
} else {
updateControl(fiber, renderDispatch);
if (fiber.elementType === "select") {
requestAnimationFrame(() => updateSelect(fiber));
}
}
}

Expand Down
Loading

0 comments on commit ecded6f

Please sign in to comment.