-
-
Notifications
You must be signed in to change notification settings - Fork 8.8k
fix(runtime-dom): allow custom element prop overrides via prototype #13707
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
fix(runtime-dom): allow custom element prop overrides via prototype #13707
Conversation
…uejs#13706) Define custom element properties on prototype instead of instance to allow subclasses to override property setters for validation and custom behavior. Fixes vuejs#13706 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
WalkthroughThe update modifies how property accessors for custom element props are defined, moving their definition from the instance level to the class prototype to allow subclass overrides. Additionally, new tests verify that subclasses can override property setters and control prop assignment behavior, including multiple subclasses and multiple props. Changes
Sequence Diagram(s)sequenceDiagram
participant Test as Test Runner
participant SubclassedElement as SubclassedElement (custom element)
participant Superclass as Base Custom Element
Test->>SubclassedElement: Set value = "valid-date"
SubclassedElement->>Superclass: Call setter with "valid-date"
Superclass-->>SubclassedElement: Set internal value
Test->>SubclassedElement: Set value = "invalid-date"
SubclassedElement->>SubclassedElement: Check value, ignore assignment
Test->>SubclassedElement: Set value = "another-valid"
SubclassedElement->>Superclass: Call setter with "another-valid"
Superclass-->>SubclassedElement: Set internal value
Estimated code review effort🎯 2 (Simple) | ⏱️ ~8 minutes Possibly related PRs
Suggested labels
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: CodeRabbit UI 📒 Files selected for processing (2)
🚧 Files skipped from review as they are similar to previous changes (2)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
CodeRabbit Configuration File (
|
Docs JSDoc: Resolves component props by setting up property getters/setters on the prototype. Unit tests test('properties are defined on prototype not instance', () => {
const E = defineCustomElement({
props: {
testProp: String,
anotherProp: Number,
},
render() {
return h('div', `${this.testProp}-${this.anotherProp}`)
},
})
customElements.define('my-prototype-test', E)
const e1 = new E()
const e2 = new E()
container.appendChild(e1)
container.appendChild(e2)
// Properties should be defined on the prototype, not instances
expect(e1.hasOwnProperty('testProp')).toBe(false)
expect(e1.hasOwnProperty('anotherProp')).toBe(false)
expect(Object.hasOwnProperty.call(E.prototype, 'testProp')).toBe(true)
expect(Object.hasOwnProperty.call(E.prototype, 'anotherProp')).toBe(true)
// Properties should have getter and setter functions
const descriptor = Object.getOwnPropertyDescriptor(E.prototype, 'testProp')
expect(descriptor).toBeDefined()
expect(typeof descriptor!.get).toBe('function')
expect(typeof descriptor!.set).toBe('function')
})
test('multiple subclasses with different override behaviors', async () => {
const E = defineCustomElement({
props: {
value: String,
},
render() {
return h('div', this.value || 'empty')
},
})
class ValidatingSubclass extends E {
set value(val: string) {
// Only allow values that start with 'valid-'
if (val && val.startsWith('valid-')) {
super.value = val
}
}
get value(): string {
return super.value || ''
}
}
class UppercaseSubclass extends E {
set value(val: string) {
// Convert to uppercase
super.value = val ? val.toUpperCase() : val
}
get value(): string {
return super.value || ''
}
}
customElements.define('validating-element', ValidatingSubclass)
customElements.define('uppercase-element', UppercaseSubclass)
const validating = new ValidatingSubclass()
const uppercase = new UppercaseSubclass()
container.appendChild(validating)
container.appendChild(uppercase)
// Test validating subclass
validating.value = 'invalid-test'
await nextTick()
expect(validating.shadowRoot!.innerHTML).toBe('<div>empty</div>')
validating.value = 'valid-test'
await nextTick()
expect(validating.shadowRoot!.innerHTML).toBe('<div>valid-test</div>')
// Test uppercase subclass
uppercase.value = 'hello world'
await nextTick()
expect(uppercase.shadowRoot!.innerHTML).toBe('<div>HELLO WORLD</div>')
})
test('subclass override with multiple props', async () => {
const E = defineCustomElement({
props: {
name: String,
age: Number,
active: Boolean,
},
render() {
return h('div', `${this.name}-${this.age}-${this.active}`)
},
})
class RestrictedSubclass extends E {
set name(val: string) {
// Only allow names with at least 3 characters
if (val && val.length >= 3) {
super.name = val
}
}
get name(): string {
const value = super.name
return value != null ? value : 'default'
}
set age(val: number) {
// Only allow positive ages
if (val && val > 0) {
super.age = val
}
}
get age(): number {
const value = super.age
return value != null ? value : 0
}
}
customElements.define('restricted-element', RestrictedSubclass)
const e = new RestrictedSubclass()
container.appendChild(e)
// Test restricted name
e.name = 'ab' // Too short, should be rejected
e.age = 25
e.active = true
await nextTick()
// Since the short name was rejected, Vue property remains undefined
expect(e.shadowRoot!.innerHTML).toBe('<div>undefined-25-true</div>')
e.name = 'alice' // Valid
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div>alice-25-true</div>')
// Test restricted age
e.age = -5 // Invalid
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div>alice-25-true</div>')
e.age = 30 // Valid
await nextTick()
expect(e.shadowRoot!.innerHTML).toBe('<div>alice-30-true</div>')
}) |
Fixes #13706
In Vue.js custom elements, it was impossible to override property setters in subclasses because Vue defined properties directly on the element instance, overwriting any existing setters.
Solution
Changed the property definition mechanism from instance-level to prototype-level, allowing subclasses to correctly override setters.
Benefits
Inheritance compatibility - subclasses can override setters
Custom validation - ability to add validation logic in subclasses
Preserved reactivity - Vue reactivity continues to work
Performance improvement - properties defined once on prototype
Backward compatibility - existing code continues to work
Practical Use Case
Now you can create custom elements with validation:
class DatePicker extends VueDateElement {
set value(date: string) {
if (!this.isValidDate(date)) {
return // Reject invalid date
}
super.value = date
}
}
Summary by CodeRabbit
Tests
Refactor