The CEDAR Embeddable Editor (CEE) puts a metadata entry form inside a web application without anyone hand-writing that form. The host page supplies a template, and the CEE renders the fields the template calls for, checks what the user enters against the template's constraints, and returns the finished record as structured metadata in JSON-LD or YAML.
A template describes the metadata to collect, not the interface that collects it. It names the fields, their types and cardinalities, which of them repeat, and which draw their values from a controlled vocabulary or from an identifier authority such as ORCID or ROR. A platform can therefore adopt or revise a metadata standard by editing a template rather than by rewriting a form. The record that comes back preserves those bindings, since a controlled term carries its IRI beside its label and an authority field carries its persistent identifier.
Templates follow the model defined by CEDAR, the metadata infrastructure maintained by the Stanford Division of Computational Medicine. Rendering a form needs neither a CEDAR account nor a running CEDAR installation. The CEE ships as a single JavaScript file defining a standard Web Component, so it embeds in a plain HTML page as readily as in an Angular, React, or Ember application.
For consistent compact controls across CEE, CEF and CED, see the shared styling guide.
The CEDAR Embeddable Editor documentation covers embedding the component in a page or a framework, configuring it, controlled terms and external identifiers, validation, appearance, and security.
Your First Embedded Editor assembles a working page from the bundle, one element, and a template. Templates and Metadata gives the input properties, the output properties, and the change event a host reads.
For the design rationale, the architecture, and deployments in research platforms, see Author Once, Publish Everywhere: Portable Metadata Authoring with the CEDAR Embeddable Editor, published in the Data Science Journal (2026).
Releases are published to npmjs.org as
cedar-embeddable-editor
under the latest tag, the public stable channel, so an embedder installs the
current release by name:
npm install cedar-embeddable-editorTo see which release that is, without depending on a version copied into a document that can go stale:
npm view cedar-embeddable-editor versionThe package holds cedar-embeddable-editor.js, the self-contained bundle, and
cedar-embeddable-editor.d.ts, the declarations for the element and its public
API. Copy the bundle to the application's static assets and load it with a
regular <script> tag. The bundle loads as a classic script, not as an ES
module.
The default cedar-embeddable-editor.js remains self-contained. Hosts that already
register CEDAR Roboto 400 and 500 can instead load
cedar-embeddable-editor.host-fonts.js. Both bundles register CEE and CEF with the
same public API; load only one. The host-font variant retains Material Icons but
omits embedded text fonts. Without the host's font faces, text falls back to the
shared system stack.
Import the token package's fonts/regular and fonts/medium Sass exports in the
host's global stylesheet before loading the host-font bundle. These exports are
available in the token source; older snapshots can import the corresponding
fonts/_roboto-400.scss and fonts/_roboto-500.scss partials through a configured
Sass include path. Production builds generate and packaging verifies both bundles,
with a separate SHA-256 manifest for each. Workspace detects whether its installed
CEE package provides the host-font variant; older CEE packages retain standalone
loading until their pin is advanced.
A host page needs the bundle, one <cedar-embeddable-editor> element, and a
template:
<cedar-embeddable-editor></cedar-embeddable-editor>
<script src="/assets/cedar-embeddable-editor.js"></script>
<script type="module">
const template = await (await fetch('/assets/dataset-template.json')).json();
await customElements.whenDefined('cedar-embeddable-editor');
const cee = document.querySelector('cedar-embeddable-editor');
cee.config = {
terminologyBaseUrl: 'https://terminology.metadatacenter.org/',
bridgeBaseUrl: 'https://bridge.metadatacenter.org/',
};
cee.templateObject = template;
</script>Templates, instances, and configuration are JavaScript objects, so a host assigns
them as properties rather than as attributes. Waiting for
customElements.whenDefined() guarantees the element exists. Set config before
the form is built, and assign templateObject last, which renders it. The two
service URLs are needed only for controlled-term and external-authority lookups.
Read the record back from currentMetadata as CEDAR JSON-LD, or from
currentMetadataYaml as YAML. The CEE neither submits nor stores it. The host
decides when and where a record is saved.
Your First Embedded Editor takes the same page apart step by step, and Embedding in a Framework covers Angular, React, and Ember.
A dialog host can set previewMode: true in either read-only or editable mode
to omit CEE's identity header and use compact, content-sized form spacing. Template
descriptions and page navigation remain available. readOnlyMode independently
controls whether values can be edited. Ordinary embeds retain their existing layout.
Trial hosts such as Workspace “Try out” and CED’s editable preview set
suppressEmptyFieldErrors: true. Clearing a control then hides its error state and
messages even after blur. Nonempty invalid values and incomplete temporal values still
show errors. This affects presentation only: required fields remain invalid in the quality
report. Ordinary metadata editing leaves the option off.
Read dataQualityReport for the whole instance, including nested and off-screen
occurrences. Each problem has a code, severity (warning or error), a field
path and occurrence indices. Pass a field problem to reveal(problem) to reach it.
Missing required answers, insufficient occurrences and unnamed attribute rows are
warnings. Invalid values, malformed incoming data and unfinished edits are errors.
Either makes isValid false; the host decides whether saving is allowed. A required
field must be answered in every existing containing element. A repeating field
needs at least one answer within each such element.
The report includes unfinished date/time and attribute-name edits, and change
fires when metadata or the report changes. Invalid imported field IRIs and
well-shaped numeric, temporal and IRI defaults remain available for correction.
Terminology membership and server-side validation remain the host's responsibility.
The bundle registers a second element. <cedar-embeddable-field> renders one field
from its artifact. In editable mode it supplies the same bare control CEE uses,
so a host such as CED can place it in its own form.
With config = { readOnlyMode: true }, CEF owns the full presentation: label,
field type, description, constraints, choices, sources and declared defaults where
present. The type is named by the icon beside the label. A supplied value is shown
read-only. CEE uses the same field presentation, so preview hosts only need to supply
the artifact and their dialog shell.
Set previewMode: true with readOnlyMode: true when the host supplies the field name
in its own header. CEF then omits its header, and with it the type icon, while
retaining the description and value or specification. A host whose header leaves out
the type, such as the Workspace's preview, also sets showFieldType: true.
CEF then states the type above the description, with the icon its header would have
drawn (for example, Paragraph). A host whose header already shows the type, as CED's
field cards do, leaves it unset.
Read-only controls have no placeholder text, and fields without constraints draw empty boxes. Static fields show their content. A standalone page break is described without creating pagination.
Designing a template is what this is for. An author giving a field a default value needs somewhere to type it, and the box that collects one has to be the control the field will actually have: a date picker for a date, a term lookup for a controlled term, a bounded number box for a number.
<cedar-embeddable-field></cedar-embeddable-field>
<script src="/assets/cedar-embeddable-editor.js"></script>
<script type="module">
const artifact = await (await fetch('/assets/organism-field.json')).json();
await customElements.whenDefined('cedar-embeddable-field');
const field = document.querySelector('cedar-embeddable-field');
field.config = { terminologyBaseUrl: 'https://terminology.metadatacenter.org/' };
field.addEventListener('valueChange', (event) => console.log(event.detail.value));
field.fieldObject = artifact;
</script>A field artifact, not a field model. A host holding a TemplateField from the CEDAR
Model TypeScript Library — a designer that just built one, say — writes it out and
assigns the result rather than assigning the object.
Assigning the object costs nothing and looks as though it should work, which is why
this is worth stating. The element's copy of the model library sits inside the CEE
bundle and the host's sits inside its own, so the two hold different classes, and CEE
decides what a field is by identity: field.cedarFieldType === CedarFieldType.TEXT,
and instanceof in three dozen other places. Every one of those comparisons is false
for an instance built elsewhere, so the field renders as a default rather than
failing. A serialization also survives the two packages pinning different versions of
the model library, which shared objects would not. The editor's templateObject
takes an artifact for the same reason.
The value comes back as a discriminated union rather than as text, because the distinctions are real ones a host has to make again the moment it writes the value into an artifact: a number is a number, a term is an IRI with a label, and a checkbox group holds a set.
import type { CedarEmbeddableFieldChangeDetail, CedarEmbeddableFieldValue } from 'cedar-embeddable-editor';
declare function recordDefault(value: CedarEmbeddableFieldValue): void;
const field = document.querySelector('cedar-embeddable-field');
field?.addEventListener('valueChange', (event: CustomEvent<CedarEmbeddableFieldChangeDetail>) => {
const { value, valid } = event.detail;
if (valid) {
recordDefault(value);
}
});fieldObject may be reassigned as often as a host likes, and each assignment builds
the control afresh — the field being designed changes type under its author's hand.
config takes one assignment, as the editor's does. A value of a kind the field
cannot hold is reported through eventHandler and ignored rather than coerced.
Malformed runtime payloads are also rejected, including assignments made before the
field arrives. Accepted values and artifacts are copied; host mutation after an
assignment does not change the editor. Getters and events return detached values.
A numeric value has the shape { kind: 'number', value: number | string }.
Ordinary numbers remain numbers. When converting to a JavaScript number would lose
significant digits or exceed its range, CEF returns the exact numeric string instead:
9007199254740993 and 0.1234567890123456789 retain every digit. Both forms may be
assigned back through value. Constraint validity remains a separate result.
Read-only mode guards user mutations in the controller, including late callbacks
and structural edits. Explicit host value assignments still work. Controlled-term
and external-authority searches cancel on a new query, a read-only transition, or
widget destruction; an old response cannot overwrite a newer query.
Requiredness and cardinality belong to a field's deployment inside a template, and this element deploys nothing, so the value it acquires is single and is allowed to be absent. That is what makes it usable for a default, which is optional by definition. A field declaring its own default starts out holding it.
readOnlyMode is the presentation half of the same element: with a value it shows the
value, and with none it replaces the control with a statement of what the field will
accept, which is what the editor shows for a template nobody has filled in.
One command produces the single file an embedder loads. Do not concatenate named Angular output files manually: their names, locations, and module structure change when Angular changes builders.
Build the production application, then run the browser suite against the single-file bundle it produced:
nvm use
npm run build:production
npm run test:visual:prebuiltUse Node 24.19.0, which .nvmrc, package.json and CI all specify. The build and
tests use that same version, so the distribution is produced by the toolchain
that exercises it.
Once that exact bundle is green, stage the publishable npm directory from it:
npm run package:npm:prebuiltFor a release candidate, npm run test:package performs both operations in one command: it builds
and browser-tests the production bundle, then stages and verifies the package from those exact
tested bytes.
This copies the tested bytes to
dist-npm/cedar-embeddable-editor/cedar-embeddable-editor.js, refreshes its
version, README, changelog, and package lock, and records the bundle manifest.
The command fails if the browser bundle is stale or does not match its SHA-256
digest. npm run check:npm-package can repeat the byte-for-byte verification
before npm pack or npm publish.
The complete test gate is available from the repository root:
npm run test:ciIt runs, in order:
ng lintover the sources and the ESLint configuration.- A type check of the application and the domain harness, with
stricton throughout. - The unit tests, in Node under Vitest.
- The headless domain harness with V8 coverage, and its per-directory coverage floors.
- A production build, then the Playwright suite against that bundle, in a
container: the full Chromium baseline at desktop and narrow viewport sizes,
plus focused Chromium, Firefox and WebKit compatibility checks. The container
is what makes a screenshot baseline mean the same thing on a laptop and on CI,
so the pixel budget is zero — see
visual/run-in-container.sh. - Staging the npm package from the bundle the suite just exercised, which checks the raw and gzip size budgets and verifies every staged byte against its source.
The domain corpora are checked into harness/fixtures/; running the tests does
not require cedar-artifact-library or cedar-test-artifacts checkouts.
.github/workflows/test.yml runs the same gate on every pull request and on
pushes to main and develop. Nothing is published from CI: releasing is a
separate, manual procedure.
npm run audit:prodOnly runtime dependencies reach the file an embedder downloads, so this audit is
the one that describes the shipped artifact, and it is deliberately not part of
test:ci — it can fail on a disclosure rather than on a commit, which would
break an unrelated pull request its author cannot fix.
A root npm audit also reports on development tooling that is not shipped to an
embedder. Never run npm audit fix --force here: it can replace the declared
toolchain with incompatible major versions. Review and update affected
dependencies explicitly instead.
The CEE resolves cedar-model-typescript-library from npmjs.org, so a sibling
checkout is not needed:
nvm use
npm ci
npm --prefix harness ciThe visual suite installs nothing here. It runs inside Playwright's own container,
which carries the browsers it drives, and installs its dependencies there against a
named volume — so it needs Docker running and no playwright install of its own.
The CEE uses Angular 22.1 and Node 24.19.0. .nvmrc, the package engines field and
CI specify the Node version.
The application bundle and the visual fixture generator each install the model library directly from npmjs.org:
"cedar-model-typescript-library": "<version>"Keep the version in the root and visual/ manifests, and both lockfiles, in
sync. The production bundle imports the root copy while the browser fixtures are
generated with the visual copy, so a mismatch means the tests and the artifact
are using different model contracts. The harness declares no separate copy; it
resolves the root installation.
Use these when working on one layer:
npm run test:unit:ci # Vitest unit tests, one run
npm run test:unit:coverage # unit tests with coverage report
npm run test:domain # Vitest domain harness
npm run test:domain:coverage # domain harness with coverage report
npm run test:bundle-size # exact raw and gzip budgets for the shipped bundle
npm run test:visual # production build, fixture preparation, Playwrightnpm test runs the unit tests once, and npm run test:watch keeps them running
for interactive development. Use npm run test:ci for a complete verification.
The unit tests run in Node and do not use TestBed or Angular's JIT compiler.
Browser behavior belongs in the Playwright suite under visual/, which tests the
shipped bundle rather than the sources.
The CEE also runs on its own, outside any host page, which shows a change to the sources immediately in a browser.
Clone this repository onto a local directory of your choice:
git clone https://github.com/metadatacenter/cedar-embeddable-editor.gitOpen the standalone application's configuration file, src/app/app.component.dev.ts.
This minimal configuration enables lookups through the public CEDAR services:
import type { CeeConfig } from 'cedar-embeddable-editor';
const ceeConfig: CeeConfig = {
terminologyBaseUrl: 'https://terminology.metadatacenter.org/',
bridgeBaseUrl: 'https://bridge.metadatacenter.org/',
};For a different CEDAR deployment, replace both URLs with its service URLs. See the configuration documentation for all available settings.
-
Navigate to the CEE directory:
cd <...>/<clone directory>/cedar-embeddable-editor/
-
Run these commands:
npm install ng serve
-
In your browser, navigate to
http://localhost:4400/. The app will automatically reload if you change any of the source files.
The host lifecycle matrix in
src/app/modules/shared/components/wrapper-lifecycle-matrix.spec.ts crosses three input
arrival orders, host mutation, one or two simultaneous numeric/lookup wrapper pairs,
ordinary/large-integer/precise-decimal values, initial/late/no read-only state,
and superseded lookup success/error/completion (324 cases). It runs in npm test
with real wrappers, artifact coordination, controllers and lookup streams; rendering
and HTTP are substituted. The Angular coordinator suite separately verifies rendered
controls, simultaneous wrappers, host events and malformed assignments.