feat(runtime): allow class extending#6362
Merged
johnjenkins merged 19 commits intomainfrom Aug 29, 2025
Merged
Conversation
jttommy
reviewed
Aug 4, 2025
added 3 commits
August 5, 2025 00:33
Contributor
|
@johnjenkins any plans also to support mixins pattern? |
Contributor
Author
|
@danielleroux can you give an example? 🙂 |
Contributor
import { Component, h, Host, Prop, State } from '@stencil/core';
type StencilElement = HTMLElement & ....;
type Constructor<T> = new (...args: any[]) => T;
declare class ValidationInterface {
isValid: boolean;
errorMessage: string;
handleValidation(value: string): void;
renderValidationFeedback(): unknown;
}
declare class LoadingInterface {
isLoading: boolean;
setLoading(loading: boolean): void;
renderLoading(container: unknown): unknown;
}
const Validation = <T extends Constructor<StencilElement>>(superClass: T) => {
class ValidationElement extends superClass {
@Prop({ mutable: true }) isValid: boolean = true;
@Prop({ mutable: true }) errorMessage: string = '';
handleValidation(value: string): void {
if (value.trim() === '') {
this.isValid = false;
this.errorMessage = 'This field cannot be empty.';
} else {
this.isValid = true;
this.errorMessage = '';
}
}
renderValidationFeedback(): unknown {
if (!this.isValid && this.errorMessage) {
return <div class="error-message">{this.errorMessage}</div>;
}
return null;
}
}
return ValidationElement as Constructor<ValidationInterface> & T;
};
const Loading = <T extends Constructor<StencilElement>>(superClass: T) => {
class LoadingElement extends superClass {
@State() isLoading: boolean = false;
setLoading(loading: boolean) {
this.isLoading = loading;
}
renderLoading(container: unknown): unknown {
if (this.isLoading) {
return <div class="loading-spinner">Loading...</div>;
}
return container;
}
}
return LoadingElement as Constructor<LoadingInterface> & T;
};
@Component({
tag: 'example-component',
styleUrl: 'example.css',
shadow: true,
})
export class ExampleComponent extends Loading(Validation(HTMLElement)) {
onValueChange(event: Event) {
const input = event.target as HTMLInputElement;
this.setLoading(true);
this.handleValidation(input.value);
this.setLoading(false);
}
render() {
return (
<Host>
{this.renderLoading(
<input
type="text"
placeholder="Type something..."
onChange={this.onValueChange.bind(this)}
/>
)}
{this.renderValidationFeedback()}
</Host>
);
}
}
@Component({
tag: 'example-component',
styleUrl: 'example.css',
shadow: true,
})
export class MyComponent extends Loading(HTMLElement) {
componentDidLoad() {
setLoading(true);
setTimeout(() => {
{this.renderValidationFeedback()}
}, 1000);
}
render() {
return (
<Host>
{this.renderLoading(
<input
type="text"
placeholder="Type something..."
onChange={this.onValueChange.bind(this)}
/>
)}
</Host>
);
}
} |
Contributor
Author
I see - thanks!
is just too dynamic to pick apart in a reliable way. if we can standardise around an api to do essentially do this for devs (eg an `@Mixin(class1, class2)’ then it would be fine |
src/compiler/transformers/component-native/tranform-to-native-component.ts
Show resolved
Hide resolved
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What is the current behavior?
Stencil component classes currently do not allow
extends... mainly because it utilises static-analysis to gather meta for properties, methods, events and docs - this is hard for dynamically extending classes.GitHub Issue Number:
What is the new behavior?
This is a decent size PR to enable all kinds of
extendswithin Stencil:extendfrom other componentsextendfrom abstract, Stencil decorated classes that are not designed to be full componentsextendfrom 3rd party / node_module stencil components (viadist/collection)extendfrom non-stencil classesThe only caveat is that Stencil decorated classes can only be extended from if the
tsconfigtarget ises2022or higher. This is due to how Stencil initialises classes by changing their prototype, how older classes instantiate class properties within the constructor and the order ofsuper()initialisation.Documentation
Does this introduce a breaking change?
Testing
Other information