Skip to content
eisgroupPublic

About

Recursive UI Rendering with Dynamic React Components

Resources

Stars

4 stars

Watchers

2 watching

Forks

Latest commit

 

History

1,315 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Check the docs folder to get a basic understanding of the project's architecture

Demo

https://eisgroup.github.io/ui-render/

Supported view names

docs/SUPPORTED-VIEWS.md lists every name a meta.json may use — the view of a node, the value of a render* attribute, and the action names accepted by onClick, onChange and onDone — and puts any view that is declared as a constant but that no resolver case handles in a table of its own. The page is generated from the FIELD constants plus the resolver source, so run npm run docs:views after adding or removing either; a contract test fails while the page and the source disagree.

Supported props on Table, Tooltip and Select / Dropdown

docs/SUPPORTED-PROPS.md documents the prop surface of the three views the semantic-ui-react exit replaced, split by what actually happens to each prop: consumed by our component, stripped before the DOM, or deliberately dropped. All three — Table, Tooltip and Select/Dropdown — are in-house now, and no file in src references the package by any mechanism, so the page reads as a record of what each component emits and what it no longer accepts rather than as a parity checklist. It is generated from the component source and the call sites — run npm run docs:props after changing one — and a contract test additionally checks it against the example corpus.

Installation (consumer)

eis-ui-render declares the following peer dependencies. The host application must install them explicitly — they are not bundled. React must remain a single shared instance, while Moment must be supplied by the host because the library build externalizes it. The bundle also imports react/jsx-runtime, which every React in the range ships, from the host's React: a build that aliases react to one copy must alias react/jsx-runtime the same way.

Package Required version Why it must be a peer
react ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 A second copy of React in the tree triggers Invalid hook call and breaks Context (forms, providers). pnpm with strict node_modules will not deduplicate copies across non-overlapping ranges.
react-dom ^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 Must use the same major version as react so the renderer pair matches.
moment ^2.29.4 The library externalizes Moment and uses it for date pickers and formatters, so the host must provide a compatible 2.x version.

Install (npm):

npm install eis-ui-render react@^18.0.0 react-dom@^18.0.0 moment@^2.29.4

Install (pnpm) — note that with auto-install-peers=false (the strict default in some setups) peer dependencies are not installed automatically, so they must be listed explicitly:

pnpm add eis-ui-render react@^18.0.0 react-dom@^18.0.0 moment@^2.29.4

React 16.14, 17 and 19 hosts are supported as well, and may keep matching react@^16.14.0, react@^17.0.0 or react@^19.0.0 dependencies. The library is developed against React 18.3, and the whole test suite runs on 16.14, 17, 18 and 19 in CI.

If the host project relies on transitive copies of react/react-dom/moment from another package instead of declaring them directly, pnpm in isolated mode will not resolve our peer through them — the application must declare these three packages itself.

Other libraries previously listed as peer dependencies (final-form, final-form-arrays, react-final-form, react-final-form-arrays) are bundled into dist/index.js, and the package declares no dependencies of its own, so the host project installs nothing beside the three peers. prop-types, also listed once, is not used at all any more: the props are TypeScript types.

eis-ui-render is consumed by a bundler — imported as a React component into a host application. Dropping dist/index.js into a page with a <script> tag is not supported: the UMD global lookup never matched React's real global name, so it has never worked.

The bundle is built for current browsers: the last two versions of Chrome, Edge and Firefox, and the last two major versions of Safari, on macOS and on iOS (browserslist in package.json). That is what the host applications support, so an older browser is not a target.

Styles and assets (consumer)

The library entry deliberately does not inject CSS, so the host loads the stylesheet itself. Both paths below work and resolve to the same rules — dist/static/all.css and font.css are one-line @import re-exports, so the bytes ship only once (semantic.css is an empty stub in both folders):

import 'eis-ui-render/static/all.css'   // or 'eis-ui-render/dist/static/all.css'
import 'eis-ui-render/static/font.css'  // icon font — only if the host does not provide its own

Copy the package's static/ folder into the web root as part of the build. It holds the stylesheets and the icon font, and is self-contained:

cp -R node_modules/eis-ui-render/static ./public/

An Image given only a name loads it from /static/images/<name>, relative to the host's web root, with the name lower-cased and each whitespace character replaced by -. The package ships no images, so those files are the host's own. Earlier releases also carried static/images/flags/, 266 country-flag SVGs that nothing in the library used; a host that links them now ships its own copy.

A host that serves static/ under a sub-path or a CDN sets path (the images folder, ending in /, which the name is appended to) or src on the Image in its meta; the library build fixes its environment at build time, so no host environment variable reaches it. Releases 0.32.4 to 0.34.3 resolved a name-only Image to a page-relative undefined/static/images/<name> instead, which 404s.

The meta.json contract (consumer)

The package ships the UI declaration contract as a JSON Schema (draft 2020-12) at eis-ui-render/meta.schema.json, so meta.json authors get autocomplete and validation in their editor instead of discovering a typo at render time.

The quickest way in is a pointer inside the file itself — no workspace configuration, and it works in VS Code and the JetBrains IDEs alike:

{
  "$schema": "./node_modules/eis-ui-render/meta.schema.json",
  "view": "Col",
  "items": [{ "view": "Text", "name": "customer.name" }]
}

Or map it once for every meta file, in .vscode/settings.json:

{
  "json.schemas": [
    {
      "fileMatch": ["**/*_meta.json", "**/meta.json"],
      "url": "./node_modules/eis-ui-render/meta.schema.json"
    }
  ]
}

The schema is permissive on purpose. Component attributes are forwarded to the underlying React component, so nodes accept properties the schema does not list, and view, render-method, action and normalizer names suggest the built-in vocabulary without rejecting an unlisted string — the renderer accepts those too. What the schema does constrain is the handful of shapes the engine genuinely requires (items/headers/extraItems/extraHeaders must be arrays, name must be a string), each of which is otherwise a render-time crash, plus the type of a few attributes the engine reads, such as relativeData and showIf, and the format of metaVersion.

$schema is stripped before rendering, so adding it changes no output. So is a _comment attribute on any node, a note for the next author; the schema does not declare it, so an editor will not suggest it.

Dev-mode validation

The same rules run at runtime behind an opt-in prop. It is off by default and walks nothing until asked, so it costs a default host nothing:

<UIRender data={data} meta={meta} validateMeta={process.env.NODE_ENV !== 'production'} />

Each problem is reported to console.warn on one line, naming the JSON path of the offending node rather than leaving a stack trace inside a minified bundle:

[ui-render] meta error at "items[3].items[0].name": name must be a string key path, got number …
[ui-render] meta warning at "headers[2].renderCell": unknown render method "double5" …

error means the engine will fail on that node; warning means it will render, but silently degraded — an unknown view becomes a "field does not exist" placeholder, an unknown render* method falls back to plain text. Pass a function instead of true to collect the problems yourself (validateMeta={problems => …}), and keep it a stable reference: the check runs again whenever meta or this prop changes identity. The reporter never throws into the host application, whatever it finds.

Contract version

meta.json may declare an optional root-level metaVersion ("MAJOR" or "MAJOR.MINOR") to record which contract it was authored against. The current contract version is 1.

  • Absence means "current" — the file targets whatever contract the installed eis-ui-render implements. That is the right choice when meta and library ship together, and it is why every existing meta.json keeps working untouched.
  • MAJOR changes only for a change that would break existing meta; MINOR for additive ones. Declaring a version equal to or below what the library implements is always compatible.
  • The engine ignores the value and strips the field before rendering: declaring it never changes output. Dev-mode validation is the only thing that reads it, and only to report a malformed value, a major version newer than the installed library implements, or a metaVersion placed on a nested node, where it means nothing.

There is no negotiation beyond that, deliberately: the field exists so a future contract change can be additive and announced, not so hosts can request a different renderer.

Note the unrelated legacy version attribute seen in older meta files: it is not a contract version (existing files use it both as a producer version at the root and as a node label deeper in the tree), the engine discards it, and new files should use metaVersion.

Renderer configuration (consumer)

Three props configure how values are formatted and how the shell is labelled. They are published to every component the renderer draws, so a nested Table cell honours them exactly like a top-level field:

<UIRender data={data} meta={meta} dateFormat="DD/MM/YYYY" currency="EUR" language="fr" />
Prop Default Effect
dateFormat MM-DD-YYYY moment format tokens for every date the renderer displays (an ISO value in a Text node, a render*: "Date" value) and edits (the date picker's display and parsing)
currency USD published as a CSS class on the renderer's shell (.app.EUR), for currency-specific styling
language en published as a CSS class on the renderer's shell (.app.lang--fr)

Each is merged, not replaced: passing only dateFormat leaves currency and language at their defaults. currency is not meta.currencyCode — that one selects the currency symbol the Currency value renderer prints, in either form ("Currency" or {"name": "Currency"}), and is declared in meta rather than passed as a prop.

These props used to be accepted and then silently ignored — every date rendered as MM-DD-YYYY whatever was passed. If your application has been passing dateFormat and compensating for it elsewhere, it now takes effect.

Error reporting (consumer)

The renderer catches a failure per node rather than letting one bad declaration blank the page: the failing node is replaced by a one-line diagnostic and everything around it keeps rendering. Pass onError to receive the same diagnostic as a structured report:

<UIRender data={data} meta={meta} onError={report => Sentry.captureException(report.error, {
    extra: { metaPath: report.path, componentStack: report.errorInfo.componentStack },
})} />
{
  error,      // the thrown value
  errorInfo,  // React's {componentStack}
  path,       // JSON path of the node in `meta`, e.g. 'items[3].items[0]' ('' = the root)
  props,      // that node's resolved props: its meta declaration plus what the engine added
  message,    // the one-line diagnostic, also rendered in place of the failed node
}

path is the point of the report — a stack trace out of a minified bundle names React internals, while items[3].items[0] names the declaration to go and fix. It is exact for a failure inside the component a node resolved to; for a failure the renderer hits while preparing a node (a malformed items, say) it names the closest enclosing node, which is the most precise position available.

The library logs the report itself as well, so onError adds a channel rather than silencing the console. It never has to be defensive: a reporter that throws is caught, and the render failure is still reported.

Development Installation

The published package declares engines.node >= 22: that is the floor for consuming it. Node 22 is the oldest line still supported upstream, and CI server-renders the packed package on it. On an older Node, npm warns at install, and refuses to install where engine-strict is set. Building this repository is a different matter and uses the version in .nvmrc, which is what CI installs: Babel 8, which compiles it, needs Node 24.11 or later.

  1. Install Node.js, if you haven't already — use the version in .nvmrc (v24). Node 24.14.0 comes with npm 11.9.0, the version package.json pins in packageManager. npm itself ignores that field. Corepack reads it once it is enabled for npm (corepack enable npm), and then runs exactly that version. CI does not enable corepack: it runs the npm that comes with its Node 24.
  2. Navigate to project root folder and install dependencies by running this command in terminal:

npm install

Available Scripts

In the project directory, you can run:

npm run start

Runs the demo in development mode on http://localhost:3001.

Edits apply in place through React Fast Refresh, without a reload. Compile errors show in the terminal and over the page; lint does not run here, so run npm run lint:js for it.

npm run test:e2e

Runs the browser leg of the contract suite (Playwright, Chromium) over a production build of the demo. It records what the tooltip actually does in a real browser — where the bubble lands relative to its trigger, whether it flips at a viewport edge, whether it is clipped, whether our CSS paints it — which is the class of fact the jsdom suites cannot express at all. It also drives the keyboard through the tooltip, the dropdown, the tabs, an Expand title and the upload drop zone, and the views a user acts on: the date input's calendar, the popup, the slider, a toggle, a checkbox, the dropdown by pointer, table sorting and pages, and progress steps. e2e/view-coverage.js names, for each view, the tests that drive it or why none needs to, and a contract test keeps that map complete.

Run npm run test:e2e:install once per machine first: it downloads Chromium into the Playwright cache (~570 MB on disk). That download is deliberately not wired into npm install, so a plain npm ci costs nothing extra for anyone who never runs this leg.

Specs live in e2e/; every expected value is in e2e/reference.js, tagged as a reference (current behaviour, expected to change), an invariant (must not change), or a pinned defect. Read that file before changing an assertion.

Live build mode

  • Install yalc globally npm install -g yalc
  • In your application add a link to the library with yalc add eis-ui-render --link and reinstall dependencies
  • Run npm run yalc-watch to build library and life reload

How to publish the library

  • Bump the package version with npm version patch (or minor, major, or an explicit version). This also synchronizes every tracked data-version attribute.
  • Inspect the package contents with npm pack --dry-run. The prepack lifecycle verifies version synchronization, checks that the generated docs/SUPPORTED-VIEWS.md and docs/SUPPORTED-PROPS.md are current, and builds the library automatically.
  • The build writes what the bundle carries from other packages into dist/: the licence file of each one in THIRD-PARTY-LICENSES.txt, and a CycloneDX SBOM in sbom.cdx.json. A bundled package with no licence file fails the build.
  • Verify the artifact with npm run test:pack. It enforces the packaging budgets and then packs, extracts and server-renders the tarball in a throwaway consumer that has only the three peer dependencies available. CI runs both gates on every pull request. After a build, npm run test:pack:peers repeats that server render against React 16.14, 17 and 19, and CI runs it too.
  • Login to npm with npm login if needed.
  • Publish the verified version with npm publish. The same prepack checks and build run again immediately before npm creates the published package.

Do not edit the version in package.json manually: use npm version so source metadata, the release commit, and the Git tag stay in sync.

How to publish on GitHub Pages

  • Run npm run build to prepare artifacts
  • Run npm run deploy to upload artifacts to GitHub

About

Recursive UI Rendering with Dynamic React Components

Resources

Stars

4 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages