Check the docs folder to get a basic understanding of the project's architecture
https://eisgroup.github.io/ui-render/
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.
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.
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.4Install (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.4React 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.
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 ownCopy 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 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.
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.
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-renderimplements. That is the right choice when meta and library ship together, and it is why every existingmeta.jsonkeeps 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
metaVersionplaced 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.
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-YYYYwhatever was passed. If your application has been passingdateFormatand compensating for it elsewhere, it now takes effect.
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.
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.
- 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 versionpackage.jsonpins inpackageManager. 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. - Navigate to project root folder and install dependencies by running this command in terminal:
In the project directory, you can run:
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.
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.
- Install
yalcgloballynpm install -g yalc - In your application add a link to the library with
yalc add eis-ui-render --linkand reinstall dependencies - Run
npm run yalc-watchto build library and life reload
- Bump the package version with
npm version patch(orminor,major, or an explicit version). This also synchronizes every trackeddata-versionattribute. - Inspect the package contents with
npm pack --dry-run. Theprepacklifecycle verifies version synchronization, checks that the generateddocs/SUPPORTED-VIEWS.mdanddocs/SUPPORTED-PROPS.mdare current, and builds the library automatically. - The build writes what the bundle carries from other packages into
dist/: the licence file of each one inTHIRD-PARTY-LICENSES.txt, and a CycloneDX SBOM insbom.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:peersrepeats that server render against React 16.14, 17 and 19, and CI runs it too. - Login to npm with
npm loginif needed. - Publish the verified version with
npm publish. The sameprepackchecks 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.
- Run
npm run buildto prepare artifacts - Run
npm run deployto upload artifacts to GitHub