Hi I am labbing with this code snippet and want to use it in Typescript but it complains about the useRef type and many more. Most importantly: it doesn't allow the next.focus();. It just says "Property 'focus' does not exist on type 'never'.ts(2339)"
Is it "not ok" in typescript to use the .focus() or do it want me to use onFocus or something?
The link to the Sandbox: https://codepen.io/vitalyq/pen/GRNXmQY?editors=0011
Here's the code in just React:
function useFocusNext() {
const controls = useRef([]);
const handler = (event) => {
if (event.keyCode === 13) {
const index = controls.current.indexOf(event.target);
const next = controls.current[index 1];
next && next.focus();
// IE 9, 10
event.preventDefault();
}
};
return useCallback((element) => {
if (element && !controls.current.includes(element)) {
controls.current.push(element);
element.addEventListener('keydown', handler);
}
}, []);
};
function Form() {
const focusNextRef = useFocusNext();
const [showMore, setShowMore] = useState(false);
const toggleMore = () => setShowMore((last) => !last);
return (
<>
<input ref={focusNextRef} placeholder="Field 1" />
{showMore && <>
<input ref={focusNextRef} placeholder="Added Field" />
</>}
<input ref={focusNextRef} placeholder="Field 2" />
<button ref={focusNextRef}>Submit</button>
<button onClick={toggleMore}>Toggle More Fields</button>
</>
);
};
ReactDOM.render(
<Form />,
document.getElementById('root')
);
And here's my code trying to transform it
const useFocusNext = () => {
const controls: MutableRefObject<never[]> = useRef([]);
const handler = (event: {
keyCode: number;
target: any;
preventDefault: () => void;
}) => {
if (event.keyCode === 13) {
if(event && event.target) {
const index: number = controls.current.indexOf(event.target as never);
const next = controls.current[index 1];
if (next) {
next.focus();
}
}
// IE 9, 10
event.preventDefault();
}
};
return useCallback((element) => {
if (element && !controls.current.includes(element as never)) {
controls.current.push(element as never);
element.addEventListener("keydown", handler);
}
}, []);
};
CodePudding user response:
You probably want to have an array of HTMLElement
so:
useRef<HTMLElement[]>([])
CodePudding user response:
The never
issue is that you're telling TypeScript that the elements in controls.current
will never exist; that's what the never
type says. Your controls will be HTMLElement
instances, so use HTMLElement[]
instead.
But the next problem is that you need a type on the element
parameter of the function you return from useFocusNext
, so TypeScript knows what values you're allowed to call it with. Also, in your handler
function, event
implicitly has the type any
(I recommend enabling noImplicitAny
so TypeScript flags this up); you'll want to say specifically it's a KeyboardEvent
. That would also have allowed your IDE to flag up that keyCode
is deprecated and shouldn't be used in new code; use key
instead.
See the indicated portions below:
function useFocusNext() {
const controls = useRef<HTMLElement[]>([]);
// −−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^
const handler = (event: KeyboardEvent) => {
// −−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
if (event.key === "Enter") {
// ^^^^^^^^^^^^^^^^
const index = controls.current.indexOf(event.target as HTMLElement);
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^
const next = controls.current[index 1];
if (next) {
next.focus();
}
// IE 9, 10
event.preventDefault();
}
};
return useCallback((element: HTMLElement | null) => {
// −−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^
if (element && !controls.current.includes(element)) {
controls.current.push(element);
element.addEventListener("keydown", handler);
}
}, []);
}
(No changes to Form
were required.)
That said, I would avoid having code adding event listeners it never removes, and adding event listeners to each and every element in the form. Instead, I'd use a single event listener on the form, something along these lines:
function useFocusNext() {
const formRef = useRef<HTMLFormElement>(null);
useEffect(() => {
const form = formRef.current;
if (!form) {
return;
}
// assert(form);
const handler = (event: KeyboardEvent) => {
if (event.key === "Enter" && event.target) {
event.preventDefault();
const elements = Array.from(form.querySelectorAll(".handle-next")) as HTMLElement[];
const index = elements.indexOf(event.target as HTMLElement);
const next = elements[index 1];
if (next) {
next.focus();
}
}
};
form.addEventListener("keydown", handler);
return () => {
form.removeEventListener("keydown", handler);
}
}, []);
return formRef;
}
function Form(props: any) {
const formRef = useFocusNext();
const [showMore, setShowMore] = useState(false);
const toggleMore = () => setShowMore((last) => !last);
return (
<form {...props} ref={formRef}>
<input className="handle-next" placeholder="Field 1" />
{showMore && (
<input className="handle-next" placeholder="Added Field" />
)}
<input className="handle-next" placeholder="Field 2" />
<button className="handle-next">Submit</button>
<button onClick={toggleMore}>Toggle More Fields</button>
</form>
);
}
That adds a form
element around the form and handles events on it; the fields you want the Enter handled on are indicated with a class. (Or it could be a data-*
attribute or something.) And the event handler is cleaned up when the component instance using it is unmounted.
Live Example:
const { useState, useRef, useEffect } = React;
function useFocusNext() {
const formRef = useRef/*<HTMLFormElement>*/(null);
useEffect(() => {
const form = formRef.current;
if (!form) {
return;
}
// assert(form);
const handler = (event/*: KeyboardEvent*/) => {
if (event.key === "Enter" && event.target) {
event.preventDefault();
const elements = Array.from(form.querySelectorAll(".handle-next"))/* as HTMLElement[]*/;
const index = elements.indexOf(event.target/* as HTMLElement*/);
const next = elements[index 1];
if (next) {
next.focus();
}
}
};
form.addEventListener("keydown", handler);
return () => {
form.removeEventListener("keydown", handler);
}
}, []);
return formRef;
}
function Form(props: any) {
const formRef = useFocusNext();
const [showMore, setShowMore] = useState(false);
const toggleMore = () => setShowMore((last) => !last);
return (
<form {...props} ref={formRef}>
<input className="handle-next" placeholder="Field 1" />
{showMore && (
<input className="handle-next" placeholder="Added Field" />
)}
<input className="handle-next" placeholder="Field 2" />
<button className="handle-next">Submit</button>
<button onClick={toggleMore}>Toggle More Fields</button>
</form>
);
}
const Example = () => {
return <Form/>;
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
That code has to have a couple of type assertions to tell TypeScript that we know we're dealing with HTMLElement
instances, not just Element
instances, since querySelectorAll
returns a NodeListOf<Element>
and event.target
's type is just EventTarget
. If you find yourself having to query HTML elements often, you'd probably be better off with a utility function with a more specific return type, and maybe even a subtype of KeyboardEvent
that refines the type of target
.