Browse Source

Extract proxy hook

Put proxy hook into react-utils.
master
TheoryOfNekomata 1 year ago
parent
commit
9643e56622
5 changed files with 139 additions and 61 deletions
  1. +8
    -44
      categories/formatted/react/src/components/PhoneNumberInput/index.tsx
  2. +78
    -0
      packages/react-utils/src/hooks/form.ts
  3. +6
    -0
      packages/react-utils/src/hooks/id.ts
  4. +2
    -0
      packages/react-utils/src/index.ts
  5. +45
    -17
      showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx

+ 8
- 44
categories/formatted/react/src/components/PhoneNumberInput/index.tsx View File

@@ -1,6 +1,6 @@
import * as React from 'react'; import * as React from 'react';
import { TextControl } from '@tesseract-design/web-base'; import { TextControl } from '@tesseract-design/web-base';
import { delegateTriggerEvent, useClientSide } from '@modal-sh/react-utils';
import { useClientSide, useProxyInput } from '@modal-sh/react-utils';
import PhoneInput, { Country, Value } from 'react-phone-number-input/input'; import PhoneInput, { Country, Value } from 'react-phone-number-input/input';
import clsx from 'clsx'; import clsx from 'clsx';


@@ -82,49 +82,13 @@ export const PhoneNumberInput = React.forwardRef<
const labelId = React.useId(); const labelId = React.useId();
const defaultId = React.useId(); const defaultId = React.useId();
const id = idProp ?? defaultId; const id = idProp ?? defaultId;
const defaultRef = React.useRef<PhoneNumberInputDerivedElement>(null);

React.useEffect(() => {
const { current: currentRaw } = defaultRef;
const current = currentRaw as unknown as PhoneNumberInputDerivedElement;
const forwardedRefProxy = new Proxy(current, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver) as unknown;
},
set(target, prop, newValue, receiver) {
if (prop === 'value') {
setPhoneNumber(newValue as string);
current.value = newValue as string;
return true;
}
return Reflect.set(target, prop, newValue, receiver);
},
}) as unknown as PhoneNumberInputDerivedElement;

if (typeof forwardedRef === 'function') {
forwardedRef(forwardedRefProxy);
return;
}

if (typeof forwardedRef === 'object' && forwardedRef) {
const mutableForwardedRef = forwardedRef as React.MutableRefObject<
PhoneNumberInputDerivedElement
>;
mutableForwardedRef.current = forwardedRefProxy;
}
}, [forwardedRef, defaultRef]);

const handlePhoneInputChange = (phoneNumberValue: Value) => {
if (!(typeof defaultRef === 'object' && defaultRef)) {
return;
}
const { current: input } = defaultRef;
if (!input) {
return;
}
setPhoneNumber(phoneNumberValue);
delegateTriggerEvent('change', input, phoneNumberValue ?? '');
};
const {
defaultRef,
handleChange: handlePhoneInputChange,
} = useProxyInput<PhoneNumberInputDerivedElement, Value>({
forwardedRef,
valueSetterFn: (v) => setPhoneNumber(v),
});


const commonInputStyles = clsx( const commonInputStyles = clsx(
'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums', 'bg-negative rounded-inherit w-full peer block font-inherit tabular-nums',


+ 78
- 0
packages/react-utils/src/hooks/form.ts View File

@@ -0,0 +1,78 @@
import * as React from 'react';
import { delegateTriggerEvent } from '../event';

type ValueType = string | number | readonly string[];

interface HTMLElementWithValue extends HTMLElement {
value: ValueType;
}

export interface UseProxyInputOptions<Element extends HTMLElementWithValue, Value = ValueType> {
forwardedRef: React.Ref<Element>;
valueSetterFn: (value: Value) => void;
transformChangeHandlerArgs?: (...e: unknown[]) => Value;
}

export const useProxyInput = <
T extends HTMLElementWithValue = HTMLElementWithValue,
V = ValueType,
>(options: UseProxyInputOptions<T, V>) => {
const {
forwardedRef,
valueSetterFn,
transformChangeHandlerArgs = (e) => e as V,
} = options;
const defaultRef = React.useRef<T>(null);

React.useEffect(() => {
const { current: currentRaw } = defaultRef;
const current = currentRaw as unknown as T;
const forwardedRefProxy = new Proxy(current, {
get(target, prop, receiver) {
return Reflect.get(target, prop, receiver) as unknown;
},
set(target, prop, newValue, receiver) {
if (prop === 'value') {
const thisNewValue = newValue as V;
const actualNewValue = (
Array.isArray(thisNewValue) ? thisNewValue.join('') : thisNewValue?.toString() ?? ''
) as V;
valueSetterFn(actualNewValue);
current.value = actualNewValue as string;
return true;
}
return Reflect.set(target, prop, newValue, receiver);
},
}) as unknown as T;

if (typeof forwardedRef === 'function') {
forwardedRef(forwardedRefProxy);
return;
}

if (typeof forwardedRef === 'object' && forwardedRef) {
const mutableForwardedRef = forwardedRef as React.MutableRefObject<T>;
mutableForwardedRef.current = forwardedRefProxy;
}
});

const handleChange = React.useCallback((e: unknown) => {
if (!(typeof defaultRef === 'object' && defaultRef)) {
return;
}
const { current: input } = defaultRef;
if (!input) {
return;
}
valueSetterFn(transformChangeHandlerArgs(e));
delegateTriggerEvent('change', input, e);
}, [valueSetterFn, transformChangeHandlerArgs, defaultRef]);

return React.useMemo(() => ({
defaultRef,
handleChange,
}), [
defaultRef,
handleChange,
]);
};

+ 6
- 0
packages/react-utils/src/hooks/id.ts View File

@@ -0,0 +1,6 @@
import * as React from 'react';

export const useFallbackId = (id?: string) => {
const defaultId = React.useId();
return id || defaultId;
};

+ 2
- 0
packages/react-utils/src/index.ts View File

@@ -1,2 +1,4 @@
export * from './hooks/client-side'; export * from './hooks/client-side';
export * from './hooks/form';
export * from './hooks/id';
export * from './event'; export * from './event';

+ 45
- 17
showcases/web-kitchensink-reactnext/src/pages/categories/formatted/index.tsx View File

@@ -22,23 +22,51 @@ const TemporalPage: NextPage = () => {
/> />
</Subsection> </Subsection>
<Subsection title="With Ref"> <Subsection title="With Ref">
<Formatted.PhoneNumberInput
label="Phone"
name="phone"
enhanced
border
ref={phoneNumberRef}
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}}
/>
<ActionButton
onClick={() => {
if (phoneNumberRef.current) {
phoneNumberRef.current.value = '+639123456789';
}
}}
>
Set Value
</ActionButton>
<div className="flex gap-4 flex-wrap">
<div>
<Formatted.PhoneNumberInput
label="Phone"
name="phone"
enhanced
border
ref={phoneNumberRef}
onChange={(e) => { console.log('change', e.currentTarget.name, e.currentTarget, e.currentTarget.value)}}
/>
</div>
<div>
<ActionButton
onClick={() => {
if (phoneNumberRef.current) {
phoneNumberRef.current.value = '+639123456789';
}
}}
>
Set Value
</ActionButton>
</div>
<div>
<ActionButton
onClick={() => {
if (phoneNumberRef.current) {
phoneNumberRef.current.value = '+63465123456';
}
}}
>
Set Other Value
</ActionButton>
</div>
<div>
<ActionButton
onClick={() => {
if (phoneNumberRef.current) {
phoneNumberRef.current.value = '+63288888888';
}
}}
>
Set Yet Other Value
</ActionButton>
</div>
</div>
</Subsection> </Subsection>
</Section> </Section>
<Section title="EmailInput"> <Section title="EmailInput">


Loading…
Cancel
Save