@meshmakers/octo-ui 3.5.420 → 3.5.440

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -219,6 +219,48 @@ providers: [{ provide: ENTITY_FORM_FALLBACK_FORMS, useValue: MY_FALLBACK_FORMS }
219
219
  This is the per-type counterpart of the built-in `form-default` safety net; the host is
220
220
  responsible for keeping its copies in step with the seed.
221
221
 
222
+ ## Cross-field validation and field marks (AB#7422)
223
+
224
+ Per-field rules (`required`, `min`, `max`, `pattern`) cannot express a rule over several fields such as
225
+ "request must not exceed limit". A host registers a validator for the CK types it knows; the library has
226
+ no domain knowledge:
227
+
228
+ ```ts
229
+ const requestWithinLimit: EntityFormCrossFieldValidator = {
230
+ ckTypeIds: ['System.UI/BaseType'], // concrete type or any ancestor, case-insensitive
231
+ validate: ({ values }) => Number(values['request']) > Number(values['limit'])
232
+ ? { request: 'Request exceeds limit', limit: 'Request exceeds limit' } // field key or attribute name, any casing
233
+ : null,
234
+ };
235
+ // route / root: providers: [provideEntityFormCrossFieldValidators(requestWithinLimit)]
236
+ // or per form: <mm-entity-form [crossFieldValidators]="[requestWithinLimit]" ...>
237
+ ```
238
+
239
+ - `values` holds the current values of the **enabled** fields (hidden by `VisibleWhen`, read-only and `view`
240
+ mode fields are absent), keyed by field key = camelCase attribute name. The validator is re-run on every value
241
+ change and when the form is built, so a stored invalid combination is marked on open.
242
+ - Each named field becomes invalid (`control.errors.crossField`), its message is shown under the field **at once**
243
+ (also on a field the user has not touched, e.g. the partner field), and `isValid()` is `false` until the validator
244
+ answers `null` or `{}`. A host message replaces the generic text of the field's own rules (min, max, pattern, ...) so a validator
245
+ can word the format error itself; `required` still comes first. The first message per field wins when several validators answer.
246
+ - A form matches by `ResolvedEntityForm.rtCkTypeId` and `ckTypeAncestors` (new, from the CK metadata), so a rule on a
247
+ base type also covers derived types and tenant forms of derived types.
248
+ - A throwing validator is logged and ignored; it never blocks the form.
249
+
250
+ **`fieldErrors` input** (`mm-entity-form`): `Record<fieldKeyOrAttributeName, text>` marks a field with a host text, for
251
+ example the field named by the last failed deployment. A mark is informational: it does not affect validity or
252
+ saving and disappears as soon as the user edits that field.
253
+
254
+ **Field hints** (`fieldHints` input, AB#7423): `(context) => Record<fieldKeyOrAttributeName, { text, actionLabel?, actionId? }>`,
255
+ called with the current values (same context as a cross-field validator) on every change. The hint is shown under the field
256
+ (warning colour, role `note`, linked with `aria-describedby`); with `actionLabel` it carries one link that emits
257
+ `fieldHintAction` `{ key, actionId }` — the host decides what it does (e.g. `focusField('valuesYaml')`, which focuses a
258
+ field by key or attribute name and expands its collapsed section). Hints are informational: no validity, no change set.
259
+
260
+ **Accessibility**: the error text (else the help text) of a field is linked to its input with `aria-describedby`
261
+ (`mm-ef-error-<key>` / `mm-ef-help-<key>`), an invalid input gets `aria-invalid="true"`, and `markAllAsTouched()` moves
262
+ the focus to the first invalid field (expanding a collapsed section; `focusFirstInvalid()` does the same on its own).
263
+
222
264
  ## Action confirmation (`ENTITY_FORM_ACTION_CONFIRMATION`)
223
265
 
224
266
  Optional `(request: { action: 'delete', ckTypeId, count, description }) => Promise<boolean>`,