Skip to content

EPBDS-16211 E2: the five components #132 converted, as TypeScript - #133

Merged
AlexSamBY merged 1 commit into
masterfrom
e2-ts-hooks-batch
Sep 30, 2026
Merged

AlexSamBY merged 1 commit into
masterfrom
e2-ts-hooks-batch

Conversation

@AlexSamBY

Copy link
Copy Markdown
Member

What changes

The five library components #132 converted to hooks become TypeScript. All five are renamed with git mv, so their history follows.

file notes
engine/Data.tsx DataProps; its instance typed as what the draft row reads off it
engine/components/PopupContent.tsx PopupContentProps; the memoized function is still created per call
engine/components/LocalDraftTableRow.tsx DraftItem, LocalDraftTableRowProps, typed state and validators
modules/upload/views/Upload.tsx UploadProps, and a DropzoneHandle for the ref
modules/form/inputs/ToggleField.tsx ToggleFieldProps, and a typed context for the inner component

Out of this batch, on purpose:

  • mapper.js's own PopupContent waits for mapper.js itself (E3).
  • The demo's Examples stays JSX, which the definition of done allows.

The pilot's conventions apply (§9.6 governance):

  • exported prop types;
  • still-JavaScript components cast to open prop bags, each cast to be deleted when its component converts;
  • propTypes kept, on the inner function, which React checks through memo. E5 removes them separately.

Two defects of my own, caught on the way and fixed

  1. A module-level cast of a cyclic import. Data first cast its import of rules.js in a top-level const. But rules.js → mapper.js → Data → rules.js is an import cycle, so while Data is evaluated that export is still undefined, and the const kept the undefined for good. Every nested form-backed document then rendered nothing, and six engine tests failed on it. The cast now happens at render, which sees the live binding as the .js did; so does PopupContent's cast of Render. CLAUDE.md states the rule for the rest of the TypeScript tail. Components can be cast at the top of a file: none imports the engine (checked).

  2. Expressions restructured for the checker.

    • A type guard on the add button, typeof oc === 'object', dropped the case where the engine hands the action as a function named addData.
    • Splitting e && e.target ? … : e into two conditionals added a branch the tests never take, and LocalDraftTableRow's 93% branch floor caught it.

    Both are the original expressions again, with casts.

Measured

  • The corpus is identical to master. Over the 38 examples through the published entry: commits at mount 107, translate calls 1013, and 27 / 64 on an edit. The DOM is identical after mount and after an edit.
  • The TypeScript share grew. src/core + src/library hold 35 TypeScript files, 7,586 lines (was 30 and 6,677), against 122 JavaScript files, 12,052 lines. That is 38.6% of the lines, up from 34.4%.
  • The bundle is essentially unchanged. dist/index.js is 327,402 → 327,475 bytes: the render-time casts and one restated guard.

Found, and left for a change of its own

LocalDraftTableRow treats maxLength, a validator factory (maxLength(length) returns the validator), as a validator. A draft whose meta says validate: 'maxLength' therefore records a function as the field's error and can never be added. No tracked example does it. The typed cast there says so rather than hiding it.

Housekeeping

  • The coverage thresholds in jest.config.js follow the renamed files.
  • docs/SUPPORTED-PROPS.md is regenerated; only the paths changed.
  • Comments that named the old .js paths name the new ones.

Gates

All exited 0:

  • lint:js, lint:css, typecheck, typecheck:contract;
  • docs:props:check, docs:views:check, css:fixture:check;
  • test:env-flags, test:coverage (thresholds included), build;
  • test:pack, test:pack:peers, test:types;
  • test:react16, test:react17, test:react19, 2907 tests each;
  • test:e2e, 41 tests;
  • the manifest contract.

🤖 Generated with Claude Code

Data, PopupContent, LocalDraftTableRow, Upload and ToggleField become .tsx,
renamed with git mv so their history follows. mapper.js's own PopupContent
waits for mapper.js (E3), and the demo's Examples stays JSX, as §9.6's
definition of done allows.

The pilot's conventions:
- exported prop types;
- unconverted components cast to open prop bags;
- propTypes kept, on the inner function, which React checks through `memo`.
  E5 removes them separately.

Two defects of my own were caught on the way and fixed:
- Data cast its import of rules.js in a module-level const. rules.js ->
  mapper.js -> Data -> rules.js is an import cycle, so the const captured
  the still-undefined export for good, and every nested form-backed document
  rendered nothing. Six engine tests failed on it. The cast now happens at
  render, as does PopupContent's cast of Render, and CLAUDE.md states the
  rule.
- Expressions restructured for the checker changed behaviour. A type guard
  on the add button's `onClick` dropped the function the engine resolves the
  action to. Splitting `e && e.target ? ... : e` added a branch the tests
  never take, which LocalDraftTableRow's 93% branch floor caught. Both are
  the original expressions again, with casts.

Measured on the corpus (38 examples, published entry), identical to master:
- commits at mount 107 and `translate` calls 1013;
- on an edit, 27 commits and 64 `translate` calls;
- the same DOM after mount and after an edit.
TypeScript is now 38.6% of the lines in src/core + src/library (was 34.4%).

Found while typing and left as it was: LocalDraftTableRow treats
`maxLength`, a validator factory, as a validator. A draft whose meta says
`validate: 'maxLength'` can never be added. The cast there says so.

The coverage thresholds follow the renamed files, SUPPORTED-PROPS.md is
regenerated for the new paths, and comments that named the old .js paths
name the new ones.
@AlexSamBY
AlexSamBY merged commit 0a827ec into master Sep 30, 2026
5 checks passed
@AlexSamBY
AlexSamBY deleted the e2-ts-hooks-batch branch September 30, 2026 13:32
AlexSamBY added a commit that referenced this pull request Sep 30, 2026
…peScript (#134)

## What changes

Eight leaf components become TypeScript:
- the seven the first batch (#133) still had to cast: `View`, `Text`,
`Row`, `Icon`, `Button`, `Loading`, `Checkbox`;
- `Spinner`, which only `Loading` renders, so that `Loading` needs no
new cast.

All eight are renamed with `git mv`. Git detects every rename (52–65%
similarity), so their history follows.

| file | notes |
|---|---|
| `View.tsx` | `ViewProps`: the props it reads, plus an open rest bag
for the `<div>` |
| `Row.tsx` | `RowProps` is `ViewProps`; `RowRef` is typed as taking a
callback ref only (below) |
| `Text.tsx` | `TextProps`; the config context is read through a
commented cast |
| `Icon.tsx` | `IconProps`, with `name` required, as its `propTypes` say
|
| `Button.tsx` | `ButtonProps`; `type` is the DOM's `'button' \|
'submit' \| 'reset'` |
| `Loading.tsx`, `Spinner.tsx` | `LoadingProps`, `SpinnerProps`; size
and color are the `propTypes` lists |
| `Checkbox.tsx` | `CheckboxProps`, `CheckboxChangeHandler` |

**The casts that stood in for these components are deleted:**
- five in `InputNumber`;
- five in `Upload`;
- one in `LocalDraftTableRow`;
- one in `ToggleField`.

Those call sites are now checked against the real prop types. A
throwaway probe of six wrong call sites failed on all six, and its six
correct counterparts compiled:
- an `Icon` without `name`;
- a button `type` outside the DOM's three;
- a spinner size outside its list;
- an object ref on `RowRef`;
- a mistyped `onChange`;
- a numeric `className`.

The first batch's conventions apply:
- exported prop types;
- the rest bag typed `unknown`, because it is forwarded and never
inspected;
- `propTypes` kept on the inner function.

## Runtime: one literal changes

A readonly `Checkbox` now passes `undefined` instead of `null` as its
input's `onChange`. The DOM typings reject `null`, and React attaches no
listener for either value. The two readonly tests pass on 16, 17, 18 and
19.

Everything else is types and casts, which Babel strips. Two casts keep a
JavaScript behaviour on purpose, and their comments say so:
- `Text` still throws outside a `ConfigContext` provider.
- `Checkbox` still throws on a change when it has no `onChange`.

## Two comments were wrong

I checked both with a probe on all four fixture legs, before and after
the change; the results were identical.

- **`RowRef` attaches a callback ref only.** An object ref from
`createRef()` stays `null` on every React, although the comment said it
works.
- The cause: `Row` attaches its second argument only if it is a
function. Through the default export's `React.memo`, that argument holds
React's legacy context instead of a ref: a frozen `{}` on 16–18,
`undefined` on 19.
  - A new test pins the real behaviour, and the type now states it.
- **On React 19, a `ref` reaches the element.** `View`'s note said a
`ref` can never arrive.
  - On 16–18 the ref is dropped, with React's warning.
- On 19, `ref` is an ordinary prop. It rides the rest bag onto the
`<div>`, and the same holds for `Row`, `Button` and `Icon`.
  - No caller passes one.

I also removed an "Enter press" that the `onClick` docs of `View`,
`Text`, `Row` and `Icon` promised; none of them implements it.

## Measured

- **The corpus is identical to master.** Over the 38 examples through
the published entry: commits at mount 107, `translate` calls 1013, and
27 / 64 on an edit. The DOM is identical after mount and after an edit.
- **The TypeScript share grew.** `src/core` + `src/library` hold 43
TypeScript files, 8,090 lines (was 35 and 7,586), against 114 JavaScript
files, 11,628 lines (was 122 and 12,052). That is 41.0% of the lines, up
from 38.6%.
- **The bundle shrank by 70 bytes.** `dist/index.js` went from 327,475
to 327,405 bytes.

## Housekeeping

- The `Checkbox` coverage threshold in `jest.config.js` follows the
renamed file.
- A test comment that named `Text.js` now names `Text.tsx`.
- The three remaining cast comments no longer point at
`InputNumber.tsx`, whose cast block is gone.
- `docs/SUPPORTED-PROPS.md` needed no change, as `docs:props:check`
confirms.
- `docs/UPGRADE-PLAN.md` §9.6-E2 records the batch.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included. The eight files' coverage is
unchanged; `Button` is still at 90% of branches, because no test renders
a loading button;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react17`, `test:react19`, 2908 tests each (one
of them new);
- `test:e2e`, 41 tests;
- the manifest contract.

In the demo on the dev server, the toggle example flips on a click and
logs no new console error.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Oct 1, 2026
…#138)

## What changes

The field components and the helpers the converted files already leaned
on become TypeScript.

- **Components, as `.tsx`:** `Input`, `InputDate` and `Table`.
- **Modules, as `.ts`:**
  - `domProps`, with a typed `omitProps`;
  - `inputs/validationRules`;
  - `inputs/normalizers`;
  - `utils/timers`, which holds `useTimers`.

All seven are renamed with `git mv`. Git detects every rename (53–96%
similarity), so their history follows.

| file | notes |
|---|---|
| `Input.tsx` | `InputProps` |
| `InputDate.tsx` | `InputDateProps`, over rc-picker's own `PickerProps`
|
| `Table.tsx` | `TableProps` and `TablePartProps`; the family's
structure is unchanged |
| `domProps.ts` | `omitProps` returns `Record<string, unknown>`, not the
`Object` its JSDoc said |
| `validationRules.ts` | `ValidationResult` |
| `normalizers.ts` | parameter types from what each body needs |
| `timers.ts` | `Timers`, with `setTimeout`'s extra arguments typed
through to the callback |

**`LocalDraftTableRow`'s last three component casts go, so no `.tsx`
file casts a component any more.** What is left of
`UnconvertedComponent` types the registry slots (`Active.Field`,
`Active.UIRender`) and the engine's import cycle. `Row`'s cast of the
`omitProps` result is gone too.

A throwaway probe of seven wrong uses failed on all seven, and its
correct counterparts compiled:
- an `onRemove` taking a number;
- an `InputDate` `onChange` taking a number;
- a string `striped`;
- `omitProps` on a string;
- `maxLength('5')`;
- `phone(5)`;
- a timer callback whose arguments do not match.

## What the checker found

- **The draft row hands its fields an `unknown` error.**
`fieldErrors[name]` reaches `Input`/`InputDate` as `error`. It is
normally a message. Under `validate: 'maxLength'` it is the function the
factory returns, the bug #133 recorded, and React does not render it. A
commented cast where it is read says so.
- **`InputDate` passes rc-picker a `resize` that rc-picker does not
declare.** A probe showed that rc-picker drops it: no element it renders
carries the attribute, the open panel included. It is typed rather than
removed, so the runtime is unchanged.
- **`password` keeps its last value on itself for `password.confirm`.**
TypeScript recognises a function's own properties only when they are
assigned at the top level, so the assignment inside the function is a
cast.

## No runtime change

Every edit is a type, a cast or a comment. `dist/index.js` went from
327,387 to 327,369 bytes, 18 smaller, because three cast aliases are
gone and one is added.

## The prop-reference generator

It statically parses `domProps` and `Table`.
- It finds them by extension, because `pack()` already looked for `.ts`.
- It matches `export const ENGINE_PROPS = [` literally, so the two lists
stay unannotated.
- `docs/SUPPORTED-PROPS.md` is regenerated. Only the paths and `Table`'s
line count changed.

## Measured

- **The corpus is identical to master.** Over the 38 examples through
the published entry: commits at mount 107, `translate` calls 1013, and
27 / 64 on an edit. The DOM is identical after mount and after an edit.
- **TypeScript is now the larger half.** `src/core` + `src/library` hold
71 TypeScript files, 10,806 lines (was 64 and 9,989), against 86
JavaScript files, 9,396 lines (was 93 and 10,111). That is 53.5% of the
lines, up from 49.7%.

## Housekeeping

- **Every mention of `domProps.js` names `domProps.ts`.** There were 22
in 17 files: component and engine comments, tests, the generator, the
curation, and a comment in the CI workflow.
- **The other renamed files' mentions follow too.** That covers the
three coverage thresholds in `jest.config.js` and nine comments. Two of
the comments are in LESS (`_semantic.less`, `tooltip.less`) that
survives compilation, so `public/static/ui-render.built.css` is rebuilt.
A comment that told history ("`Table.js` used to be a 24-line wrapper")
now names the component without an extension.
- **The plan records the batch.** `docs/UPGRADE-PLAN.md` §9.6-E2 has the
note.

## A miss of my own, which the gates caught

The first run of the gate set failed `test:coverage`: "Coverage data for
`./src/core/components/Table.js` was not found", and the same for
`Input.js` and `InputDate.js`. The three thresholds still named the old
files.

My stale-path check had passed, but vacuously. On this machine `git grep
-E` with `\b` matches nothing: 0 hits where the same search without `\b`
finds 9. Every check in this PR was rerun word-bounded with `git grep
-P`, next to a control search for a known hit.

That rerun also found older stale mentions, which are fixed here as
well:
- **six comments still named `Data.js`.** #133 said it had renamed all
of them, and its search missed these.
- **five named `TooltipPop.js`.**

## Gates

The final run, after the fixes above, exited 0 on every gate:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included. Every touched file's coverage is
unchanged, and `validationRules`' and `LocalDraftTableRow`'s partial
branches are the same expressions as on master;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react17`, `test:react19`, 2908 tests each, with
no worker crash;
- `test:e2e`, 41 tests;
- the manifest contract.

In the demo on the dev server:
- **`input` example:** typing kept the value, and the wrapper took the
`done` class. That is the path the `done` cast covers.
- **`nestedDataKind` example:** it renders four `ui table`s. None of
their 58 cells carries an empty `class`, the cells hold 44 fields, and
the draft row's "Add line" button is there.
- **Console:** no error.

The demo has no date field, since the one date-typed meta is an orphan,
so `InputDate` is covered by its suites.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
AlexSamBY added a commit that referenced this pull request Oct 1, 2026
…drifted (#139)

## What changes

This PR covers the bugs found during the modernization that were
recorded and left open, and the documentation that had drifted. **Every
behaviour change has a test that fails on the previous code.** I checked
each one by running its test against master's version of the file; the
two deletions have nothing to fail.

### Bugs

1. **`validate: 'maxLength'` failed every value.**
- `FIELD.VALIDATION` registered the length *factory* as the validator.
The draft row registered the same factory (#133 recorded it there).
- So the field's validator returned a function, which counts as an
error. A form field declaring it could never be submitted, and a draft
row could never be added.
- Both now register `maxLength()`, the validator at the factory's
default of 100 characters.
- Tests: `UIRender.form-flows.test.js` and
`LocalDraftTableRow.append-validation.test.js`.
2. **The per-form registries lasted one render.**
- react-final-form 6.5.9 hands its render prop a new `{...form, reset}`
on every render. The registries in `state/formRegistry` (touched fields,
errors, baseline) were keyed by that object.
- That defeated the touched registry, which exists to remember a field
that unmounted. Measured: on a pristine form, touch a required field so
that "Required" shows, switch tabs and come back. The error had
vanished.
- The wrapper now keeps **one object per final-form instance**, so the
registries and the subscription outlive a render. All those objects
share the instance's methods.
- **Why the instance is told apart by its methods.** My first version
kept the very first object seen. `examples.strict-mode.test.js` failed
on `nestedDataKind`, because StrictMode's discarded first render builds
an instance of its own. The document was then bound to that discarded
instance, and an edit did not show.
   - The corpus measured identical, commits included.
3. **A popup item bound by `name` did not show its row.** Popup content
renders with `relativeData: false`, so the item's data is never resolved
by name. The mapper's `Text`/`Title` view rendered whatever context it
had:
- **nothing**, in a popup declared inside a nested row document, where
`_data` is unset and the row is in `data`;
- **the whole row object**, which React rejects, when the popup resolved
its row into `_data`.

It now reads the field it names from that row, as `config.md` promises.
Test: `rules.popup-content.test.js`.
4. **Two keys leaked to the DOM.** Both are in `ENGINE_PROPS` now.
   - `meta`: every popup item carried it as `meta="[object Object]"`.
- `@class`: OpenL's serializer puts this tag on every meta node, and
React reported it as an invalid attribute name.

   The per-component DOM-boundary tests pick both up from the list.
5. **`StandaloneTabs` rendered a `{text}` tab without an icon as the
object itself.** Its `propTypes` accept such a tab, and React rejects
rendering it. It now renders the text. The component is demo-only.
6. **The demo's percent slider showed "5,000 %" for 50.**
- The `Percent` renderer takes a fraction and multiplies it by 100,
while the example's marks treat the values as percents.
- The example now uses `unit: '%'` and shows "50%". The renderer is
unchanged, since other views rely on its fraction semantics.
   - The DOM snapshot changed by exactly that tooltip.
7. **The corpus logged five warnings, and now logs none.**
   - Two metas put `buttoned`, a `Tabs` prop, on a `VerticalLayout`.
- `InputNumber`'s `propTypes` rejected the `error={false}` every field
is handed before the user interacts with it.
- The form layer marked `formProps` and `instance` as required. A nested
`Data` document does not pass them, because it shares its parent's form,
which the layer's own `form` getter already supports.
   - `@class` reached a `<span>` (item 4).

**A new contract test, `examples.console-clean.test.js`, renders every
example under StrictMode and expects no console output.** With one stray
`buttoned` restored, it fails.
8. **Dead code removed.**
- `popupScope`'s search for a row in the form values' keys could never
succeed, because final-form nests a bracketed name into a real array.
Doing what it meant, reading a row from field names, would be the guess
§2.6-16 removed, so only its live half is kept: the values.
- `components/utils/layouts.js`, imported by nothing, is deleted, and so
is `utils/SizeMe.js`, imported only by it.

### Documentation

- **`docs/UPGRADE-PLAN.md`**:
- roadmap row 4-S is marked done (H3, H4 and H5 all landed 2026-09-23),
and row 4 records H1 as done;
- **risk R14 is retired after a re-check.** Every global it names is
closed or was measured not to leak, and it is pinned by the two-instance
suites;
  - the §9.3 and E2 findings above are marked fixed.
- **`CLAUDE.md`** no longer says the source is still JavaScript.
- **`TooltipPop.tsx`**'s note that a ref cannot reach those components
now says that this held on React 16–18. On 19 a `ref` is an ordinary
prop and reaches the element; this was measured in #134.
- **`changelog.md`** records the consumer-visible fixes under Unreleased
→ Fixes.
- **`docs/SUPPORTED-PROPS.md`** is regenerated: the boundary lists and
one line count.

## Measured

- **The corpus is identical to master except the slider example.** That
example's DOM changed by its tooltip, and it makes one fewer `translate`
call, because the `Percent` renderer's "%" text is gone: 1013 → 1012 in
total.
- **Commits are unchanged:** 107 at mount and 27 on an edit.

## Gates

All exited 0:
- `lint:js`, `lint:css`, `typecheck`, `typecheck:contract`;
- `docs:props:check`, `docs:views:check`, `css:fixture:check`;
- `test:env-flags`, `build`;
- `test:coverage`, thresholds included;
- `test:pack`, `test:pack:peers`, `test:types`;
- `test:react16`, `test:react17`, `test:react19`, 2914 tests each, with
no worker crash;
- `test:e2e`, 41 tests;
- the manifest contract.

In the demo on the dev server:
- **Slider example:** its five tooltips read 35%, 120$, 760$, 500 and
50%.
- **Console:** no error in the three examples that used to log one,
`dropdownExperience`, `ratingDetails` and `tableForm`.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant