Skip to content

Latest commit

 

History

History
792 lines (634 loc) · 42.8 KB

File metadata and controls

792 lines (634 loc) · 42.8 KB

Changelog

All notable changes to @dynamic-entity/core, ngx-dynamic-entity and ngx-dynamic-entity-builder. The three packages share a version and are released together.

This project follows Semantic Versioning.


[1.10.0] — 2026-09-03

Two seams that were configurable in principle and partly ignored in practice: date formatting reached some surfaces and not others, and a beforeSave veto could be walked around and could not be observed. Found while wiring the demo to exercise every documented extension point, which is what made both visible.

Added

  • saveRejected on ngx-dynamic-record-form. The record editor embeds ngx-dynamic-form, so a whole-record save always did run beforeSave and always did refuse when the hook said no — but nothing carried that refusal out to a host binding the record editor. The veto worked and was indistinguishable from a Save button that did nothing, which is the exact failure saveRejected was added to prevent in 1.8.x, one layer up.

Fixed

  • saveSection no longer bypasses beforeSave. The record editor saves one tab at a time, and that button emitted sectionSave without consulting the hook. The payload is extractRecord() — the whole record, the same object the Save button sends — so identical data reached persistence by two routes, only one of which could be vetoed. A hook registered to refuse a save is a data-integrity mechanism; a second way around it made it advisory. A refused section save now emits saveRejected and leaves the section open, so the refused values are still in front of the user.

    sectionSave still fires synchronously when no hook is registered. Awaiting an already-resolved promise would have deferred the emit by a microtask for every consumer, to no purpose; only the hook path defers.

  • setDateFormatters now reaches date and datetime fields. Both formatted with their own toLocaleDateString() / toLocaleString() calls rather than through core's formatDisplayValue, so a host that configured formatters got them in the record summary and on time fields, and silently did not get them on the two field types most likely to be the reason it configured formatters at all. Both now go through the shared formatter, and are handed the form's language.

    No visual change without configuration: the default formatters are the same toLocale* calls these fields made directly. An unparseable stored value is still shown verbatim and never reaches a formatter — records outlive schemas.

    monthYear is deliberately unchanged. It renders a month name, not a formatted date, and routing it through formatters.date would print a day component the field does not have.

Notes

No API removed or renamed. DynamicRecordFormComponent.saveSection() now returns Promise<void> rather than void; callers that ignored the return value are unaffected.


[1.9.1] — 2026-09-02

The npm README pages now document MASKED_PLACEHOLDER and setDateFormatters. Both shipped in 1.9.0, but only EXTENDING.md mentioned them, and npm does not show that file. No runtime change.

Docs

  • Root, core and renderer READMEs cover the masked placeholder, date formatters, and that XXXXXXXXX is the default rather than the only text. The core Quick Start comment on catalog length is 21, matching the list above it.
  • The builder README points those two knobs at the renderer and core, so a reader of that page is not left looking for a builder token that does not exist.

[1.9.0] — 2026-09-02

Every word the libraries render themselves is now translatable, and every rendered control has a name and an id of its own. Work since 1.8.1.

Added

  • The libraries' own text is translatable. Field labels, placeholders and options were LocalizedText and already followed language; the chrome around them — Save, Reset, "No rows yet.", every tooltip and panel heading in the builder — was English literals in the templates. An application in German rendered German labels around English buttons, and there was no way to change that short of forking a component.

    The libraries do no translating. They publish the keys they render and resolve whatever the host hands back, per key: 49 keys in the renderer (UI_TEXT, provideNgxDynamicEntity({ uiText })) and 149 in the builder (BUILDER_TEXT, plus a uiLanguage input on EntityBuilderComponent). A value may be LocalizedText — the same shape a field label uses, resolved against the same language — a flat string, or a resolver (key, defaultText, language) => string for a host that already has ngx-translate, Transloco or $localize.

    DEFAULT_UI_TEXT and DEFAULT_BUILDER_TEXT are exported with their English source strings, so a translation file can be generated rather than transcribed. Anything left out keeps its English default, so an unconfigured install renders exactly what it rendered before.

    The vocabularies are deliberately separate: an app that ships only the renderer should not see the builder's keys in completion. What they share is resolveUiText.

    uiLanguage is not languages. languages is the vocabulary a label is authored in; uiLanguage is the language of the builder's own interface. Tying the chrome to the authoring language would flip the whole panel every time an author switched the label language they were editing.

  • The masked placeholder is configurable. MASKED_PLACEHOLDER replaces the XXXXXXXXX literal that was repeated across twenty-one templates. Bullets read as a redaction, a word reads as a permission, and English reads as neither if the app is not in English. Defaults to XXXXXXXXX.

  • Date display is configurable. setDateFormatters({ date, datetime, time }) in @dynamic-entity/core. The default remains the browser's locale rather than the form's language: language selects which LocalizedText key to read, which is a different question from how to punctuate a date, and tying them would silently change the format on upgrade for every consumer whose browser is set to something else. A partial object overrides one kind; setDateFormatters() restores the defaults.

Fixed

  • Five field types rendered a control with no accessible name. multiSelect, currency, email, password and monthYear drew a <label> beside their control without pointing at it, so a screen reader announced an unlabelled combo box and clicking the label focused nothing. The accessibility scan never saw it: it ran over a demo config made of text and dropdowns, and none of the five are in it. monthYear additionally names each of its two selects, because one label in front of a pair does not say which half the reader has landed on.

    A per-component sweep now asserts that every rendered control has a name, so a field type added later is covered the day it is registered rather than the day someone points an axe run at a page that happens to contain it.

  • A field rendered twice put duplicate ids on the page. A control's DOM id came straight from field.id, which is unique in a config and not in a document: an array renders the same child fields once per row, so a two-row Contacts array produced two #name inputs. <label for> resolves to the first match in document order, so the second row's label focused the first row's input and announced the same association twice — and duplicate ids on focusable elements are a WCAG failure in their own right.

    Ids now carry a per-instance token: email-de7. They were never a public contract — address a control through its data-testid or its label — and the e2e suite, which had been reaching for #fullName, now does what its own helper file says it should. radio also gained a data-testid per option, which it had no stable hook for at all.

  • A common module registered by selector took its tab down. CommonModuleEntry typed component as a string, the token's own example showed one, and COMMON_MODULES — the catalogue the builder's picker offers — is made of them. The renderer passes that value to ngComponentOutlet, which mounts a component type and throws an assertion on a string. Following the documented shape broke the feature, which is worse than the feature not existing.

    component is now string | ComponentClass, a selector resolves to nothing renderable and warns once naming the fix, and the examples show a class.

  • resolveUiText could return something that was not text. map[key] walks the prototype chain, so a key of toString answered with a function and __proto__ with an object — both of which reached the template. A resolver that threw took the whole form down rather than one label, and a {placeholder} matching an inherited name substituted a function body into a sentence. Keys are typed, but these values arrive from a translation catalogue, a JSON file, or a host written in JavaScript, and none of that is checked at the boundary.

  • Configured validation messages now reach every field type. They reached three of fifteen. The other twelve rendered a fixed "This field has an error", so a consumer who configured validationMessages saw it on text, number and dropdown and the generic string everywhere else — a documented feature working on a fifth of its surface. Four field types had no error UI at all. A sweep spec walks the real field registry, so a type added later is covered the day it is registered.

  • core reports a collection that is present but is not an array. tabs: {} or fields: 'x' was silently ignored rather than reported, which read as an empty entity instead of a malformed one.

Internal

1,550 unit tests and 148 E2E, up from 1,337 and 124 at 1.8.1. What is new is the kind: two source sweeps that assert the published key lists and the templates agree in both directions; two rendering sweeps that mount every field type and the whole builder with every key overridden and assert no English default survives in the markup; a per-component accessible-name sweep; property-based fuzzing of resolveUiText over 1,500 seeded override shapes per property; and an E2E that asserts German text does not push the page into a horizontal scroll at 412px. The three fixes above were all found by one of them.

The demo's reference stylesheet also gained flex-wrap on the tab strip and shrinkable header rows: a five-tab form was wider than a phone, and the narrow Playwright project had been rendering that horizontal scroll on every run without asserting on it.

Notes

Only noRows and the field-list wording in the critical-field banner changed shape internally; rendered text is identical. <strong>Add field</strong> in the builder's canvas empty state lost its bold — an emphasis span embedded mid-sentence cannot survive translation, so the sentence is now one key.

Upgrading

Control DOM ids are no longer field.id. An array row repeats the same child ids, so two #name inputs were on the page and the second row's label focused the first. Ids now look like email-de7. Address a control through its data-testid or its label; #fullName is not a contract.

MASKED_PLACEHOLDER, setDateFormatters, UI_TEXT / provideNgxDynamicEntity({ uiText }) and BUILDER_TEXT / uiLanguage are additive. An unconfigured install renders what it rendered in 1.8.1, including the English chrome.


[1.8.1] — 2026-09-01

Hardening, almost all of it found by adding property-based fuzzing over core and integration tests across the three packages.

Fixed

  • core no longer throws on a malformed config. Seven crashes, all the same shape: ?. guards undefined and nothing else, so a config holding a string, a number or null where a collection belongs reached .map or .forEach and threw. validateConfig, collectFieldScopes, normalizeConfig, normalizeConfigOptions, parseFieldRef and evaluateFormRules were all affected — including the validator failing on the input it exists to describe, which took dynamic-entity validate down with it in CI.

    normalizeConfig also skipped its object-to-array conversion for every falsy non-array, because the guard was x && !Array.isArray(x).

  • The builder no longer saves a config validateConfig rejects. An entity with no tabs is an error to core — nothing can render — while the builder reported a warning and left Save enabled. The builder now defers to core, in core's wording.

  • Focus no longer scrolls the page. focusActivePanel exists to tell a screen reader the panel changed; the default focus() also scrolls it into view, which jumps the layout on a short viewport. It now passes preventScroll.

Upgrading

BuilderStore.isValid() returns false for an entity with no tabs, where it returned true. Nothing is stuck: addField creates the first tab when there is none, so one field is enough. If you drive the builder programmatically and asserted on the old value, that assertion changes.

Internal

1,337 unit tests and 124 e2e across two Playwright projects — the second is a narrow viewport, which the responsive grid collapse never had. Core is fuzzed with 1500 seeded runs per property, and the seed is printed on failure so a red run is reproducible.


[1.8.0] — 2026-09-01

Work since 1.7.0: undo and redo in the builder, and three defects found by covering the branches that had none.

Added

  • Undo / redo in the builder. Ctrl/Cmd+Z and Ctrl/Cmd+Shift+Z, plus toolbar buttons that disable at the ends of the history. BuilderStore gains undo(), redo(), canUndo and canRedo.

    History records the {config, rules} pair — they are two signals, and undoing one without the other could leave a rule pointing at a field that no longer exists. Consecutive edits coalesce inside 400ms when the structure is unchanged, so typing a label is one undo step while a fast double-click on the palette is still two.

    The shortcut ignores keystrokes aimed at an input, textarea or contenteditable: those have their own undo stack, and hijacking it would discard a structural edit when the author wanted one character back.

Fixed

  • validateConfig crashed on a malformed config. A null in fields or tabs threw "Cannot read properties of null" — it failed on exactly the input it exists to describe, and took dynamic-entity validate down with it in CI. The main pass guards; the second pass that re-walks the tree for references did not. It now reports the entry and carries on.

  • A date field showed "Invalid Date". new Date('nonsense') does not throw and toLocaleDateString() returns that string, so the try/catch meant to fall back to the stored value was dead code. Records outlive schemas — a field retyped from text to date can hold anything — so an unparseable value is now shown as stored.

  • Radio options with no value shared one input id. The ?? 'opt' fallback could not fire, because option resolution returns '' rather than null. Duplicate ids break the for that ties each label to its input.

  • The form returned to the wrong tab. A form kept mounted while initialData was swapped opened the next record on the previous one's tab. Fixed in two steps: 1.7.0's reset was placed after an early return that a swap could skip, so it is now cleared before that guard. activeTabConfig already falls back to the first visible tab, which is what makes clearing safe on its own.

Internal

Branch coverage rose to 90% in core, 88% in the renderer and 85% in the builder, and the thresholds moved up to hold it. 1,317 unit tests, 102 e2e.


[1.7.0] — 2026-08-31

Work since 1.6.0: a second long-form field type, and a builder that will open the per-scope configs the rest of the stack has accepted since 1.4.0.

Added

  • A markdown field. textarea was the only long-form input. The important decision is what it stores: markdown source, never HTML, so the record stays plain text — diffable, portable, safe to log, and impossible to turn into stored XSS by writing it to a database.

    It works with nothing configured — the editor is a textarea, and a read-only view shows the source with its line breaks preserved and nothing interpreted. That default exists because these packages declare no runtime dependencies beyond tslib, and a markdown parser is a large thing to force on someone who wanted a form library. To render, provide MARKDOWN_RENDERER, in the same shape as UPLOAD_HANDLER and the lookup registries; a Preview tab appears only when one exists, since without it Preview could only echo the source back.

    Rendered HTML is bound through [innerHTML], so Angular's sanitizer strips scripts, inline handlers and javascript: URLs — a backstop, not a licence, and one the specs assert rather than assume. A renderer that throws falls back to the source instead of taking the form down.

    Field types go from 20 to 21. FIELD_TYPE_CATALOG, the JSON Schema enum and both documented lists move together.

Fixed

  • The builder can open a config with the same id in two scopes. people ships personal.address and work.address; loading it raised "Duplicate field id" and, because an error disables Save, made the whole config a dead end where unrelated edits could not be saved either. Id uniqueness is now counted per scope, using core's collectFieldScopes rather than a second copy of the rule. Two ids in one scope remain an error — there they share a control and a record key.

    Relaxing only the check would have turned a safe failure into a corrupting one: every matcher in the store compared bare ids, so mutateField rewrote both address fields on a rename, removeField deleted both, and moveField moved whichever came first. Each now resolves the target once and matches on identity.

  • A markdown field re-renders when its value changes from outside. Under OnPush the rendered output read control.value, which is neither an input nor a template event, so patchValue, a rule or an autoPatch mapping left the previous document on screen.

Documentation

  • How a field is addressed. refererField is the substance of 1.4.0 through 1.6.0 and had appeared in no documentation file at all. It now leads EXTENDING.md: what opens a scope, why [work.address] is the form to write, and the two errors the model prevents.

[1.6.0] — 2026-08-31

Work since 1.5.0. The headline is that permissions.edit now means what it says: it decides whether the fields are editable, not merely whether a Save button is drawn.

Fixed

  • permissions.edit is honoured by the fields, not just the actions block. A role outside the edit list received a fully editable form — it could type into every field and only discovered the record was not its to change when no Save button appeared, which is after the typing rather than before. DynamicFormComponent.isFieldReadonly consulted the readonly input, the field's own flag, readOnlyFields and the critical-field lock, but never the permission. Unlocking a criticalField had the same hole, so a viewer could open the one control that exists to make an edit deliberate.
  • DynamicRecordFormComponent computes permissions at all. It declared a userRoles input and read it from nowhere: no RbacService, no permissions, so the record view was editable for every role. It now resolves them on the same terms as the plain form, cached against config and userRoles and invalidated when either changes.

A config that declares no permissions is unaffected — hasPermission(roles, undefined) is true, so the unrestricted case stays editable. That is the common case, and a unit test pins it alongside the denied case, the granted case, and re-evaluation after a role switch.

Upgrading: if you relied on RBAC-denied fields staying editable, they are read-only now. Pass readonly="false" semantics through your own inputs, or widen permissions.edit, whichever matches what you meant.

Demo

The demo is published at https://berserker5619.github.io/Dynamic-Entity/ and now seeds records for every entity rather than three of eight — insuranceClaims, the richest config here, previously opened to an empty list. It also exposes all three record presentations the renderer supports: the editable form, the record view with its per-tab "Edit section" flow, and a data-only view (isReadOnly) that had no route to it, since a role that could edit always saw the Edit section button.


[1.5.0] — 2026-08-30

Work since 1.4.0: nothing in the builder names a field by typing any more, the validator understands the paths the builder authors, and the rule editor stopped locking the browser.

Added

  • validateConfig understands field paths, and can check rules. A showWhen keyed [work.address], or a cascade parent written the same way, used to be reported as an unknown field — the form the builder authors after 1.4.0. Those paths now resolve to the one field they name. Pass rules (or --rules rules.json on dynamic-entity validate) to apply the same check to a rule's trigger, compareToField and field targets; without that option, rules remain an @Input the config file cannot see and the renderer still warns in development.

Changed

  • The people entity in the reference dataset has an address on Personal Details and another on Work Details, with deskNumber shown only when [work.address] is HQ — a config the validator used to refuse, now the fixture the path tests hold.
  • Every field reference in the builder is now chosen from a list. 1.4.0 covered the rule form; showWhen, both ends of a patchOnTrue mapping and an autoPatch target were still text boxes, and the cascade parent was a list of bare ids. All of them offer the same field paths. Typing was the one way left to author a reference that names two fields at once.
  • A Tab picker on the field inspector. moveFieldToTab shipped in 1.4.0 with nothing calling it. Moving a field rewrites its path and repoints the rules that named it.
  • setActiveTab(tabId, { focusPanel: false }). Activating a tab moves focus into its panel, which is right for a keyboard user pressing a tab. A quick-jump also switches tabs and then focuses the field it was aiming at — and the panel focus runs on requestAnimationFrame, after afterNextRender, so it landed second and took the focus back. A caller that will focus something more specific can now say so.

Fixed

  • The rule editor locked the browser. [ngModel] on the targets multi-select was bound to a method returning a fresh array on every call, so ngModel saw a new value on each change-detection pass: it wrote, which scheduled another pass, which built another array. Clicking "Rule" froze the page outright. The array identity is now held stable while its contents are unchanged.
  • A new showWhen condition seeded the literal string field, which is not a field id — so the condition referenced nothing and hid the field until someone noticed. It seeds a real field, falling back to the placeholder only when there is nothing else to watch.
  • The cascade parent offered the field as its own parent.
  • Three capabilities the demo could not reach, and so nothing tested: the builder always opened a blank entity and can now open any saved one; the record editor — the only host of the quick-jump links — was never rendered; and no entity marked a field showOnMinimize.

[1.4.0] — 2026-08-29

Work since 1.3.0: a field is now addressed by its path rather than its id, so two tabs may each have an address; the builder can relocate a field and show the ones nested in sub-tabs; and rules are chosen from a list instead of typed.

Added

  • dynamic-entity validate. validateConfig was an API you had to wrap yourself. The core package now ships a bin that reads a JSON file, prints every problem, and exits 1 when any of them is an error — so a consumer can gate configs in CI with npx dynamic-entity validate ./form-config.json. --additional-field-types and --fail-on-warnings cover the two options the function already had. This is not a new check; it is the existing one on a command line.
  • A stated SSR position, and a job that exercises it. The renderer is intended to work under Angular SSR; the builder is a Material visual editor and is not an SSR target. CI packs the published tarballs and calls renderApplication on Angular 20, so the claim is a passing job rather than the absence of document access.
  • Zoneless SSR. The renderer does not use NgZone. CI now renderApplications the same form under provideZonelessChangeDetection() with no zone.js on the machine. The demo still loads zone because it is an Angular 17 Material app; that is the demo, not the library.
  • A field is addressed by its path. refererField now carries the scopes a field's value nests under, then its id — work.address. A rule or condition names a field by bracketing it, [work.address], and the builder authors that form for every new rule. A bare id still resolves, so every config and rule written before this keeps working; the runtime emits both keys and evaluateFormRules needed no change at all. The path is maintained rather than derived: the builder restamps it after each structural edit and repoints the rules that named what moved. A refererField the config declares is never rewritten — it has always been a binding override, and taking one over as an identity would silently rebind data.
  • moveFieldToTab. The builder could add, remove, duplicate and reorder a field but never relocate one, so a field authored on the wrong tab had to be deleted and rebuilt — losing its validators, options and every rule aimed at it.
  • Rule fields are chosen, not typed. The rule form had a free-text trigger id and no targets UI at all, so a rule could only ever act on the field it triggered from. Both are now pickers over the config's fields, each option carrying its path, which closes the last route to authoring an ambiguous reference.

Changed

  • Choice-field any is gone. dropdown / radio / multiSelect already store a DropdownOption (LocalizedText) — the displayed text is the value — so a generic on the field config would have described a contract the library does not have. getOptionStoredValue and resolveOptionValue return unknown and string | number | boolean rather than any, and the three field components follow.
  • Field ids are unique per scope, not across the config. A record nests by tab, the form builds a FormGroup per tab, and getControl already resolved a field in its own tab first — so Personal Details and Work Details could each hold an address all along, stored and submitted separately. Only validateConfig refused such a config. It now enforces uniqueness within a scope, computed exactly as buildForm computes it: a tab opens one, a flatData tab shares its parent's, a group field opens one for its children. Two fields sharing an id inside one scope is still an error. What cannot be duplicated is an id something points at by bare name: showWhen and cascade parents are reported as ambiguous, and the renderer warns in dev when a rule does the same, since rules arrive as an @Input the validator cannot see.
  • The workspace toolchain moved to Angular 21. The published peer range was already 17–22; only the repo's own build and test stack was still on 17.

Fixed

  • Fields on a sub-tab were invisible in the builder. The canvas read a view that stopped at top-level tabs — nine of the demo's twenty-eight insuranceClaims fields never appeared, and could not be selected or restructured. The same view also fed the entity-reference picker, which could not offer a nested field as a cascade parent, and the drift check, which looked a nested field up, found nothing and returned without checking. Showing them exposed a second defect: drag-and-drop reorders by index and the canvas passed that index with no tab, so it reordered tabs[0] regardless of what was dragged. The canvas now renders one drop list per tab.

[1.3.0] — 2026-08-29

Work since 1.2.0: datetime stopped discarding the time it advertised, time joined the vocabulary, the quick-jump links started working for the fields they could never reach, and two accessibility specs that had been skipping themselves started running.

Added

  • A time field type. A bare time of day, with no date and no zone. TimeFieldComponent renders <input type="time"> and stores HH:mm — the value the input already reads and writes, so the control binds straight through. This is deliberately not datetime: a 09:00 opening time is not a moment in time, and storing it as UTC would move it whenever the offset changed. Twenty field types now, one component each.

Fixed

  • datetime rendered a date-only input, so editing truncated the time. The type was in RichFieldType, in the published JSON Schema, accepted by validateConfig, and offered by the builder palette as "Date & Time — Date and time picker" — and it resolved to DateFieldComponent, whose input is type="date". Saving a record whose datetime field held a time silently dropped it. The two display paths disagreed as well: formatDisplayValue showed the time, the field's own readonly branch did not. DateTimeFieldComponent renders datetime-local, stores ISO 8601 UTC, and displays with toLocaleString(). It reads a legacy date-only value as local midnight, because new Date('2020-01-01') is UTC midnight and renders as the previous day west of Greenwich — and every value written by the old input has that shape.
  • Quick-jump links did nothing for any field in a sub-tab, and never moved focus. jumpToField searched top-level fields only, so a sub-tab field was never found; its target was a plain div, so el.focus() was a no-op; and it waited on a 50 ms setTimeout that touched an unguarded document and was never cancelled on destroy. It now walks sub-tabs and selects them, schedules with afterNextRender, and the field slot carries tabindex="-1". There is no longer any raw document or window access in either library.

Changed

  • Field slots carry tabindex="-1" so a programmatic jump can focus them.
  • The builder's per-file coverage floor rose from 76/50/50/79 to 85/75/85/85, matching the other two packages; global rose to 95/82/97/97. Reaching it meant first specs for the canvas and tree-node components, edge coverage for the inspector and rules editor, and deleting a dead onDrop/fieldTypeLabel/fieldTypeIcon/fieldLabel block that the canvas extraction had left on EntityBuilderComponent.
  • Two accessibility specs stopped skipping themselves. Both guarded on what the fixture happened to contain: the tab-focus spec loaded an entity with exactly one tab, so it had never run, and the builder spec needed two rows from a builder that opens empty. The suite is 72 passed, 0 skipped.

1.2.0 — 2026-08-28

Work since 1.1.0: config can be checked before it is stored, a save can be vetoed, referenced-field drift is visible on the form, and the field components stopped re-rendering on every change-detection pass.

Added

  • validateConfig and a JSON Schema. A config is data, so TypeScript cannot police it — this repository's own dataset shipped field types that do not exist and nothing noticed. validateConfig reports every problem (unknown types, duplicate ids, showWhen/parentField pointing nowhere, colSpan outside the 12-column grid). entity-form-config.schema.json ships as @dynamic-entity/core/schema for editor completion.
  • Async validators and an abortable beforeSave. Name them with validators.customAsync and provideNgxDynamicEntity({ asyncValidators }). Pending checks block submit. The ${entity}:beforeSave hook can now return false or throw to stop the save; (saveRejected) reports why.
  • Runtime drift. hasDrift was written by the builder and ignored at runtime. A referenced field whose source has changed now shows a role="status" note naming that source.
  • A stylesheet, a typed field contract, and overridable validation messages. ngx-dynamic-entity/styles.css is optional. Custom fields implement DynamicFieldComponentContract. Messages resolve through ValidationMessagesService, overridable per key via provideNgxDynamicEntity({ validationMessages }).
  • insuranceClaims in the demo dataset, with Playwright coverage for the happy path, hostile edges, and composed multi-feature flows.

Fixed

  • autoPatch actually appears. Entity-ref selection publishes after the control updates, readonly text tracks the patched value, and hosted fields share the form's selection bus so a concurrent form cannot leak a pick.
  • The builder cache is dropped on save. ConfigSourceService.clearCache existed and was never called, so a referenced-field lookup after an edit still saw the copy loaded before it.
  • @Input() config is no longer mutated. Normalisation is an accessor over a copy. The builder's label setters copy only the path to the edited field instead of cloning the whole config per keystroke.
  • core no longer publishes its build toolchain. A derived dist manifest ships; scripts and devDependencies do not.
  • Icon-only builder actions have accessible names; builder rows are keyboard operable; tab switches move focus into a tabpanel; three contrast failures below WCAG AA are corrected.

Changed

  • Field components are OnPush. External mutations (markAllAsTouched, patchForm, autoPatch, patchOnTrue) refresh the hosted component so an OnPush field does not keep showing a stale value.

Documentation

  • CONTRIBUTING, SECURITY.md, issue and PR templates. mongodb-memory-server removed from the root (unused, ~200MB).

1.1.0 — 2026-08-28

The headline of this release is that 1.0.0 could not be installed. Its peer ranges admitted Angular 17 only, its main field pointed at a path absent from the tarball, and the builder shipped a wildcard runtime dependency that installed a second copy of the renderer. Everything below follows from fixing that and then verifying the rest of the claims the packages were making.

Upgrading

Behaviour changes that can affect an existing app are listed under Changed — most notably permissions.view is now honoured. Everything else is additive or a bug fix. No config or record migration is required to move from 1.0.0.

Fixed — packaging

  • Angular 17 through 22 are supported. Peer ranges were pinned to ^17.0.0, so npm install failed with ERESOLVE on any newer Angular. Each major is now verified in CI by installing the packed tarballs and AOT-compiling a consumer component.
  • main no longer points outside the tarball. A hand-written main was copied verbatim into the published manifest, where it resolved to dist/dist/…. Anything falling back to main — Jest's resolver, CJS require, SSR tooling — could not load the package.
  • The builder no longer installs a second copy of the renderer. ngx-dynamic-entity was declared as both a wildcard dependency and a peerDependency. Because Angular resolves InjectionToken by object reference, a duplicate copy meant registries provided by the host application were invisible to the builder and fields rendered blank.
  • @angular/material and @angular/cdk are no longer peers of ngx-dynamic-entity, which imports neither. The builder still requires both.
  • sideEffects: false added to @dynamic-entity/core.
  • repository, homepage, bugs and author added to all three manifests.

Fixed — correctness

  • Submission is blocked while a validation rule is failing. submit() checked only Angular form validity, while the record editor's saveSection() also honoured rule errors — so the same rule blocked one save path and merely showed a banner on the other, and anything wired to (formSubmit) persisted records the rules engine had rejected.
  • A hidden required field no longer deadlocks the form. Fields hidden by a rule or a showWhen condition kept their validators, holding form.invalid true forever with the Save button disabled and nothing on screen to explain it. Hidden controls are now disabled, which excludes them from validity while preserving their values and validators.
  • permissions.view is enforced. It was computed and discarded: a user whose roles failed it still received the complete form with every value in the DOM.
  • The record editor's summary reads through the tab nesting. It read values flat while the form patched by tab path, so a flat record rendered real values in the summary over a form whose controls were all empty — data loss disguised as a successful load.
  • Dot-paths cannot reach an object's prototype. setValueByPath walked config-supplied paths with no guard, so a refererField of __proto__.isAdmin polluted Object.prototype. __proto__, constructor and prototype are now refused on both the read and write paths.
  • Drift detection is key-order independent. It compared with JSON.stringify, so a config round-tripped through a backend that orders keys differently reported drift on every referenced field.
  • SYSTEM_DEFAULT_CAN_EDIT receives real roles. It was invoked with a hardcoded empty array, so any predicate that inspected roles answered false for everyone. The token was also declared twice under the same name in two packages; since token identity is by reference, providing the documented one did nothing. There is now a single token.
  • The email validator no longer collides with pattern. The builder expressed "email" by writing a regex into validators.pattern, so a field could not have both, a custom pattern made the Email box appear ticked, and un-ticking Email deleted the pattern.
  • Referenced-field drift is checked against the edited field, not whichever field happened to be selected.
  • The builder's remove, duplicate, move and reorder now reach fields on sub-tabs, and id uniqueness is validated across the whole tree rather than top-level tabs only.

Added

  • Schema migration. EntityFormConfig.version and VersionedRecord._configVersion were declarations nothing read. @dynamic-entity/core now exports migrateRecord, needsMigration, stampRecord, validateMigrations and the RecordMigration type — pure, so the same steps run in a browser and on a server. Register them with provideNgxDynamicEntity({ migrations }) and they are applied where a record enters the form. An unstamped record is deliberately left alone, and a gap in the chain throws rather than half-upgrading. See the README's Schema versioning section.
  • A dev-mode warning when initialData is silently dropped. A record is nested by tab id unless the tab sets flatData: true; passing a flat record to a nested tab populated nothing and reported nothing. The renderer now names the keys that went unused.
  • registerFieldType opens the field-type catalog. The lookup index was frozen at module evaluation, so a custom type pushed onto FIELD_TYPE_CATALOG was invisible to the builder's palette and to createFieldConfig.
  • All 18 field components are exported. Only 8 were, which defeated the provideFieldTypes({ … }) tree-shaking seam the package documents: wanting eleven of them meant bundling all of them.
  • FieldValidators.email and FieldValidators.custom. Custom validators registered through provideNgxDynamicEntity({ validators }) were reachable only from the untyped string[] form, so naming one from a typed schema required casting to any.
  • DynamicFormComponent.canDelete, canView, ruleValidationErrors and submitBlocked.
  • EntityBuilderComponent.userRoles, distinct from availableRoles — who is editing, rather than the role vocabulary a schema may reference.
  • HookFn type, replacing Function in the hook registry.

Changed

  • permissions.view now hides the form. Previously it was ignored, so a config that set it rendered as though it had not. This is presentational only — masking and permissions stop the browser drawing data, they do not stop it reaching the browser. Authorize on the server.
  • The builder writes validators.email instead of a regex in validators.pattern. Configs authored by the previous builder are still recognised, and are migrated as they are edited.
  • ConnectionSourceConfigComponent has been removed from ngx-dynamic-entity-builder's public API. It wrote a connectionSource property that is not part of NestedFieldConfig and that nothing read.

Documentation

The READMEs described an API the packages did not have, and the Quick Start did not compile. Removed: the SHOW_WHEN / ENABLE_WHEN / REQUIRE_WHEN / CALCULATE rule types (the real action types are visibility, validation and info), the READ_WRITE / READ_ONLY / MASKED / HIDDEN permission levels (the model is view/edit/delete role lists plus maskData), and "dynamic table rendering" — the package ships no table. Corrected the field type list (19 types, entity-ref not entityRef), the Quick Start bindings (initialData and userRoles, not initialValue and role), and FIELD_CATALOGFIELD_TYPE_CATALOG. Added sections on record shape, security, styling and schema versioning.

Every fenced code block in every README is now extracted and compiled in CI.

Internal

  • CI was an empty directory. There are now three workflows: verification (lint, build, test, coverage) plus an Angular 17–22 consumer matrix and a README-snippet compile; a Playwright job; and a tag-driven release that verifies before it publishes and authenticates through npm trusted publishing rather than a long-lived token.
  • eslint could not load a TypeScript file — @typescript-eslint/parser was declared but never installed — so every rule had been dormant and lint had quietly become tsc --noEmit.
  • npm run test:coverage had never passed in any package. It does now, in all three.
  • Deleted src/lib/stores/, an abandoned extraction of 162 unreferenced lines.
  • test_data.json used three field types that do not exist; the spec that "rendered" it watched only for uncaught exceptions and passed green over them.

Initial public release.