Finalize how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty
>>> [!note] Migrated issue <!-- Drupal.org comment --> <!-- Migrated from issue #3516754. --> Reported by: [larowlan](https://www.drupal.org/user/395439) Related to !923 !713 >>> <h3 id="problem-summary">Problem Summary</h3> <p> This issue addresses a <strong>semantic mismatch</strong> in how Canvas treats "required" for<br> <code>type: array</code> (multi-cardinality) props. Canvas was incorrectly treating<br> <code>required: [prop]</code> as equivalent to <code>minItems: 1</code>, which is wrong per JSON Schema.<br> This caused three separate problems: </p> <ol> <li> <strong>UI over-enforced</strong>: The "cannot remove last item" behavior applied to ALL required array<br> props, even those where <code>value: []</code> is perfectly schema-valid. </li> <li> <strong>API under-enforced</strong>: Programmatic saving accepted <code>value: []</code> for required<br> array props (correct per spec, but inconsistent with UI behavior). See<br> <a href="https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_749762">MR !923 note_749762</a>. </li> <li> <strong><code>minItems: 1</code> was blocked entirely</strong>: Canvas rejected all SDC and code<br> component props that included <code>minItems</code>, making it impossible to explicitly opt into<br> &ge;1 enforcement. </li> </ol> <h3 id="overview">Overview</h3> <h4>Related: <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/experience_builder/issues/3529788" title="Status: Closed (fixed)">#3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases</a></span> and <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/experience_builder/issues/3537154" title="Status: Closed (fixed)">#3537154: Regression in #3529788: can publish component instances with empty required props</a></span></h4> <p> <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/experience_builder/issues/3529788" title="Status: Closed (fixed)">#3529788: Required `type: string` (without other constraints, so "prose strings") props should fall back to empty string (even though invalid) while Content Author is typing, to allow matching live preview in most cases</a></span> introduced graceful degradation for required string props (retaining empty string during<br> editing) with a <code>@todo Expand to support multiple-cardinality</code>. <span class="drupalorg-gitlab-issue-link project-issue-status-info project-issue-status-7"><a href="https://www.drupal.org/project/experience_builder/issues/3537154" title="Status: Closed (fixed)">#3537154: Regression in #3529788: can publish component instances with empty required props</a></span> fixed a<br> regression and added a second such <code>@todo</code>. Both of those <code>@todo</code> items have<br> since been addressed and removed from the codebase, making this issue the remaining piece of the<br> multi-cardinality required-prop story. </p> <h4>Root Cause</h4> <p>The false assumption was encoded directly in a code comment in <code>JsonSchemaType.php</code>:</p> <blockquote><p> <code>// marking an SDC prop as required has the same effect as `minItems: 1`</code> </p></blockquote> <p>This incorrect belief led Canvas to:</p> <ul> <li> Block <em>all</em> <code>minItems</code> values in both <code>JsonSchemaType.php</code> and<br> <code>EntityFieldPropSourceMatcher.php</code> (treating it as redundant with <code>required</code>). </li> <li> Call <code>setRequired(TRUE)</code> on the Drupal field for ALL required array props &mdash; causing<br> the form to show the required asterisk and prevent removing the last item, even when the JSON Schema<br> did not require it. </li> </ul> <p> <strong>This assumption is false.</strong> Proven by tests in<br> <a href="https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_749761">MR !923 note_749761</a>:<br> with <code>required: ['data']</code> but no <code>minItems</code>, storing <code>value: []</code><br> produces <strong>zero violations</strong> from <code>validateComponentInput()</code>. </p> <h4>Key Technical Findings</h4> <ul> <li> <code>required: [prop]</code> in JSON Schema means <em>"the key must be present in the object"</em> &mdash;<br> it does NOT mean the array must be non-empty. <code>value: []</code> is perfectly schema-valid. </li> <li> <code>minItems: 1</code> is the correct JSON Schema mechanism to require &ge;1 array item. </li> <li> Drupal Field API's <code>setRequired(TRUE)</code> means "&ge;1 value" &mdash; which maps to<br> <code>minItems: 1</code> semantics, <strong>not</strong> bare <code>required</code>. </li> <li> The UI enforcement chain: PHP <code>setRequired(TRUE)</code> &rarr; Drupal renders<br> <code>.form-required</code> CSS class &rarr; JS <code>isRemoveButtonEnabled()</code> checks<br> <code>.form-required</code> + <code>rowCount === 1</code> &rarr; remove button disabled. </li> </ul> <h4>Behavioral Changes</h4> <table> <thead> <tr> <th>Scenario</th> <th>Before fix</th> <th>After fix</th> </tr> </thead> <tbody> <tr> <td>SDC with <code>required: [prop]</code>, no <code>minItems</code>, <code>value: []</code></td> <td>UI blocks removing last item (incorrect)</td> <td>Canvas-ineligible</td> </tr> <tr> <td>SDC with <code>required: [prop]</code> + <code>minItems: 1</code>, <code>value: []</code></td> <td>Canvas-ineligible (all <code>minItems</code> blocked)</td> <td>Canvas-eligible; <code>[]</code> correctly fails validation</td> </tr> <tr> <td>SDC with <code>minItems: 2</code></td> <td>Canvas-ineligible</td> <td>Still ineligible (Drupal Field API cannot enforce minimum cardinality &gt; 1)</td> </tr> <tr> <td>Code component editor: toggle Required ON for array prop</td> <td>No <code>minItems</code> serialized</td> <td><code>minItems: 1</code> auto-serialized</td> </tr> </tbody> </table> <h3 id="proposed-resolution">Proposed resolution</h3> <h4>1 &mdash; Allow <code>minItems: 1</code> in prop definitions (PHP)</h4> <p>Two files previously blocked ALL <code>minItems</code> values:</p> <ul> <li> <code>src/JsonSchemaInterpreter/JsonSchemaType.php</code> &mdash; now allows <code>minItems</code><br> when value is exactly <code>1</code>; rejects all other values. Drupal Field API has no concept<br> of minimum cardinality &gt; 1. </li> <li> <code>src/ShapeMatcher/EntityFieldPropSourceMatcher.php</code> &mdash; same change. </li> </ul> <p>Also removed the false code comment claiming <code>required</code> and <code>minItems: 1</code> are equivalent.</p> <h4>2 &mdash; Code component editor auto-sets <code>minItems: 1</code> (TypeScript)</h4> <p> Component authors using the Canvas code editor should not need to know about<br> <code>minItems: 1</code> &mdash; toggling "Required" on an array prop is enough. </p> <ul> <li> <code>ui/src/types/CodeComponent.ts</code> &mdash; added <code>minItems?: number</code> to both<br> <code>CodeComponentProp</code> and <code>CodeComponentPropSerialized</code>. </li> <li> <code>ui/src/features/code-editor/codeEditorSlice.ts</code> &mdash; <code>toggleRequired</code> now<br> auto-sets <code>minItems: 1</code> when enabling required on an array prop, and clears it when<br> disabling. </li> <li> <code>ui/src/features/code-editor/utils/utils.ts</code> &mdash; <code>serializeProps</code> outputs<br> <code>minItems</code> and <code>deserializeProps</code> reads it back. </li> </ul> <h4>3 &mdash; Fix UI enforcement (PHP)</h4> <p> <code>src/Plugin/Canvas/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php</code>:<br> <code>setRequired(TRUE)</code> is now only called for array props when BOTH <code>required</code><br> AND <code>minItems: 1</code> are present. For array props with <code>required</code> but no<br> <code>minItems: 1</code>, <code>setRequired(FALSE)</code> is used &mdash; no required asterisk, user<br> CAN remove all items. </p> <p> No JavaScript changes were needed. <code>isRemoveButtonEnabled()</code> already correctly read<br> the <code>.form-required</code> CSS class; fixing the PHP source of that class was sufficient. </p> <h4>New test SDC: <code>sparkline_min_1</code></h4> <p> <code>tests/modules/canvas_test_sdc/components/sparkline_min_1/</code> &mdash; a Canvas-eligible<br> sparkline with explicit <code>minItems: 1</code>. This forms a three-component test set: </p> <ul> <li><code>sparkline</code> &mdash; <code>required: [data]</code>, no <code>minItems</code>; <code>[]</code> is schema-valid (Canvas-eligible)</li> <li><code>sparkline_min_1</code> &mdash; <code>required: [data]</code> + <code>minItems: 1</code>; <code>[]</code> fails validation (Canvas-eligible)</li> <li><code>sparkline_min_2</code> &mdash; <code>minItems: 2</code>; Canvas-ineligible</li> </ul> <p>New/updated tests in <code>SingleDirectoryComponentTest</code>:</p> <ul> <li> <code>testValidateComponentInputRejectsEmptyRequiredMultiCardinalityPropWithMinItems1</code>:<br> verifies <code>value: []</code> produces &ge;1 violations, and <code>value: [42]</code> produces 0. </li> <li> <code>testValidateComponentInputAllowsEmptyRequiredMultiCardinalityPropWithNoMinItems</code>:<br> clarified that 0 violations for <code>[]</code> on a bare-required prop IS correct post-fix behavior,<br> not a bug. </li> </ul> <h4>Developer experience note</h4> <p> <strong>For SDC authors:</strong> <code>required: [prop]</code> alone intentionally does NOT<br> enforce &ge;1 items in Canvas. This respects JSON Schema semantics and keeps the SDC valid across<br> non-Canvas contexts. To enforce &ge;1 value, add <code>minItems: 1</code> explicitly.<br> <code>minItems &amp;gt; 1</code> remains unsupported (Drupal Field API cannot enforce it). </p> <p> <strong>For code component authors:</strong> Toggling "Required" in the Canvas editor for an<br> array prop automatically handles <code>minItems: 1</code> &mdash; no manual YAML editing needed. </p> <h3 id="ui-changes">User interface changes</h3> <p> No new UI visible to content authors. The behavior change &mdash; required array props without<br> <code>minItems: 1</code> no longer show the required asterisk or block removing the last item &mdash;<br> is a bug fix aligning the UI with JSON Schema semantics, not a new UI feature. </p> > Related issue: [Issue #3467870](https://www.drupal.org/node/3467870) > Related issue: [Issue #3493943](https://www.drupal.org/node/3493943) > Related issue: [Issue #3529788](https://www.drupal.org/node/3529788) > Related issue: [Issue #3537945](https://www.drupal.org/node/3537945) > Related issue: [Issue #3546869](https://www.drupal.org/node/3546869) > Related issue: [Issue #3537154](https://www.drupal.org/node/3537154) > Related issue: [Issue #3577946](https://www.drupal.org/node/3577946)
issue