Clarin9/Port per-field type binding (submit.type-bind.field "A=>B") (#876) - #1431
Conversation
…876) Upstream type-bind supports exactly one global controlling field. LINDAT needs per-field control, expressed as `submit.type-bind.field = dc.type, dc.language.iso=>edm.type`, and none of that machinery was ported to the v9 branch: `FormFieldModel` had no `typeBindField` (so cerialize dropped the REST value), the parser always stamped the relation with the global type field, and `FormBuilderService` kept a single `typeField` string and a single type bind model. Selecting `edm.type = TEXT` therefore evaluated the relation against the empty `dc.type` model and the dependent language field kept its `d-none` class. - `FormFieldModel.typeBindField` is deserialized again. - `FormBuilderService` keeps a map of controlling fields (default + one entry per `A=>B` override, order-independent, duplicate-safe, trimmed) and a map of registered controlling models, plus a subject that emits every registration. `getTypeBindModel(ref?)` takes an optional ref so all existing call sites and mocks keep working. - `FieldParser.getTypeBindFieldRef()` stamps the relation with the controlling model id when `<type-bind field="...">` is declared, and otherwise with the field's own metadata name, which is resolved against the map later - the property arrives over REST asynchronously. - `DsDynamicTypeBindRelationService` passes the relation id through, no longer dereferences a missing bind model, and attaches to a controlling model that is only registered by a later `modelFromConfiguration()` call. Deliberate deviations from the 7.x implementation (documented in the PR): `findById` and `row-parser` are left untouched, `typeBindField` is not carried on control models, and the inverted second clause of the 7.x `getTypeBindModel` guard is dropped - it is a no-op for the LINDAT config and would otherwise fall back to `dc_type`, reproducing this very bug. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR ports and extends DSpace submission “type-bind” behavior to support per-field controlling type fields (e.g. dc.language.iso=>edm.type) in the Angular UI, ensuring dependent fields show/hide correctly when their configured controlling field changes.
Changes:
- Adds
typeBindFieldtoFormFieldModelso<type-bind field="...">survives REST deserialization. - Refactors
FormBuilderServiceto support multiple controlling fields via maps, plus a registration-updates observable. - Updates field parsing and type-bind relation evaluation to resolve the correct controlling model and handle late availability; adds targeted unit tests.
Reviewed changes
Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| src/app/shared/mocks/form-builder-service.mock.ts | Extends mock to support getTypeBindModelUpdates() for updated type-bind relation logic. |
| src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts | Adds tests for deriving the type-bind relation id from <type-bind field="..."> vs fallback behavior. |
| src/app/shared/form/builder/parsers/field-parser.ts | Uses a new resolver for type-bind controlling field refs (explicit <type-bind field> vs metadata-name fallback). |
| src/app/shared/form/builder/models/form-field.model.ts | Introduces autoserialized typeBindField property. |
| src/app/shared/form/builder/form-builder.service.ts | Replaces single type-bind field/model with maps + update stream; parses submit.type-bind.field including A=>B entries. |
| src/app/shared/form/builder/form-builder.service.spec.ts | Adds a dedicated suite covering per-field model resolution, fallbacks, updates emission, and end-to-end parsing behavior. |
| src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts | Passes relation ids through to model resolution; guards missing controlling models; supports late attachment. |
| src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.spec.ts | Adds tests for passing relation id, missing-model behavior, and late attachment. |
…arden parsing Behaviour: - subscribeRelations now returns a single owning Subscription. The caller spreads the returned array into its own, so a child created after that snapshot - which is exactly what the late-registration listener does - could never be torn down and kept mutating hidden/disabled on a destroyed model. - Always listen for type bind model registrations, not only when nothing was attached: until the real controlling model exists the field is temporarily attached to the default one, and that case was never re-attached (Copilot). - The type bind registry is now dropped when the submission changes. Sections of one submission still share it (a controlling field may live in another section), but a model from the previously opened collection's form can no longer answer lookups and defeat the fall-back-to-default behaviour. - Controlling models are registered again once submit.type-bind.field arrives, so an override that exists only in the property - with no <type-bind field="..."> in the XML - still resolves if the config lands after the form was parsed. - A self-referencing type bind is now a console.warn + skip instead of a throw: it can be raised from the registration callback, and one misconfigured field should not take down the whole submission section. - Tolerate blank/whitespace/malformed values in the property (a null entry used to throw inside the subscribe) and trim typeBindField before building the model id (Copilot). getTypeBindModel is typed `| undefined` (Copilot). Tests: late registration now asserts the relation is really re-evaluated and stops on unsubscribe; added the attached-to-default case, cross-submission isolation, a delayed configuration response, malformed values, and a cerialize round-trip proving typeBindField survives deserialization. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/app/shared/form/builder/form-builder.service.ts:390
modelFromConfiguration()appends every parsedrowsarray totypeBindParsedRows, but that cache is only cleared when the submission id changes. If the same submission section is re-parsed (e.g. section form rebuilds during updates), this can grow without bound and retain large model graphs for the lifetime of the submission, increasing memory usage and making laterregisterTypeBindModelsre-walk more data than needed.
Consider caching rows only until submit.type-bind.field has been processed, then clearing and disabling further caching (e.g. a typeBindConfigLoaded flag set in setTypeBindFieldFromConfig(), and only push() when it’s false).
if (hasValue(typeBindModel)) {
this.setTypeBindModel(typeBindModel);
} else {
this.typeBindParsedRows.push(rows);
this.registerTypeBindModels(this.getTypeBindModelIds(rawData), rows);
}
src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts:117
- The comment above the
hasNoValue(bindModel)guard implies the controlling model might be absent from the form, butFormBuilderService.getTypeBindModel()can fall back to the default model in that case. This makes the comment misleading for future maintenance: theundefinedcase here primarily means the controlling model hasn’t been registered yet (e.g. parsed later / config not arrived), not that it is permanently absent.
// The controlling model may not be part of this form at all (e.g. `dc.language.iso=>edm.type`
// is configured deployment-wide but this collection's form has no `edm.type` field), or it may
// not be registered yet. Keep MATCH_VISIBLE fields hidden until it becomes available.
…omment - typeBindParsedRows is only filled until submit.type-bind.field has been processed, and is dropped once it has. A section form re-parses on every data update, so the cache would otherwise keep growing and retain the model graph of every re-parse for the lifetime of the submission. Without a config service the map can never change, so nothing is cached at all. - Reword the matchesCondition guard comment: getTypeBindModel falls back to the default model, so no model at all means neither the field's controlling model nor the default one has been registered yet - not that the controlling field is permanently absent from the form. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/app/shared/form/builder/form-builder.service.ts:450
getTypeBindModelIds()addsfield.typeBindFielddirectly after anisNotEmptycheck, butisNotEmpty(' ')is true in this codebase. If the REST/XML ever contains whitespace around/insidetypeBindField, the collected id can become e.g.' edm_type 'and the controlling model will never be registered (even thoughgetTypeBindFieldRef()correctly trims). Trim (and re-check) before replacing dots so the pre-registration path is consistent with relation ids.
(formRows || []).forEach((formRow: FormRowModel) => (formRow?.fields || []).forEach((field: FormFieldModel) => {
if (isNotEmpty(field?.typeBindField)) {
ids.add(field.typeBindField.replace(/\./g, '_'));
}
isNotEmpty(' ') is true in this codebase, so a padded `<type-bind field=" edm.type ">`
would have registered the controlling model as ' edm_type ' while
FieldParser.getTypeBindFieldRef - which does trim - stamps the relations with
'edm_type'. The lookup would then miss, fall back to dc_type and leave the
dependent field permanently hidden, i.e. reproduce the very bug this PR fixes.
The existing end-to-end spec now uses a padded value, so it fails without the trim.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot suppressed comment on |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts:262
- Self-referencing type-bind relations are warned about and skipped in getRelatedFormModel(), but evaluateRelations() still evaluates the relation via matchesCondition(). If the controlling model is the same as the bound model, this can still hide/disable the control during the initial evaluation (and can make the field permanently unreachable). Consider explicitly skipping evaluation of self-bound relations here as well.
this.dynamicMatchers.forEach((matcher) => {
// Find the relation
const relation = this.dynamicFormRelationService.findRelationByMatcher((model as any).typeBindRelations, matcher);
// If the relation is defined, get matchesCondition result and pass it to the onChange event listener
if (relation !== undefined) {
const hasMatch = this.matchesCondition(relation, matcher);
matcher.onChange(hasMatch, model, control, this.injector);
}
Attachment was deduped by model id, but setTypeBindModel emits on identity: when the section holding the controlling field is re-parsed (section forms re-parse on every data update) a NEW instance is registered under the same id, and a bound field in another section kept listening to the dead one - so the dependent field stopped reacting, which is the A3 symptom again in a narrower form. Attachment is now keyed by id but compared by identity, and the stale child subscription is removed from the owning Subscription and unsubscribed, so nothing accumulates across re-parses either. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Skipping the self-reference in getRelatedFormModel only stopped it from being subscribed to; evaluateRelations still ran matchesCondition against the field's own (empty) value on the initial pass, hid the field, and - with nothing attached - never re-evaluated it, so a misconfigured <type-bind field="..."> pointing at its own metadata field made that field permanently unreachable. subscribeRelations now detects the self-reference up front, warns once and returns without evaluating or attaching anything, leaving the field exactly as rendered. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot suppressed comment on |
…ling model The self-reference guard used the runtime lookup, which falls back to the default model. For a field whose own id IS the default model id, a relation whose real target simply had not been parsed yet therefore looked like a misconfiguration: subscribeRelations bailed out before wiring the registration listener, so the field never picked up its controlling model. Resolve the reference from configuration only (new FormBuilderService.resolveTypeBindModelId, order-independent) to decide whether the field really is bound to itself; the transient case now just skips the initial evaluation - so the field is not hidden for no reason - and still attaches when the real model is registered. Also add the spec that actually pins the getTypeBindModelIds trim. The one added in 79f94f1 did not: the suite's config already contributes 'edm_type' through its A=>B entry, so the padded id was merely an extra miss. The new case configures only 'dc.type', making the padded <type-bind field=" edm.type "> the sole source of the controlling id - verified to fail with the trim removed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Correction to my earlier comment on the Same commit also fixes a defect in the self-reference guard added in f25829c: it used the runtime lookup, which falls back to the default model, so for a field whose own id is the default model id a relation whose real target had not been parsed yet looked like a misconfiguration and the field never picked up its controlling model. The guard now resolves from configuration only ( |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (4)
src/app/shared/form/builder/form-builder.service.ts:182
resolveTypeBindModelId()is declared to returnstring, but the current expression can returnundefinedif the default entry is missing (and it also relies onMap.get()which returnsstring | undefined). Add an explicit final fallback so callers always receive a concrete model id.
resolveTypeBindModelId(typeBindFieldRef?: string): string {
return this.typeFields.get(typeBindFieldRef) ?? typeBindFieldRef ?? this.typeFields.get(TYPE_BIND_DEFAULT_KEY);
}
src/app/shared/form/builder/form-builder.service.ts:173
getTypeBindModel()assumesTYPE_BIND_DEFAULT_KEYis always set, butMap.get()returnsstring | undefined, which can flow intoMap.get(defaultModelId)and make the fallback path type-unsafe (and potentially undefined at runtime if the map is ever cleared/mutated). Make the fallback explicit so this method always queries with a concrete default id.
This issue also appears on line 180 of the same file.
getTypeBindModel(typeBindFieldRef?: string): DynamicFormControlModel | undefined {
const defaultModelId = this.typeFields.get(TYPE_BIND_DEFAULT_KEY);
return this.typeBindModel.get(this.resolveTypeBindModelId(typeBindFieldRef)) ?? this.typeBindModel.get(defaultModelId);
}
src/app/shared/form/builder/models/form-field.model.ts:141
typeBindFieldis optional in the REST payload (and the new spec asserts it remainsundefinedwhen absent), but the model declares it as a requiredstring. Mark it optional to reflect the actual deserialization behavior and prevent unsafe assumptions in callers.
@autoserialize
typeBindField: string;
src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-type-bind-relation.service.ts:82
- The inline comment references
dependsOnItself(), but that method doesn’t exist in this service (the relevant guard isisConfiguredToDependOnItself). Updating the reference avoids confusion while debugging type-bind evaluation.
if (bindModel.id === model.id) {
// A misconfigured <type-bind field="..."> pointing at the field itself - see
// dependsOnItself(), which stops the relation from being evaluated at all.
return;
}
resolveTypeBindModelId declared `string` but ended in a bare Map.get, and getTypeBindModel fed the same possibly-undefined value into its fallback lookup. Both now go through getDefaultTypeBindModelId(), which falls back to the TYPE_BIND_DEFAULT_MODEL_ID constant that also replaces the four scattered 'dc_type' literals. Also fix a comment still naming the pre-rename dependsOnItself(). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot suppressed comments on the review of c0891cf — 3 of 4 applied in 7312c5b:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/app/shared/form/builder/form-builder.service.ts:733
getTypeField()is declared to returnstring, butMap.get()can returnundefined, so the currentreturn this.typeFields.get(TYPE_BIND_DEFAULT_KEY);violates the signature and can propagate an unexpectedundefinedto call sites. Use the existinggetDefaultTypeBindModelId()helper (which guarantees a fallback) for the return value.
return this.typeFields.get(TYPE_BIND_DEFAULT_KEY);
It was the one remaining place that returned a bare Map.get for a method declared to return string. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot suppressed comment on |
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (2)
src/app/shared/mocks/form-builder-service.mock.ts:50
- In the mock,
resolveTypeBindModelIdis currently configured to returnundefined. The realFormBuilderService.resolveTypeBindModelId()returns a string model id, and callers (e.g.DsDynamicTypeBindRelationService.isConfiguredToDependOnItself) may rely on that. Returningundefinedin the shared mock can lead to test/runtime errors if any code starts using the return value as a string.
getTypeBindModelUpdates: EMPTY,
resolveTypeBindModelId: undefined,
src/app/shared/form/builder/models/form-field.model.ts:141
typeBindFieldis optional in the REST payload (and the new spec asserts it can beundefined), but it’s typed as a requiredstring. This makes it easy for future call sites to dereference it unsafely (TypeScript will assume it’s always present). Mark it optional to match runtime behavior and to keep the?.trim()usage in parsers type-correct.
@autoserialize
typeBindField: string;
…ield - The shared FormBuilderService mock returned undefined from resolveTypeBindModelId while the real one always returns a model id. It now mirrors the real implementation for a reference the type field map does not remap, so a caller that starts using the value does not silently get undefined. - typeBindField is genuinely optional in the REST payload and the code already treats it that way (`?.trim()` in FieldParser and in getTypeBindModelIds, and a spec asserting it stays undefined when the attribute is absent). I argued for consistency with the other non-optional @autoserialize members earlier; the usage asymmetry is the stronger argument, so it is now declared optional. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Both Copilot suppressed comments from the 9d9c747 review applied in 4cde913:
|
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 9 out of 9 changed files in this pull request and generated no new comments.
Suppressed comments (1)
src/app/shared/mocks/form-builder-service.mock.ts:55
- The mock’s
resolveTypeBindModelIdcallFake takestypeBindFieldRef: string, but the real method accepts an optional ref (typeBindFieldRef?: string). Keeping the mock signature aligned avoids type drift and matches the existing??fallback (which only makes sense forundefined/null).
// mirror the real implementation for a reference that the type field map does not remap
formBuilderService.resolveTypeBindModelId.and.callFake((typeBindFieldRef: string) => typeBindFieldRef ?? 'dc_type');
resolveTypeBindModelId takes an optional ref, which is what the `??` fallback in the fake is there for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Copilot suppressed comment on |
Keep the non-obvious reasoning, drop the prose around it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Problem description
Fixes A3 of dataquest-dev/dspace-customers#876 (analysed in dataquest-dev/dspace-customers#871).
Admin → new submission into LINDAT / CLARIAH-CZ DH Data → set
input#edm_type= TEXT. The dependent field isrendered in the DOM (
label#label_27_array= "Pick the languages of the TEXT *") but its container keeps theBootstrap hide class (
div.mb-2.d-none,ds-dynamic-form-control-container.d-none), so the field never appears andthe item cannot be filled in. On 7.6.5 selecting TEXT reveals it.
The backend is fine — the submission-form payload is byte-identical between 7.6.5 and 9.3 and does contain
"typeBind": ["TEXT", …], "typeBindField": "edm.type".Analysis
Upstream type-bind supports exactly one global controlling field. LINDAT needs per-field control, expressed as
submit.type-bind.field = dc.type, dc.language.iso=>edm.type("dc.language.isois controlled byedm.type,everything else by
dc.type"). None of that was ported to the v9 branch:dtq-dev(7.x)dtq-dev-9-baseFormFieldModel.typeBindField@autoserializefield-parserrelation idparserOptions.typeFieldFormBuilderService.typeFieldMap+TYPE_BIND_DEFAULT_KEYstringsetTypeBindFieldFromConfigA=>Bvalues[0], drops the restgetTypeBindModel()Net effect:
dc.language.isogets a relation whose condition id isdc_type,matchesConditionreads the (empty)dc.typemodel,'TEXT' !== '', the hidden matcher fires and the container keepsd-none.What this PR does
FormFieldModel.typeBindField— add the@autoserializeproperty so<type-bind field="edm.type">survivesdeserialization.
FormBuilderService—typeFields: Map<string,string>(TYPE_BIND_DEFAULT_KEY→ default model id, plus oneentry per
A=>Boverride) andtypeBindModel: Map<string,DynamicFormControlModel>keyed by model id, with atypeBindModelUpdatessubject.getTypeBindModel(ref?)takes an optional ref, so every existing zero-arg callsite and mock keeps working. The registry is scoped to one submission: sections of the same submission share it
(a controlling field may live in another section), but it is dropped when the submission changes so a model from
the previously opened collection's form cannot answer lookups. Registration is re-run when
submit.type-bind.fieldarrives, so an override that exists only in the property still resolves if the configlands after the form was parsed.
field-parser.getTypeBindFieldRef()— the relation condition id is now the controlling model id when<type-bind field="…">is declared, and otherwise the field's own metadata name, resolved later against thesubmit.type-bind.fieldmap (that property arrives over REST asynchronously, which is exactly why 7.x defers it).DsDynamicTypeBindRelationService— pass the relation id through togetTypeBindModel(…), guard the twoplaces that dereferenced the bind model unconditionally, and keep listening for registrations so the real
controlling model is attached even when the field was temporarily bound to the default one.
subscribeRelationsreturns a single owning
Subscription: the caller spreads the returned array into its own, so a child createdafter that snapshot would otherwise never be torn down.
form-builder-service.mock—getTypeBindModelUpdates: EMPTY.Deliberate deviations from a 1:1 7.x port (please read before diffing against
dtq-dev)findByIdis not widened to acceptstring[]. 7.x had to, and even then it registered only one model percall (later matches overwrote the result). Instead
modelFromConfigurationloops the candidate ids and calls theunchanged
findById(id, rows)once per id —findByIdhas ~40 call sites and its own spec coverage, and this alsofixes the one-model-per-call limitation.
row-parserandparserOptionsare untouched. 7.x repurposesparserOptions.typeFieldinside the row loop tomean "this field's own metadata name", mutating a shared object that is handed to an
Injectorby reference. On v9typeFieldis a real parameter ofRowParser.parse()with 10 assertions in its spec;FieldParseralready knowsthe field id, so the same information is available one level down with zero changes there.
controlModel.typeBindFieldis not carried on control models. Its only reader on 7.x is the guard below; theparser reads
configData.typeBindFielddirectly.getTypeBindModelguard is dropped. 7.x hasif (isUndefined(candidate) || isNotUndefined(candidate.typeBindField)) return default;. The second clause isinverted with respect to its own comment, and the candidate here is always a controlling model whereas
typeBindFieldmarks a bound field — for the LINDAT config it is a no-op, and if it ever did fire it would fallback to
dc_type, i.e. silently reproduce this very bug. Clause 1 (fall back to the default when the configuredcontrolling field is not part of this collection's form) is kept.
A=>Bparsing is order-independent, duplicate-safe and trimmed. The deployment currently reports['dc.type', 'dc.type', 'dc.language.iso=>edm.type']; 7.x sets the default key on every iteration and would set itto
''if the=>entry came first, and does not trim, so", dc.language.iso=>…"would produce a key that nevermatches. Blank and malformed values (
'','=>','a=>b=>c', a null entry) are skipped with a warning.FormControl X cannot depend on itself, but with per-field ids that comparison had gone inert; restoring it as athrow would mean one misconfigured
<type-bind field="…">takes down the whole submission section, and it can nowbe reached from the registration callback. One field degrades instead.
Tests
form-builder.service.spec.ts— new sibling suite: duplicated property still yieldsdc_type; per-field modelresolution (
dc.language.iso→edm_type, direct model id, unmapped field → default, no-arg → default); fallbackwhen the controlling model is not in the form;
getTypeBindModelUpdates()emits every registration; and anend-to-end
modelFromConfigurationtest that reproduces the LINDAT shape and assertstypeBindRelations[0].when[0].id === 'edm_type'. The existing suite is unchanged (shared helpers were hoisted tomodule scope so a sibling describe can reuse them).
form-builder.service.spec.ts— also: cross-submission isolation, a delayed configuration response (theproperty-only override still resolves), and blank/malformed values.
ds-dynamic-type-bind-relation.service.spec.ts— 4 new specs: the relation id is passed to the form builder; amissing controlling model keeps a MATCH_VISIBLE field hidden instead of throwing; a late registration really
re-evaluates the relation and stops when the caller unsubscribes; the same when the field was first attached to
the default model.
form-field.model.spec.ts(new) — a cerialize round-trip provingtypeBindFieldsurvives deserialization, whichis the root cause and was otherwise only exercised through hand-built object literals.
onebox-field-parser.spec.ts— 2 new specs for the twogetTypeBindFieldRef()branches.npm run test:headless5662/5662,ng lintclean,check-circ-depsclean.Problems
Two things worth one minute against the live REST payload of the DH Data form, because they change what a reviewer
should expect:
dc.typefield at all? If not, the default type bind model is never registered and everyordinary type-bound field in it falls into the "no controlling model → stay hidden" branch. That is correct by
config, not a regression.
edm.typefield itself carry a<type-bind>? If it did, the self-dependency guard would throw(
FormControl edm_type cannot depend on itself) — it did on 7.x too, but with dotted ids the comparison had goneinert, so this PR restores the check.
Remaining known hole (narrow): if the controlling model is registered only after the bound field's container has
already attached to something, the field re-attaches via
getTypeBindModelUpdates(); but a form section that isdestroyed and rebuilt between those two events re-runs the whole attach anyway, so the only uncovered case is a
controlling model that is never registered at all — which correctly leaves the field hidden.
Out of scope: the dead
submission.typeBind.fieldkey insrc/config/default-app-config.ts(zero consumers on v9,superseded by the REST-exposed
submit.type-bind.field) — worth a separate cleanup PR.Manual Testing (if applicable)
edm.type = TEXT→ "Pick the languages of theTEXT" becomes visible; pick another type → it hides again.
dc.typestill shows/hides correctly (no regression on thedefault path).
Copilot review