diff --git a/src/@types/global.d.ts b/src/@types/global.d.ts index 098220615..5d7d5dedf 100644 --- a/src/@types/global.d.ts +++ b/src/@types/global.d.ts @@ -8,6 +8,7 @@ declare namespace React { interface HTMLAttributes { // Optimization for DOM nodes prepends and inserts teactFastList?: boolean; + teactExperimentControlled?: boolean; } // Teact feature diff --git a/src/components/right/management/ManageChatPrivacyType.tsx b/src/components/right/management/ManageChatPrivacyType.tsx index 5deb523b2..75940fd67 100644 --- a/src/components/right/management/ManageChatPrivacyType.tsx +++ b/src/components/right/management/ManageChatPrivacyType.tsx @@ -88,6 +88,7 @@ const ManageChatPrivacyType: FC = ({ openLimitReachedModal({ limit: 'channelsPublic' }); const radioGroup = e.currentTarget.closest('.radio-group') as HTMLDivElement; // Patch for Teact bug with controlled inputs + // TODO Teact support added, this can now be removed (radioGroup.querySelector('[value=public]') as HTMLInputElement).checked = false; (radioGroup.querySelector('[value=private]') as HTMLInputElement).checked = true; return; diff --git a/src/components/right/management/ManageGroup.tsx b/src/components/right/management/ManageGroup.tsx index 978102955..5a9eaeb1b 100644 --- a/src/components/right/management/ManageGroup.tsx +++ b/src/components/right/management/ManageGroup.tsx @@ -202,6 +202,7 @@ const ManageGroup: FC = ({ } // Teact does not have full support of controlled form components, we need to "disable" input value change manually + // TODO Teact support added, this can now be removed const checkbox = isPreHistoryHiddenCheckboxRef.current?.querySelector('input') as HTMLInputElement; checkbox.checked = !chat.fullInfo?.isPreHistoryHidden; }, [isChannelsPremiumLimitReached, chat.fullInfo?.isPreHistoryHidden]); diff --git a/src/components/test/TestControlledInput.tsx b/src/components/test/TestControlledInput.tsx new file mode 100644 index 000000000..e92d55690 --- /dev/null +++ b/src/components/test/TestControlledInput.tsx @@ -0,0 +1,58 @@ +import type { FC } from '../../lib/teact/teact'; +import React, { useState } from '../../lib/teact/teact'; + +const Test: FC = () => { + const [inputValue, setInputValue] = useState('Controlled'); + const [isCheckboxAllowed, setIsCheckboxAllowed] = useState(true); + const [isChecked, setIsChecked] = useState(true); + + function removeVowels(e: React.FormEvent) { + const nextValue = e.currentTarget.value.replace(/[aeiou]/g, ''); + // eslint-disable-next-line no-console + console.log('!!!', { nextValue }); + setInputValue(nextValue); + } + + function handleAllowCheckbox() { + setIsCheckboxAllowed((current) => !current); + } + + function handleCheck() { + if (!isCheckboxAllowed) { + return; + } + + setIsChecked((current) => !current); + } + + return ( + <> +
+ +
+ +
+
Input value: {inputValue}
+ +
+ +
+ +
Checkbox value: {String(isChecked)}
+
+ +
+
+ +
+ + ); +}; + +export default Test; diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 68576d5d4..9e440d6c6 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -27,7 +27,8 @@ type VirtualDomHead = { }; const FILTERED_ATTRIBUTES = new Set(['key', 'ref', 'teactFastList', 'teactOrderKey']); -const HTML_ATTRIBUTES = new Set(['dir', 'role']); +const HTML_ATTRIBUTES = new Set(['dir', 'role', 'form']); +const CONTROLLABLE_TAGS = ['INPUT', 'TEXTAREA', 'SELECT']; const MAPPED_ATTRIBUTES: { [k: string]: string } = { autoPlay: 'autoplay', autoComplete: 'autocomplete', @@ -250,12 +251,16 @@ function createNode($element: VirtualElementReal): Node { props.ref.current = element; } + processControlled(tag, props); + Object.entries(props).forEach(([key, value]) => { if (props[key] !== undefined) { setAttribute(element, key, value); } }); + processUncontrolledOnMount(element, props); + $element.children = children.map(($child, i) => ( renderWithVirtual(element, undefined, $child, $element, i) )); @@ -504,7 +509,55 @@ function renderFragment( return newChildren; } +function processControlled(tag: string, props: AnyLiteral) { + // TODO Remove after tests + if (!props.teactExperimentControlled) { + return; + } + + const isValueControlled = props.value !== undefined; + const isCheckedControlled = props.checked !== undefined; + const isControlled = (isValueControlled || isCheckedControlled) && CONTROLLABLE_TAGS.includes(tag.toUpperCase()); + if (!isControlled) { + return; + } + + const { + value, checked, onInput, onChange, + } = props; + + props.onChange = undefined; + props.onInput = (e: React.ChangeEvent) => { + onInput?.(e); + onChange?.(e); + + if (value !== undefined) { + e.currentTarget.value = value; + } + + if (checked !== undefined) { + e.currentTarget.checked = checked; + } + }; +} + +function processUncontrolledOnMount(element: HTMLElement, props: AnyLiteral) { + if (!CONTROLLABLE_TAGS.includes(element.tagName)) { + return; + } + + if (props.defaultValue) { + setAttribute(element, 'value', props.defaultValue); + } + + if (props.defaultChecked) { + setAttribute(element, 'checked', props.defaultChecked); + } +} + function updateAttributes($current: VirtualElementParent, $new: VirtualElementParent, element: HTMLElement) { + processControlled(element.tagName, $new.props); + const currentEntries = Object.entries($current.props); const newEntries = Object.entries($new.props);