Problem Summary

This issue addresses a semantic mismatch in how Canvas treats "required" for
type: array (multi-cardinality) props. Canvas was incorrectly treating
required: [prop] as equivalent to minItems: 1, which is wrong per JSON Schema.
This caused three separate problems:

  1. UI over-enforced: The "cannot remove last item" behavior applied to ALL required array
    props, even those where value: [] is perfectly schema-valid.
  2. API under-enforced: Programmatic saving accepted value: [] for required
    array props (correct per spec, but inconsistent with UI behavior). See
    MR !923 note_749762.
  3. minItems: 1 was blocked entirely: Canvas rejected all SDC and code
    component props that included minItems, making it impossible to explicitly opt into
    ≥1 enforcement.

Overview

Related: #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 and #3537154: Regression in #3529788: can publish component instances with empty required props

#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 introduced graceful degradation for required string props (retaining empty string during
editing) with a @todo Expand to support multiple-cardinality. #3537154: Regression in #3529788: can publish component instances with empty required props fixed a
regression and added a second such @todo. Both of those @todo items have
since been addressed and removed from the codebase, making this issue the remaining piece of the
multi-cardinality required-prop story.

Root Cause

The false assumption was encoded directly in a code comment in JsonSchemaType.php:

// marking an SDC prop as required has the same effect as `minItems: 1`

This incorrect belief led Canvas to:

  • Block all minItems values in both JsonSchemaType.php and
    EntityFieldPropSourceMatcher.php (treating it as redundant with required).
  • Call setRequired(TRUE) on the Drupal field for ALL required array props — causing
    the form to show the required asterisk and prevent removing the last item, even when the JSON Schema
    did not require it.

This assumption is false. Proven by tests in
MR !923 note_749761:
with required: ['data'] but no minItems, storing value: []
produces zero violations from validateComponentInput().

Key Technical Findings

  • required: [prop] in JSON Schema means "the key must be present in the object"
    it does NOT mean the array must be non-empty. value: [] is perfectly schema-valid.
  • minItems: 1 is the correct JSON Schema mechanism to require ≥1 array item.
  • Drupal Field API's setRequired(TRUE) means "≥1 value" — which maps to
    minItems: 1 semantics, not bare required.
  • The UI enforcement chain: PHP setRequired(TRUE) → Drupal renders
    .form-required CSS class → JS isRemoveButtonEnabled() checks
    .form-required + rowCount === 1 → remove button disabled.

Behavioral Changes

Scenario Before fix After fix
SDC with required: [prop], no minItems, value: [] UI blocks removing last item (incorrect) Canvas-ineligible
SDC with required: [prop] + minItems: 1, value: [] Canvas-ineligible (all minItems blocked) Canvas-eligible; [] correctly fails validation
SDC with minItems: 2 Canvas-ineligible Still ineligible (Drupal Field API cannot enforce minimum cardinality > 1)
Code component editor: toggle Required ON for array prop No minItems serialized minItems: 1 auto-serialized

Proposed resolution

1 — Allow minItems: 1 in prop definitions (PHP)

Two files previously blocked ALL minItems values:

  • src/JsonSchemaInterpreter/JsonSchemaType.php — now allows minItems
    when value is exactly 1; rejects all other values. Drupal Field API has no concept
    of minimum cardinality > 1.
  • src/ShapeMatcher/EntityFieldPropSourceMatcher.php — same change.

Also removed the false code comment claiming required and minItems: 1 are equivalent.

2 — Code component editor auto-sets minItems: 1 (TypeScript)

Component authors using the Canvas code editor should not need to know about
minItems: 1 — toggling "Required" on an array prop is enough.

  • ui/src/types/CodeComponent.ts — added minItems?: number to both
    CodeComponentProp and CodeComponentPropSerialized.
  • ui/src/features/code-editor/codeEditorSlice.tstoggleRequired now
    auto-sets minItems: 1 when enabling required on an array prop, and clears it when
    disabling.
  • ui/src/features/code-editor/utils/utils.tsserializeProps outputs
    minItems and deserializeProps reads it back.

3 — Fix UI enforcement (PHP)

src/Plugin/Canvas/ComponentSource/GeneratedFieldExplicitInputUxComponentSourceBase.php:
setRequired(TRUE) is now only called for array props when BOTH required
AND minItems: 1 are present. For array props with required but no
minItems: 1, setRequired(FALSE) is used — no required asterisk, user
CAN remove all items.

No JavaScript changes were needed. isRemoveButtonEnabled() already correctly read
the .form-required CSS class; fixing the PHP source of that class was sufficient.

New test SDC: sparkline_min_1

tests/modules/canvas_test_sdc/components/sparkline_min_1/ — a Canvas-eligible
sparkline with explicit minItems: 1. This forms a three-component test set:

  • sparklinerequired: [data], no minItems; [] is schema-valid (Canvas-eligible)
  • sparkline_min_1required: [data] + minItems: 1; [] fails validation (Canvas-eligible)
  • sparkline_min_2minItems: 2; Canvas-ineligible

New/updated tests in SingleDirectoryComponentTest:

  • testValidateComponentInputRejectsEmptyRequiredMultiCardinalityPropWithMinItems1:
    verifies value: [] produces ≥1 violations, and value: [42] produces 0.
  • testValidateComponentInputAllowsEmptyRequiredMultiCardinalityPropWithNoMinItems:
    clarified that 0 violations for [] on a bare-required prop IS correct post-fix behavior,
    not a bug.

Developer experience note

For SDC authors: required: [prop] alone intentionally does NOT
enforce ≥1 items in Canvas. This respects JSON Schema semantics and keeps the SDC valid across
non-Canvas contexts. To enforce ≥1 value, add minItems: 1 explicitly.
minItems > 1 remains unsupported (Drupal Field API cannot enforce it).

For code component authors: Toggling "Required" in the Canvas editor for an
array prop automatically handles minItems: 1 — no manual YAML editing needed.

User interface changes

No new UI visible to content authors. The behavior change — required array props without
minItems: 1 no longer show the required asterisk or block removing the last item —
is a bug fix aligning the UI with JSON Schema semantics, not a new UI feature.

Issue fork canvas-3516754

Command icon Show commands

Start within a Git clone of the project using the version control instructions.

Or, if you do not have SSH keys set up on git.drupalcode.org:

Comments

larowlan created an issue. See original summary.

wim leers’s picture

Title: [PP-2] Review how we ascertain a non-scalar component source value is empty » [PP-2] Review how we ascertain a non-scalar SDC/JS prop value is empty
Issue summary: View changes

I didn't grok this issue summary at first, but by reading https://git.drupalcode.org/project/experience_builder/-/merge_requests/8... too, I did grok it. Added some clarifications.

Related original MR thread: https://git.drupalcode.org/project/experience_builder/-/merge_requests/8...

wim leers’s picture

Title: [PP-2] Review how we ascertain a non-scalar SDC/JS prop value is empty » Review how we ascertain a non-scalar SDC/JS prop value is empty

#3467870: Support `{type: array, …}` prop shapes landed. But, it's impossible right now to close this issue, because per #3467870-28: Support `{type: array, …}` prop shapes, selecting multiple values doesn't quite work yet.

Project: Experience Builder » Drupal Canvas

Experience Builder has been renamed to Drupal Canvas in preparation for its beta release. You can now track issues on the new project page.

tedbow’s picture

This could be worked on now correct?

#3546869: Editing support for `type: array`: support multiple-cardinality Field Widgets (`StaticPropSource`s) stops the editing but we could still write back tests to confirm whatever approach we decide on works

vipin.mittal18’s picture

Issue summary: View changes
narendrar’s picture

narendrar’s picture

vipin.mittal18’s picture

Hello @larowlan, It seems this issue is outdated.

wim leers’s picture

It's still needed.

#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 introduced

        // Required string component props that evaluate to '' must be retained:
        // while the empty string is NOT considered a valid value, this is the
        // fallback behavior XB opts for to enhance the user experience: it
        // allows a component to render even at the point in time where a
        // Content Author has *emptied* the string input, as they're thinking
        // about what string they do want.
        // ⚠️ This won't work for components whose logic specifically checks for
        // an empty string and refuses to render then.
        // @todo Expand to support multiple-cardinality.
        if ($required && $evaluated === '' && $this->getExplicitInputDefinitions()['shapes'][$prop]['type'] === JsonSchemaType::STRING->value) {
          // Confirm that *if* this weren't special-cased, that this would
          // indeed enter the next branch, which would cause it to be skipped.
          // @todo Consider adding a new `GracefulDegradationPropSource` to
          // encapsulate this similarly to `DefaultRelativeUrlPropSource`.
          assert(!$source instanceof StaticPropSource || ($source->fieldItemList->count() > 0 && $source->fieldItemList->isEmpty()));
        }

It also introduced a regression, which was fixed in #3537154: Regression in #3529788: can publish component instances with empty required props, and then adde3d another @todo:

        try {
          \assert(\array_key_exists('expression', $raw_prop_source) && \array_key_exists('value', $raw_prop_source) && \array_key_exists('sourceType', $raw_prop_source));
          StaticPropSource::isMinimalRepresentation($raw_prop_source);
        }
        catch (\LengthException $e) {
          // During previews, empty values are intentionally allowed. Those must
          // be filtered away when validating, which then in turn MAY trigger an
          // error from ComponentValidator::validateProps() — if this is for a
          // required prop.
          // In other words: let a prop source being emptier than it portrays
          // result in the appropriate validation errors at the component level.
          // @see \Drupal\canvas\PropSource\StaticPropSource::withValue(allow_empty: TRUE)
          // @todo Expand to support multiple-cardinality.
          unset($inputValues[$component_prop_name]);
          continue;
        }

Both of those need to be fixed here.

wim leers’s picture

Discussed MR713 with @tedbow. Approved his test additions on the MR and provided a pointer.

However, that actually is only tangentially related to the original intent/scope of this issue.

  • MR713 is about an edge case for invalid example values in an SDC or code component. ← this is a data integrity concern, and is AFAICT basically an edge case we missed in #3568264: Update code component props schema to support 'array' type
  • The original intent/scope: correctly handle "allowed to be empty during preview" for multiple-cardinality (type: array) props. See IS + #11.
tedbow’s picture

FYI, I have pushed up https://git.drupalcode.org/issue/canvas-3516754/-/tree/3516754-test-module which contains a test module that allows editing auto-save directly. using it for debugging locally I think because other multi-value issues are still in motion and I tried emptying out an array field from the UI and was not able to.

tedbow’s picture

Since #11 the code @wim-leers referenced has changed in #3546869: Editing support for `type: array`: support multiple-cardinality Field Widgets (`StaticPropSource`s) again and

// @todo Expand to support multiple-cardinality.

Was removed.

Now the code is

try {
          \assert(\array_key_exists('expression', $raw_prop_source) && \array_key_exists('value', $raw_prop_source) && \array_key_exists('sourceType', $raw_prop_source));
          StaticPropSource::isMinimalRepresentation($raw_prop_source);
        }
        catch (\LengthException $e) {
          // During previews, empty values are intentionally allowed. Those must
          // be filtered away when validating, which then in turn MAY trigger an
          // error from ComponentValidator::validateProps() — if this is for a
          // required prop.
          // In other words: let a prop source being emptier than it portrays
          // result in the appropriate validation errors at the component level.
          // @see \Drupal\canvas\PropSource\StaticPropSource::withValue(allow_empty: TRUE)
          // If a StaticPropSource's field item list is empty, consider it not
          // set at all.
          if ($source->isEmpty()) {
            unset($inputValues[$component_prop_name]);
            continue;
          }
        }

Also in that issue \Drupal\canvas\PropSource\StaticPropSource::isEmpty was also added

/**
   * Determines if this prop source is empty.
   *
   * Uses the field type's own isEmpty() logic (via filterEmptyItems()) to
   * determine which items to drop, rather than a PHP-level falsy check. This
   * correctly handles all field types, including those where a value of 0,
   * FALSE, or '' is valid and must be retained.
   *
   * Intended for multiple-cardinality prop sources that arrive in a
   * "mid-input" state during preview/auto-save, where the user may have left
   * some entries blank while still filling in others.
   *
   * @return bool
   *   If the prop is empty.
   */
  public function isEmpty(): bool {
    $filtered = clone $this->fieldItemList;
    $filtered->filterEmptyItems();
    return $filtered->isEmpty();
  }

and test coverage was added to \Drupal\Tests\canvas\Kernel\Plugin\Canvas\ComponentSource\SingleDirectoryComponentTest, specifically testValidateComponentInputFiltersEmptyItemsForMultiCardinalityProps and testClientModelToInputRetainsEmptyArrayForRequiredMultiCardinalityProp

I am still looking this to see if the current is still relavent or if need to add more test covergae

tedbow’s picture

So I pushed up another MR. @wim-leers I am not sure if will agree this relates to the scope of this issue but I do think it relates to the title "Review how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty"
but the issue summary says

The goal is to ensure that empty states don't break the live preview or lead to data loss during the authoring experience.

This would imply this issue is only dealing with the authoring experience so I am not sure this mean to cover this but..

Basically I found that programmatically you can save required prop of the type array with an empty array \Drupal\Tests\canvas\Kernel\Plugin\Field\FieldType\ComponentTreeItemListTest::testEntityLevelValidationAllowsEmptyRequiredMultiCardinalityProp
required in json schema terms just means the key is test not that the array can't be empty. You could enforce it with minItems: 1 but we don't allow that. But it seems from our code comment we don't allow minItems: 1 because we assume `required` would enforce the same rule, which is does not.

We do enforce array props that are required in the UI. I have figured this out through manual testing. I think we should enforce the rules on the API level too, which we currently are not. Or least document that we aren't

See my MR comments and code comments in the MR

wim leers’s picture

#17: I think it definitely relates. That's exactly why we have this issue!

tedbow’s picture

tedbow’s picture

Issue summary: View changes
Status: Active » Needs work

summary update to reflect the current state of !923 and the fact that todos mentioned have actually been removed

I pretty sure this will fail tests be will see

tedbow’s picture

Looking into phpunit test failures first

tedbow’s picture

Status: Needs work » Needs review

@isholgueras thanks for the review, I have addressed you feedback

wim leers’s picture

Assigned: Unassigned » tedbow
Issue summary: View changes
Status: Needs review » Needs work
Issue tags: +validation, +Needs issue summary update, +Needs change record

@tedbow asked me to review this.

I had one key concern (see link for details, summarized: What's the point of marking a type: array prop as required if it can be empty? Then it might as well not be required, right?.

Then I saw that this MR is updating the code component editor UI to ensure that whenever a prop is marked as required, minItems: 1 is also set. That's exactly what I proposed!

But I proposed to do this generically: to impose this at the prop-definition-in-JSON-Schema level, i.e. for both code components and SDCs. For code components, this is already making it so. For SDCs, it is not. That's easily solved.

So:

  1. I agree with the analysis in the issue summary+MR
  2. I disagree with the conclusion: to allow both minItems: 1 and no minItems. If a type: array prop is required without minItems: 1, then it should simply be optional!
  3. Code components that have already been created with type: array props need an update path

In my proposal:

Behavioral Changes

Scenario Before fix After fix
SDC with required: [prop], no minItems, value: [] UI blocks removing last item (incorrect) Canvas-ineligible 👈 This is the only change I propose
SDC with required: [prop] + minItems: 1, value: [] ✅ What's in the issue summary/MR is 👍
SDC with minItems: 2 ✅ What's in the issue summary/MR is 👍
Code component editor: toggle Required ON for array prop No minItems serialized minItems: 1 auto-serialized 👈 Unchanged, this makes sense, but it doesn't make sense that SDCs were treated differently ⚠️ However, this AFAICT means this will need an update path for code components that have already been created.

Summarized my changes on the MR: https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_811732

wim leers’s picture

Title: Review how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty » [PP-1] Finalize how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty
Issue tags: +technical debt, +Needs update path, +Needs update path tests

Per https://git.drupalcode.org/project/canvas/-/merge_requests/923#note_811745, #3572553: Save Multi-Value Prop Configuration for Code Components should land first.

My concerns in #23 may seem big, but they're not. They're essentially just: "your analysis is right, your solution is right, but you went too far in supporting what does not make sense; we should instead require the sensible thing you applied to code components to everything".

So: fantastic work here! 🤩👏 It resolves a piece of technical debt that I introduced in #3467870: Support `{type: array, …}` prop shapes (I did not handle the "empty" edge case at all in there).

This MR even significantly improves shape matching against (required) multi-value entity fields, resulting in more structured data becoming available in Canvas! 💙

wim leers’s picture

Pushed several more "update tests" commits. I think they illustrate how the slight shift in #23 significantly improves cohesion and reduces complexity.

The only remaining test failures are those in JavaScriptComponent(Validation)Test + JsComponentTest, which illustrate the need for an update path.

I'll leave that for @tedbow & co to address.

wim leers’s picture

Status: Needs work » Reviewed & tested by the community
Issue tags: -Needs update path, -Needs update path tests

Paired with @tedbow on this issue/MR.

He had an insight I was missing: all of the "multi-value prop" functionality was gated behind the canvas_dev_mode module. Hence no code components could've been created that would need an update path!

(The only exceptions could be: code components could've used type: array since #3568264, which shipped in https://www.drupal.org/project/canvas/releases/1.3.0 on March 21. We'll work with @balintbrews to figure out if somebody could've conceivably already used it, and if so, we'll add an update path in a subsequent MR. To be clear: nobody could have created this through the Canvas UI! That's why #3577946: [PP-2] Remove Canvas Dev Mode flag for Multi-Value Props support exists: to actually enable this for everyone. And that's why this issue blocks that one.)

wim leers’s picture

  • tedbow committed 299b1d42 on 1.x
    feat: #3516754 Require `minItems: 1` for required `type: array` props...
tedbow’s picture

Assigned: tedbow » Unassigned
Status: Reviewed & tested by the community » Fixed

Thanks, every one!

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

nagwani’s picture

Title: [PP-1] Finalize how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty » Finalize how we ascertain a `type: array` (aka multiple-cardinality) SDC/JS prop value is empty