FRAMEWORKS
React
React 19 supports custom elements directly. It sets arrays, objects, and booleans as properties, and it listens for custom events through on props. You do not need wrapper components.
Install and register
npm install @dojo-ng/select @dojo-ng/chip @dojo-ng/text-input
Each package registers its tag when you import it. Import the packages once, in your entry file, before the first render:
// main.jsx: import the components once, before anything renders
import "@dojo-ng/select";
import "@dojo-ng/chip";
import "@dojo-ng/text-input";
import { createRoot } from "react-dom/client";
import App from "./App.jsx";
createRoot(document.getElementById("root")).render(<App />);
The order matters. React decides between a property and an attribute when it renders the tag. If the package is not imported yet, React cannot see the property, so it writes an attribute instead, and an array becomes the text "[object Object]".
An example
import { useState } from "react";
const plans = [
{ value: "free", label: "Free" },
{ value: "pro", label: "Pro" },
];
export default function App() {
const [plan, setPlan] = useState("free");
const [name, setName] = useState("");
const [tags, setTags] = useState(["design", "docs"]);
return (
<>
<dj-select
label="Plan"
options={plans}
value={plan}
onChange={(e) => setPlan(e.target.value)}
></dj-select>
<dj-text-input
label="Name"
value={name}
onInput={(e) => setName(e.target.value)}
></dj-text-input>
{tags.map((tag) => (
<dj-chip
key={tag}
closeable
ondj-close={() => setTags(tags.filter((t) => t !== tag))}
>
{tag}
</dj-chip>
))}
</>
);
}
Properties and attributes
- When the element has a property with the prop's name, React sets the property. Arrays, objects, booleans, and functions arrive as they are, such as
options={plans}above. - Other props become attributes, so
aria-*anddata-*work as usual. - Use the property names from the API reference. Where a property also has an attribute with a different name, such as
helperTextandhelper-text, either works.
Events
- For a Dojo NG event, write
onfollowed by the exact event name:ondj-close,ondj-submit,ondj-select. The name is case-sensitive and keeps its dash. - The standard events that cross the shadow boundary, such as
input,change, andclick, work with React's usualonInput,onChange, andonClick. - The event data is in
event.detail. Each component's API page lists its events.
Forms
- For a controlled text input, pass
valueand update it inonInput, as in the example. - For a checkbox or switch, pass
checkedand reade.target.checkedinonChange. - For a select, pass
valueand reade.target.valueinonChange. - The controls are form-associated, so an uncontrolled form also works: read the values with
new FormData(form)inonSubmit. See Forms & validation.
TypeScript
TypeScript does not know the dj-* tags. Add this declaration once so they type-check:
// dj-elements.d.ts, anywhere in your TypeScript project
import "react";
declare module "react" {
namespace JSX {
interface IntrinsicElements {
[tag: `dj-${string}`]: any;
}
}
}
React 18 and earlier
React 18 writes every prop as an attribute and cannot listen for custom events. Set arrays and objects, and add event listeners, through a ref:
// React 18: set rich data and listen for events through a ref
const ref = useRef(null);
useEffect(() => {
const el = ref.current;
el.options = plans;
const onClose = () => setLabel("(cleared)");
el.addEventListener("dj-close", onClose);
return () => el.removeEventListener("dj-close", onClose);
}, []);
Strings still work as props, because the components read them from attributes. Set booleans through the ref as well.