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>
≥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 — 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> —<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 ≥1 array item.
</li>
<li>
Drupal Field API's <code>setRequired(TRUE)</code> means "≥1 value" — 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> → Drupal renders<br>
<code>.form-required</code> CSS class → JS <code>isRemoveButtonEnabled()</code> checks<br>
<code>.form-required</code> + <code>rowCount === 1</code> → 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 > 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 — 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> — 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 > 1.
</li>
<li>
<code>src/ShapeMatcher/EntityFieldPropSourceMatcher.php</code> — 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 — 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> — toggling "Required" on an array prop is enough.
</p>
<ul>
<li>
<code>ui/src/types/CodeComponent.ts</code> — 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> — <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> — <code>serializeProps</code> outputs<br>
<code>minItems</code> and <code>deserializeProps</code> reads it back.
</li>
</ul>
<h4>3 — 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 — 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> — a Canvas-eligible<br>
sparkline with explicit <code>minItems: 1</code>. This forms a three-component test set:
</p>
<ul>
<li><code>sparkline</code> — <code>required: [data]</code>, no <code>minItems</code>; <code>[]</code> is schema-valid (Canvas-eligible)</li>
<li><code>sparkline_min_1</code> — <code>required: [data]</code> + <code>minItems: 1</code>; <code>[]</code> fails validation (Canvas-eligible)</li>
<li><code>sparkline_min_2</code> — <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 ≥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 ≥1 items in Canvas. This respects JSON Schema semantics and keeps the SDC valid across<br>
non-Canvas contexts. To enforce ≥1 value, add <code>minItems: 1</code> explicitly.<br>
<code>minItems &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> — 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 — required array props without<br>
<code>minItems: 1</code> no longer show the required asterisk or block removing the last item —<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