Dojo NG is being built in the open, one section at a time — follow along on Heptapod.

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-* and data-* work as usual.
  • Use the property names from the API reference. Where a property also has an attribute with a different name, such as helperText and helper-text, either works.

Events

  • For a Dojo NG event, write on followed 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, and click, work with React's usual onInput, onChange, and onClick.
  • The event data is in event.detail. Each component's API page lists its events.

Forms

  • For a controlled text input, pass value and update it in onInput, as in the example.
  • For a checkbox or switch, pass checked and read e.target.checked in onChange.
  • For a select, pass value and read e.target.value in onChange.
  • The controls are form-associated, so an uncontrolled form also works: read the values with new FormData(form) in onSubmit. 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.

Next: Vue Properties, events, and v-model with Vue 3.