Event handler rename codemod

Renames the deprecated onChange event handler to onCheckedChange on the Pluma form-control components that standardized on onCheckedChange in v3:

table
BeforeAfter
<Checkbox onChange={…} /><Checkbox onCheckedChange={…} />
<Radio onChange={…} /><Radio onCheckedChange={…} />
<OptionCard onChange={…} /><OptionCard onCheckedChange={…} />

Affected components: Checkbox, Radio, OptionCard (React) and PlumaCheckbox, PlumaRadio, PlumaOptionCard (Ember). The group components RadioGroup / CheckboxGroup / OptionCardGroup keep their own onChange prop and are not touched.

There are two transforms — one for React (.tsx/.ts/.jsx/.js) and one for Ember (.gts/.gjs/.hbs). Run whichever apply to your app.

Running it

Run it through the Pluma CLI from inside your consuming app. Always start with a dry run to review the diff before applying:

# from the root of your consuming app

# Dry run first (no writes; per-file diff printed inline)
pnx @customerio/pluma-cli@latest upgrade --codemod event-handler-rename react --dry ./src
pnx @customerio/pluma-cli@latest upgrade --codemod event-handler-rename ember --dry ./app

# Then apply for real
pnx @customerio/pluma-cli@latest upgrade --codemod event-handler-rename react ./src
pnx @customerio/pluma-cli@latest upgrade --codemod event-handler-rename ember ./app ./addon

Both frameworks accept --dry / -d. The React path also forwards standard jscodeshift flags, so -p prints transformed output.

What gets matched

React. Only JSX elements whose tag resolves (via the import in the same file) to Checkbox, Radio, or OptionCard from @customerio/pluma-components/react (or the bare @customerio/pluma-components entry). Aliased imports (import { Checkbox as C } from …) are handled. A local component named Checkbox that wasn't imported from Pluma is left alone, as is onChange on any native <input> or on another component.

Ember. Only @onChange on the <PlumaCheckbox …>, <PlumaRadio …>, and <PlumaOptionCard …> element nodes is renamed. @onChange on any other tag — including <PlumaRadioGroup> / <PlumaCheckboxGroup> — is left alone.

After running, you can grep for any leftover onChange on these components to catch anything reached through indirection the codemod can't see.

Callback auto-fix

For the narrow case where it can prove the rewrite is safe, the codemod also migrates the callback to the new signature:

- <Checkbox onChange={(e) => setIsDisabled(e.target.checked)} />
+ <Checkbox onCheckedChange={(checked) => setIsDisabled(checked)} />

It auto-fixes only when the callback has exactly one parameter whose every use is <param>.target.checked / <param>.currentTarget.checked — nothing else (no e.preventDefault(), no e.target.value, not passed along). The param becomes a checked boolean (or isChecked, if checked is already in scope, to avoid a no-shadow). If both names are taken, or the body contains a nested function, it stays rename-only.

In React this applies to two shapes:

  • Inline callbacksonChange={(e) => …} / onChange={function (e) { … }}.
  • Same-file named handlersonChange={myFn} where myFn is a const/let/function/useCallback binding defined in the same file. The definition is rewritten once.

In Ember it applies to strict-mode .gts/.gjs backing-class methods — see Ember below. Classic .hbs stays rename-only.

Ember

For strict-mode .gts/.gjs (template co-located with the component class), the codemod also rewrites the backing class method:

  export default class Demo extends Component {
    @action
-   handleToggle(event) {
-     this.value = event.target.checked;
+   handleToggle(checked) {
+     this.value = checked;
    }

    <template>
-     <PlumaCheckbox @onChange={{this.handleToggle}} />
+     <PlumaCheckbox @onCheckedChange={{this.handleToggle}} />
    </template>
  }

Same safety rule as React: this.handleToggle's signature is rewritten only when every reference to it is a migrated @onChange site on PlumaCheckbox/PlumaRadio/PlumaOptionCard. If it's also wired to {{on "change" this.handleToggle}}, passed via {{fn this.handleToggle}}, called elsewhere in the class, or used by any other component, the method is left alone and the site is reported for a manual fix. The body must match the same narrow pattern (single event param used only as .target.checked / .currentTarget.checked). Both @action/method and class-field-arrow shapes are handled. Under the hood the .gts/.gjs file is split into JS + template regions with content-tag; if anything about the round-trip looks off, the transform falls back to rename-only rather than risk touching the file.

Classic .hbs (template + a separate .js/.ts class file) stays rename-only + report: the backing class is in another file the codemod can't see, so update those methods by hand.

Review output

After a run the codemod prints a per-site checklist, split into two groups so you know exactly what still needs attention. It prints in both dry and write runs:

✅ Auto-migrated — double-check these rewritten callbacks — 1 site across 1 file:
  src/Form.tsx (1):
    L5  — `<Checkbox>` `onChange`→`onCheckedChange`: callback auto-migrated to a `checked` param — verify.

📝 Renamed — these callbacks still need a manual fix — 1 site across 1 file:
  src/Form.tsx (1):
    L6  — `<Radio>` `onChange`→`onCheckedChange`: signature changed from `(event)` to `(checked, event)` — update the callback to take `checked` as its first argument.
  • Auto-migrated (verify): the callback body was rewritten — sanity-check it.
  • Needs manual callback fix: renamed only (named/shared handlers that aren't exclusive to migrated sites, complex bodies, name collisions, and classic .hbs Ember sites). Update these by hand to read the new checked argument.

Callbacks that provably ignore their argument (0 params, or a single unused param) are renamed silently — they need no fix and aren't listed.

Working on the codemod itself

The React/Ember transform engines and reporting are shared across the attribute-rename codemods under ../../_shared/; this codemod is a thin config wrapper (attrRenames for the rename, attrRenameNotes for the review text, and the opt-in rewriteCheckedCallback for the React callback auto-fix — the other codemods don't set it, so they're unaffected). There's a test suite under tests/:

pnpm --filter @customerio/pluma-cli run test:transforms

It uses Node's built-in test runner (node --test).