Skip to content

feat(runtime): allow class extending #6362

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

Draft
wants to merge 10 commits into
base: main
Choose a base branch
from
Draft

Conversation

johnjenkins
Copy link
Contributor

@johnjenkins johnjenkins commented Jul 31, 2025

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 extends within Stencil:

  1. Components can extend from other components
  2. Components can extend from abstract, Stencil decorated classes that are not designed to be full components
  3. Components can extend from 3rd party / node_module stencil components (via dist/collection)
  4. Components can extend from non-stencil classes
  5. The extend tree / chain is preserved and works appropriately

The only caveat is that Stencil decorated classes can only be extended from if the tsconfig target is es2022 or 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 of super() initialisation.

Documentation

Does this introduce a breaking change?

  • Yes
  • No

Testing

TODO!

Other information

@johnjenkins johnjenkins requested a review from a team as a code owner July 31, 2025 16:43
@johnjenkins johnjenkins marked this pull request as draft August 4, 2025 23:09
@danielleroux
Copy link
Contributor

@johnjenkins any plans also to support mixins pattern?

@johnjenkins
Copy link
Contributor Author

@danielleroux can you give an example? 🙂

@danielleroux
Copy link
Contributor

danielleroux commented Aug 6, 2025

@danielleroux can you give an example? 🙂

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>
    );
  }
}

Source: https://lit.dev/docs/composition/mixins/

@johnjenkins
Copy link
Contributor Author

@danielleroux can you give an example? 🙂

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>
    );
  }
}

Source: https://lit.dev/docs/composition/mixins/

I see - thanks!
I can’t imagine this kind of pattern would be easy to get working.
The nature of static analysis means we need reliable, repeatable rules we can pick apart to get what we need. The nature of something like

const Loading = <T extends Constructor<StencilElement>>(superClass: T) => { class LoadingElement extends superClass

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Feature Request : @Props OR component inheritance Add props at compile time
3 participants