Skip to content

fix(runtime-dom): preserve nullish values for custom element properties#14459

Open
apoorvdarshan wants to merge 1 commit intovuejs:mainfrom
apoorvdarshan:fix/custom-element-nullish-prop-coercion
Open

fix(runtime-dom): preserve nullish values for custom element properties#14459
apoorvdarshan wants to merge 1 commit intovuejs:mainfrom
apoorvdarshan:fix/custom-element-nullish-prop-coercion

Conversation

@apoorvdarshan
Copy link

@apoorvdarshan apoorvdarshan commented Feb 18, 2026

Summary

Fixes #14209

Custom element properties may accept multiple types, so when a nullish value (null/undefined) or empty string is passed, Vue should not coerce it based on the existing property type.

Previously in patchDOMProp, the nullish/empty value coercion logic (lines 58-71) would check typeof el[key] and coerce accordingly — even for custom elements:

  • null0 for number properties
  • null'' for string properties
  • ''true for boolean properties

This is correct for native HTML elements (which have fixed property types), but wrong for custom elements whose properties can intentionally accept null/undefined or change types.

The fix skips this type-based coercion for custom elements (tag.includes('-')) and preserves the value as-is, while still removing the attribute when the value is nullish.

Test plan

  • Updated existing "value for custom elements" test to expect null instead of coerced ''
  • Added new test covering all three coercion cases: number, boolean, and string props on custom elements
  • All 220 runtime-dom tests pass

Summary by CodeRabbit

  • Bug Fixes

    • Custom elements now preserve null and other nullish property values as-is instead of coercing them to default primitives; standard HTML elements retain prior coercion behavior.
  • Tests

    • Added tests confirming nullish handling and attribute reflection for custom element properties, including empty-string and null preservation.

@coderabbitai
Copy link

coderabbitai bot commented Feb 18, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f682338 and e682c35.

📒 Files selected for processing (2)
  • packages/runtime-dom/__tests__/patchProps.spec.ts
  • packages/runtime-dom/src/modules/props.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-dom/src/modules/props.ts

📝 Walkthrough

Walkthrough

This change preserves nullish values (null/undefined) when writing properties to custom elements in patchDOMProp; custom element properties are no longer coerced to default primitives (e.g., '' or 0) while standard elements retain prior coercion behavior.

Changes

Cohort / File(s) Summary
Tests
packages/runtime-dom/__tests__/patchProps.spec.ts
Adds tests verifying null/undefined preservation for custom element properties (number, boolean, string) and updates expectation for value to assert null instead of ''.
Runtime DOM props logic
packages/runtime-dom/src/modules/props.ts
Modifies patchDOMProp to detect custom elements (tag includes '-') and avoid coercing empty-string / nullish values for their properties; sets removal behavior (needRemove) based on nullishness while leaving standard element coercion unchanged.

Sequence Diagram(s)

sequenceDiagram
    participant Patch as PatchDOMProp
    participant Elm as DOMElement
    participant CE as CustomElement
    Note over Patch,Elm: PatchDOMProp called with (el, key, value)
    Patch->>Elm: inspect tagName / isCustom (contains '-')
    alt is custom element
        Patch->>CE: set property directly (value can be null/undefined/'')
        CE-->>Patch: property assigned (no coercion)
        Patch->>Elm: if value == null then remove attribute else keep
    else standard element
        Patch->>Elm: coerce value based on prop type (bool/string/number)
        Elm-->>Patch: property/attribute updated (coerced value)
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested labels

ready to merge

Poem

🐰 I hopped through the patch with a flick of my ear,
Nulls kept their shape, no more coercion fear.
Properties honest, no zeros or lies,
Web components smiling with clear little eyes. 🥕

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title directly and accurately describes the main change: preserving nullish values for custom element properties, which is the core fix implemented across both modified files.
Linked Issues check ✅ Passed The PR fully addresses issue #14209 by implementing type-based coercion skipping for custom elements (those with '-' in tag name), preserving null/undefined values as-is, while removing attributes when values are nullish. Tests confirm the fix for number, boolean, and string properties.
Out of Scope Changes check ✅ Passed All changes are directly scoped to the objectives: the runtime-dom props handler modification for custom elements and corresponding test updates. No unrelated changes or scope creep detected.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (2)
packages/runtime-dom/__tests__/patchProps.spec.ts (1)

79-105: Good coverage of all three primitive types and the '' case; minor nit on the getAttribute assertion.

The test correctly exercises null on number, boolean, and string props as well as the ''-on-boolean edge case.

One minor observation: Line 91's expect(el.getAttribute('myNumber')).toBe(null) doesn't actually validate that removeAttribute had any effect, because myNumber was never reflected as an attribute to begin with — the assertion passes trivially before the patchProp call too. To make this test more meaningful, the attribute could be set first:

📝 Strengthened getAttribute assertion
+    el.setAttribute('myNumber', '1')
     // setting null on a number prop should preserve null, not coerce to 0
     patchProp(el, '.myNumber', null, null)
     expect(el.myNumber).toBe(null)
     expect(el.getAttribute('myNumber')).toBe(null)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/runtime-dom/__tests__/patchProps.spec.ts` around lines 79 - 105, The
getAttribute assertion for myNumber is vacuous because the attribute was never
set — before calling patchProp(el, '.myNumber', null, null) set the attribute on
el (e.g., el.setAttribute('myNumber', '1')) so the test verifies that patchProp
removes or clears the attribute; update the setup in the TestElement/test case
to setAttribute on el for 'myNumber' prior to invoking patchProp and then assert
el.getAttribute('myNumber') is null afterwards.
packages/runtime-dom/src/modules/props.ts (1)

59-78: LGTM — logic is correct; minor semantic drift in needRemove worth noting.

The control-flow split is well-placed and consistent with the existing !tag.includes('-') guard on line 31. For the CE path, value is preserved as-is and the attribute is removed only when value == null — which aligns with the PR objective.

One minor nit: the unchanged comment on line 108–109 reads "do not warn if value is auto-coerced from nullish values", but in the CE branch no coercion takes place — needRemove is being reused to mean "this is an intentional nullish assignment" rather than "coercion happened". The behaviour is correct, but a small follow-up comment clarification would keep future readers from being confused.

📝 Suggested comment clarification (line 108)
-  // do not warn if value is auto-coerced from nullish values
+  // do not warn if value is auto-coerced from nullish values,
+  // or if a nullish value is intentionally preserved for custom elements
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/runtime-dom/src/modules/props.ts` around lines 59 - 78, Update the
existing comment that mentions "do not warn if value is auto-coerced from
nullish values" to clarify that in the custom element branch (the guard using
tag.includes('-')) no coercion occurs and that the needRemove flag is being
reused to indicate an intentional nullish assignment rather than a coercion;
keep the logic and the tag.includes('-') branch untouched and only change the
comment near the code that sets needRemove for custom elements so future readers
understand the different semantics.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/runtime-dom/__tests__/patchProps.spec.ts`:
- Around line 79-105: The getAttribute assertion for myNumber is vacuous because
the attribute was never set — before calling patchProp(el, '.myNumber', null,
null) set the attribute on el (e.g., el.setAttribute('myNumber', '1')) so the
test verifies that patchProp removes or clears the attribute; update the setup
in the TestElement/test case to setAttribute on el for 'myNumber' prior to
invoking patchProp and then assert el.getAttribute('myNumber') is null
afterwards.

In `@packages/runtime-dom/src/modules/props.ts`:
- Around line 59-78: Update the existing comment that mentions "do not warn if
value is auto-coerced from nullish values" to clarify that in the custom element
branch (the guard using tag.includes('-')) no coercion occurs and that the
needRemove flag is being reused to indicate an intentional nullish assignment
rather than a coercion; keep the logic and the tag.includes('-') branch
untouched and only change the comment near the code that sets needRemove for
custom elements so future readers understand the different semantics.

@github-actions
Copy link

Size Report

Bundles

File Size Gzip Brotli
runtime-dom.global.prod.js 104 kB (+33 B) 39.3 kB (+9 B) 35.4 kB (+7 B)
vue.global.prod.js 162 kB (+33 B) 59.3 kB (+7 B) 52.8 kB (+37 B)

Usages

Name Size Gzip Brotli
createApp (CAPI only) 47.9 kB (+33 B) 18.6 kB (+8 B) 17.1 kB (+4 B)
createApp 56 kB (+33 B) 21.7 kB (+6 B) 19.8 kB (+6 B)
createSSRApp 60.3 kB (+33 B) 23.4 kB (+7 B) 21.4 kB (+8 B)
defineCustomElement 61.6 kB (+33 B) 23.4 kB (+6 B) 21.4 kB (+7 B)
overall 70.4 kB (+33 B) 27 kB (+8 B) 24.6 kB (+66 B)

@pkg-pr-new
Copy link

pkg-pr-new bot commented Feb 24, 2026

Open in StackBlitz

@vue/compiler-core

pnpm add https://pkg.pr.new/@vue/compiler-core@14459
npm i https://pkg.pr.new/@vue/compiler-core@14459
yarn add https://pkg.pr.new/@vue/compiler-core@14459.tgz

@vue/compiler-dom

pnpm add https://pkg.pr.new/@vue/compiler-dom@14459
npm i https://pkg.pr.new/@vue/compiler-dom@14459
yarn add https://pkg.pr.new/@vue/compiler-dom@14459.tgz

@vue/compiler-sfc

pnpm add https://pkg.pr.new/@vue/compiler-sfc@14459
npm i https://pkg.pr.new/@vue/compiler-sfc@14459
yarn add https://pkg.pr.new/@vue/compiler-sfc@14459.tgz

@vue/compiler-ssr

pnpm add https://pkg.pr.new/@vue/compiler-ssr@14459
npm i https://pkg.pr.new/@vue/compiler-ssr@14459
yarn add https://pkg.pr.new/@vue/compiler-ssr@14459.tgz

@vue/reactivity

pnpm add https://pkg.pr.new/@vue/reactivity@14459
npm i https://pkg.pr.new/@vue/reactivity@14459
yarn add https://pkg.pr.new/@vue/reactivity@14459.tgz

@vue/runtime-core

pnpm add https://pkg.pr.new/@vue/runtime-core@14459
npm i https://pkg.pr.new/@vue/runtime-core@14459
yarn add https://pkg.pr.new/@vue/runtime-core@14459.tgz

@vue/runtime-dom

pnpm add https://pkg.pr.new/@vue/runtime-dom@14459
npm i https://pkg.pr.new/@vue/runtime-dom@14459
yarn add https://pkg.pr.new/@vue/runtime-dom@14459.tgz

@vue/server-renderer

pnpm add https://pkg.pr.new/@vue/server-renderer@14459
npm i https://pkg.pr.new/@vue/server-renderer@14459
yarn add https://pkg.pr.new/@vue/server-renderer@14459.tgz

@vue/shared

pnpm add https://pkg.pr.new/@vue/shared@14459
npm i https://pkg.pr.new/@vue/shared@14459
yarn add https://pkg.pr.new/@vue/shared@14459.tgz

vue

pnpm add https://pkg.pr.new/vue@14459
npm i https://pkg.pr.new/vue@14459
yarn add https://pkg.pr.new/vue@14459.tgz

@vue/compat

pnpm add https://pkg.pr.new/@vue/compat@14459
npm i https://pkg.pr.new/@vue/compat@14459
yarn add https://pkg.pr.new/@vue/compat@14459.tgz

commit: 487a8df

@edison1105 edison1105 added 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. scope: custom elements breaking change labels Feb 24, 2026
@edison1105
Copy link
Member

/ecosystem-ci run

@vuejs vuejs deleted a comment from edison1105 Feb 28, 2026
@vue-bot
Copy link
Contributor

vue-bot commented Feb 28, 2026

📝 Ran ecosystem CI: Open

suite result latest scheduled
primevue success success
language-tools success success
quasar success success
radix-vue success success
vite-plugin-vue success success
vue-simple-compiler success success
vueuse success success
router success success
vitepress success success
vue-macros success success
vant success success
nuxt success success
vue-i18n success success
pinia success success
test-utils success success
vuetify success success

@apoorvdarshan apoorvdarshan force-pushed the fix/custom-element-nullish-prop-coercion branch from 487a8df to f682338 Compare February 28, 2026 10:21
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/runtime-dom/__tests__/patchProps.spec.ts`:
- Around line 79-105: The test only asserts null handling but should also assert
undefined handling for full "nullish" coverage: in the 'nullish value handling
for custom element props' test (class TestElement and calls to patchProp), add
additional calls that set each prop to undefined (patchProp(el, '.myNumber',
null, undefined), patchProp(el, '.myBool', null, undefined), patchProp(el,
'.myString', null, undefined)) and assert that el.myNumber, el.myBool, and
el.myString are undefined (and that attributes are removed or remain
null/undefined as appropriate, e.g.,
expect(el.getAttribute('myNumber')).toBe(null) or equivalent) to mirror the
existing null assertions.

ℹ️ Review info

Configuration used: defaults

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 487a8df and f682338.

📒 Files selected for processing (2)
  • packages/runtime-dom/__tests__/patchProps.spec.ts
  • packages/runtime-dom/src/modules/props.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/runtime-dom/src/modules/props.ts

Custom element properties may accept multiple types, so when a nullish
value (null/undefined) or empty string is passed, Vue should not coerce
it based on the existing property type. Previously, null would be
coerced to 0 for number props, '' for string props, and '' would be
coerced to true for boolean props on custom elements.

close vuejs#14209
@apoorvdarshan apoorvdarshan force-pushed the fix/custom-element-nullish-prop-coercion branch from f682338 to e682c35 Compare February 28, 2026 10:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change 🔨 p3-minor-bug Priority 3: this fixes a bug, but is an edge case that only affects very specific usage. scope: custom elements

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Invalid nullish value handling in patchDOMProp for custom elements.

3 participants