Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/compiler/transformers/update-stencil-core-import.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,4 +115,5 @@ const KEEP_IMPORTS = new Set([
'jsx',
'jsxs',
'jsxDEV',
'ReactiveControllerHost',
]);
41 changes: 41 additions & 0 deletions src/declarations/stencil-public-runtime.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,47 @@ export interface ComponentInterface {
[memberName: string]: any;
}

/**
* Interface for reactive controllers that can be attached to a ReactiveControllerHost.
* Controllers implement lifecycle hooks that are called by the host during component lifecycle.
*/
export interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
hostWillLoad?(): Promise<void> | void;
hostDidLoad?(): void;
hostWillRender?(): Promise<void> | void;
hostDidRender?(): void;
hostWillUpdate?(): Promise<void> | void;
Comment on lines +642 to +652
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The documentation for the ReactiveController interface is minimal. Consider adding JSDoc comments explaining the purpose and usage of each lifecycle hook method, similar to how ComponentInterface methods are documented elsewhere in the codebase. This would help developers understand when each hook is called and what they should be used for.

Suggested change
* Interface for reactive controllers that can be attached to a ReactiveControllerHost.
* Controllers implement lifecycle hooks that are called by the host during component lifecycle.
*/
export interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
hostWillLoad?(): Promise<void> | void;
hostDidLoad?(): void;
hostWillRender?(): Promise<void> | void;
hostDidRender?(): void;
hostWillUpdate?(): Promise<void> | void;
* Interface for reactive controllers that can be attached to a {@link ReactiveControllerHost}.
*
* Controllers implement lifecycle hooks that are called by the host at specific points in the
* component's lifecycle. These hooks mirror the {@link ComponentInterface} lifecycle methods
* and allow cross‑cutting logic (such as state management, subscriptions, or side‑effects)
* to be encapsulated and reused across multiple components.
*/
export interface ReactiveController {
/**
* Called when the host component is connected to the DOM.
*
* This corresponds to the host's `connectedCallback`. Use this hook to perform setup
* work that depends on the host being attached, such as registering event listeners
* on global objects or starting subscriptions.
*/
hostConnected?(): void;
/**
* Called when the host component is disconnected from the DOM.
*
* This corresponds to the host's `disconnectedCallback`. Use this hook to clean up
* any work started in {@link ReactiveController.hostConnected}, such as removing
* event listeners, cancelling timers, or disposing of subscriptions.
*/
hostDisconnected?(): void;
/**
* Called before the host component has loaded and before its first render.
*
* This corresponds to the host's `componentWillLoad`. Use this hook to perform
* asynchronous or synchronous setup that should complete before the initial
* render, such as fetching data or initializing controller state.
*
* This hook is only called once during the lifetime of the host.
*/
hostWillLoad?(): Promise<void> | void;
/**
* Called after the host component has loaded and completed its first render.
*
* This corresponds to the host's `componentDidLoad`. Use this hook for logic that
* depends on the host's initial DOM being rendered, such as measuring layout or
* interacting with rendered child elements.
*
* This hook is only called once during the lifetime of the host.
*/
hostDidLoad?(): void;
/**
* Called before the host component is about to render.
*
* This corresponds to the host's `componentWillRender`. Use this hook to perform
* last‑minute state adjustments before a render is triggered. It may be called
* multiple times over the life of the host as it re‑renders.
*/
hostWillRender?(): Promise<void> | void;
/**
* Called after the host component has just rendered.
*
* This corresponds to the host's `componentDidRender`. Use this hook to perform
* work that depends on the latest rendered DOM, such as DOM measurements or
* non‑reactive side‑effects.
*
* It may be called multiple times over the life of the host as it re‑renders.
*/
hostDidRender?(): void;
/**
* Called before the host component updates and re‑renders due to a state or prop change.
*
* This corresponds to the host's `componentWillUpdate`. Use this hook to react to
* changes before the DOM is updated. It may be called multiple times throughout the
* life of the host as it updates.
*
* This hook is not called for the initial render.
*/
hostWillUpdate?(): Promise<void> | void;
/**
* Called after the host component has updated and re‑rendered.
*
* This corresponds to the host's `componentDidUpdate`. Use this hook to perform
* side‑effects that depend on the updated DOM or state after an update.
*
* It may be called multiple times throughout the life of the host as it updates,
* and is not called for the initial render.
*/

Copilot uses AI. Check for mistakes.
hostDidUpdate?(): void;
}

/**
* Base class that implements ComponentInterface and provides reactive controller functionality.
* Components can extend this class to enable reactive controller composition.
*
* Known Limitation: Components extending ReactiveControllerHost cannot use
* `<Host>` as their root element in the render method. This is because
* ReactiveControllerHost does not extend HTMLElement. Instead, return a
* regular element (like `<div>`) as the root.
*/
export declare class ReactiveControllerHost implements ComponentInterface {
controllers: Set<ReactiveController>;
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The controllers property in the type declaration should be declared as readonly to match the recommendation for the implementation and prevent external modification of the Set. The API provides addController and removeController methods which should be the only way to manage controllers.

Suggested change
controllers: Set<ReactiveController>;
readonly controllers: Set<ReactiveController>;

Copilot uses AI. Check for mistakes.

addController(controller: ReactiveController): void;
removeController(controller: ReactiveController): void;
requestUpdate(): void;
connectedCallback(): void;
disconnectedCallback(): void;
componentWillLoad(): void;
componentDidLoad(): void;
componentWillRender(): void;
componentDidRender(): void;
componentWillUpdate(): void;
Comment on lines +673 to +677
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The type declaration for lifecycle methods doesn't match the implementation. The componentWillLoad, componentWillRender, and componentWillUpdate methods should be declared with return type Promise<void> | void to match the ComponentInterface and properly handle async operations. Currently they're declared as returning void only.

Suggested change
componentWillLoad(): void;
componentDidLoad(): void;
componentWillRender(): void;
componentDidRender(): void;
componentWillUpdate(): void;
componentWillLoad(): Promise<void> | void;
componentDidLoad(): void;
componentWillRender(): Promise<void> | void;
componentDidRender(): void;
componentWillUpdate(): Promise<void> | void;

Copilot uses AI. Check for mistakes.
componentDidUpdate(): void;
[memberName: string]: any;
}

// General types important to applications using stencil built components
export interface EventEmitter<T = any> {
emit: (data?: T) => CustomEvent<T>;
Expand Down
1 change: 1 addition & 0 deletions src/hydrate/platform/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,7 @@ export {
postUpdateComponent,
proxyComponent,
proxyCustomElement,
ReactiveControllerHost,
renderVdom,
setAssetPath,
setMode,
Expand Down
2 changes: 2 additions & 0 deletions src/internal/stencil-core/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export type {
PropOptions,
QueueApi,
RafCallback,
ReactiveController,
VNode,
VNodeData,
} from '../stencil-public-runtime';
Expand All @@ -45,6 +46,7 @@ export {
Mixin,
MixinFactory,
Prop,
ReactiveControllerHost,
readTask,
render,
setAssetPath,
Expand Down
1 change: 1 addition & 0 deletions src/runtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ export { setNonce } from './nonce';
export { parsePropertyValue } from './parse-property-value';
export { setPlatformOptions } from './platform-options';
export { proxyComponent } from './proxy-component';
export { ReactiveController, ReactiveControllerHost } from './reactive-controller';
export { render } from './render';
export { HYDRATED_STYLE_ID } from './runtime-constants';
export { getValue, setValue } from './set-value';
Expand Down
84 changes: 84 additions & 0 deletions src/runtime/reactive-controller.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import type { ComponentInterface } from '../declarations/stencil-public-runtime';
import { forceUpdate } from './update-component';

export interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
hostWillLoad?(): Promise<void> | void;
hostDidLoad?(): void;
hostWillRender?(): Promise<void> | void;
hostDidRender?(): void;
hostWillUpdate?(): Promise<void> | void;
Comment on lines +4 to +11
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The ReactiveController interface definition lacks JSDoc comments for individual methods. Consider documenting each lifecycle hook to explain when it's called and what it should be used for. For example, clarify that hostConnected corresponds to connectedCallback, hostWillLoad to componentWillLoad, etc.

Suggested change
export interface ReactiveController {
hostConnected?(): void;
hostDisconnected?(): void;
hostWillLoad?(): Promise<void> | void;
hostDidLoad?(): void;
hostWillRender?(): Promise<void> | void;
hostDidRender?(): void;
hostWillUpdate?(): Promise<void> | void;
export interface ReactiveController {
/**
* Called when the host component's `connectedCallback` is invoked.
* Use this to perform setup that depends on the host being connected to the DOM.
*/
hostConnected?(): void;
/**
* Called when the host component's `disconnectedCallback` is invoked.
* Use this to perform cleanup when the host is removed from the DOM.
*/
hostDisconnected?(): void;
/**
* Called when the host component's `componentWillLoad` lifecycle runs.
* Use this to perform work before the host first renders.
*/
hostWillLoad?(): Promise<void> | void;
/**
* Called when the host component's `componentDidLoad` lifecycle runs.
* Use this to perform work after the host has finished its initial render.
*/
hostDidLoad?(): void;
/**
* Called when the host component's `componentWillRender` lifecycle runs.
* Use this to react to state changes before each render.
*/
hostWillRender?(): Promise<void> | void;
/**
* Called when the host component's `componentDidRender` lifecycle runs.
* Use this to perform work after each render.
*/
hostDidRender?(): void;
/**
* Called when the host component's `componentWillUpdate` lifecycle runs.
* Use this to react to prop or state changes before an update.
*/
hostWillUpdate?(): Promise<void> | void;
/**
* Called when the host component's `componentDidUpdate` lifecycle runs.
* Use this to perform work after an update has been applied.
*/

Copilot uses AI. Check for mistakes.
hostDidUpdate?(): void;
}

/**
* Base class for components that want to use reactive controllers.
*
* Components extending this class can use the composition pattern to share
* stateful logic via reactive controllers.
*
* Known Limitation: Components extending ReactiveControllerHost cannot use
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't understand this limitation (/ is it acceptable?) - can you explain it to me?

<Host> is just a jsx helper, allowing devs to target and sprout attributes on the output <custom-element> tag.

Copy link
Contributor

@paulvisciano paulvisciano Jan 21, 2026

Choose a reason for hiding this comment

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

When a component extends ReactiveControllerHost and uses as the root element, Stencil's runtime expects the component instance to be the actual custom element (HTMLElement), but ReactiveControllerHost doesn't satisfy that contract, leading to initialization failures. When I tried to make the ReactiveControllerHost extend HTMLElement I hit some initialization issues

Runtime error : custom-elements#externalruntime: undefined
When ReactiveControllerHost extends HTMLElement and is imported from @stencil/core, the Stencil compiler's lazy loading mechanism fails to properly initialize component instances. The runtime thinks these are external custom-element components that need the externalRuntime flag, but they're actually regular Stencil components.

If you have tips on how to address this it would be awesome.

* `<Host>` as their root element in the render method. This is because
* ReactiveControllerHost does not extend HTMLElement. Instead, return a
* regular element (like `<div>`) as the root.
*
* @example
* ```tsx
* @Component({ tag: 'my-component' })
* export class MyComponent extends ReactiveControllerHost {
* private myController = new MyController(this);
*
* render() {
* return <div>...</div>; // Use <div>, not <Host>
* }
* }
* ```
*/
export class ReactiveControllerHost implements ComponentInterface {
controllers = new Set<ReactiveController>();
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The controllers property is declared as public without any access modifier or documentation. Consider making it readonly to prevent external modification of the Set, as the API provides addController and removeController methods for managing controllers. This would ensure the Set is only modified through the intended API surface.

Suggested change
controllers = new Set<ReactiveController>();
readonly controllers = new Set<ReactiveController>();

Copilot uses AI. Check for mistakes.

addController(controller: ReactiveController) {
this.controllers.add(controller);
}

removeController(controller: ReactiveController) {
this.controllers.delete(controller);
}

requestUpdate() {
forceUpdate(this);
}

connectedCallback() {
this.controllers.forEach((controller) => controller.hostConnected?.());
}

disconnectedCallback() {
this.controllers.forEach((controller) => controller.hostDisconnected?.());
}

componentWillLoad() {
this.controllers.forEach((controller) => controller.hostWillLoad?.());
}

componentDidLoad() {
this.controllers.forEach((controller) => controller.hostDidLoad?.());
}

componentWillRender() {
this.controllers.forEach((controller) => controller.hostWillRender?.());
}

componentDidRender() {
this.controllers.forEach((controller) => controller.hostDidRender?.());
}

componentWillUpdate() {
this.controllers.forEach((controller) => controller.hostWillUpdate?.());
Comment on lines +61 to +78
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The lifecycle methods componentWillLoad, componentWillRender, and componentWillUpdate do not properly handle Promise return values from controller hooks. The controller interface allows these methods to return Promise<void> | void, but the host implementation doesn't await these promises. This could lead to race conditions where lifecycle execution continues before async controller operations complete. Consider using Promise.all() to await all controller promises before proceeding.

Suggested change
componentWillLoad() {
this.controllers.forEach((controller) => controller.hostWillLoad?.());
}
componentDidLoad() {
this.controllers.forEach((controller) => controller.hostDidLoad?.());
}
componentWillRender() {
this.controllers.forEach((controller) => controller.hostWillRender?.());
}
componentDidRender() {
this.controllers.forEach((controller) => controller.hostDidRender?.());
}
componentWillUpdate() {
this.controllers.forEach((controller) => controller.hostWillUpdate?.());
componentWillLoad(): Promise<void> | void {
const promises: Promise<void>[] = [];
this.controllers.forEach((controller) => {
const result = controller.hostWillLoad?.();
if (result instanceof Promise) {
promises.push(result);
}
});
if (promises.length > 0) {
return Promise.all(promises).then(() => undefined);
}
}
componentDidLoad() {
this.controllers.forEach((controller) => controller.hostDidLoad?.());
}
componentWillRender(): Promise<void> | void {
const promises: Promise<void>[] = [];
this.controllers.forEach((controller) => {
const result = controller.hostWillRender?.();
if (result instanceof Promise) {
promises.push(result);
}
});
if (promises.length > 0) {
return Promise.all(promises).then(() => undefined);
}
}
componentDidRender() {
this.controllers.forEach((controller) => controller.hostDidRender?.());
}
componentWillUpdate(): Promise<void> | void {
const promises: Promise<void>[] = [];
this.controllers.forEach((controller) => {
const result = controller.hostWillUpdate?.();
if (result instanceof Promise) {
promises.push(result);
}
});
if (promises.length > 0) {
return Promise.all(promises).then(() => undefined);
}

Copilot uses AI. Check for mistakes.
}

componentDidUpdate() {
this.controllers.forEach((controller) => controller.hostDidUpdate?.());
Comment on lines +53 to +82
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

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

The lifecycle methods in ReactiveControllerHost don't include error handling when invoking controller hooks. If a controller's lifecycle hook throws an error, it could prevent other controllers from executing their hooks. Consider wrapping controller hook calls in try-catch blocks to ensure one controller's error doesn't affect others.

Suggested change
connectedCallback() {
this.controllers.forEach((controller) => controller.hostConnected?.());
}
disconnectedCallback() {
this.controllers.forEach((controller) => controller.hostDisconnected?.());
}
componentWillLoad() {
this.controllers.forEach((controller) => controller.hostWillLoad?.());
}
componentDidLoad() {
this.controllers.forEach((controller) => controller.hostDidLoad?.());
}
componentWillRender() {
this.controllers.forEach((controller) => controller.hostWillRender?.());
}
componentDidRender() {
this.controllers.forEach((controller) => controller.hostDidRender?.());
}
componentWillUpdate() {
this.controllers.forEach((controller) => controller.hostWillUpdate?.());
}
componentDidUpdate() {
this.controllers.forEach((controller) => controller.hostDidUpdate?.());
private invokeControllerHook(hook: keyof ReactiveController) {
this.controllers.forEach((controller) => {
const callback = controller[hook];
if (typeof callback === 'function') {
try {
callback.call(controller);
} catch (error) {
// Ensure one controller's error doesn't prevent others from running
console.error(
`Error in ReactiveController ${String(hook)} hook:`,
error,
);
}
}
});
}
connectedCallback() {
this.invokeControllerHook('hostConnected');
}
disconnectedCallback() {
this.invokeControllerHook('hostDisconnected');
}
componentWillLoad() {
this.invokeControllerHook('hostWillLoad');
}
componentDidLoad() {
this.invokeControllerHook('hostDidLoad');
}
componentWillRender() {
this.invokeControllerHook('hostWillRender');
}
componentDidRender() {
this.invokeControllerHook('hostDidRender');
}
componentWillUpdate() {
this.invokeControllerHook('hostWillUpdate');
}
componentDidUpdate() {
this.invokeControllerHook('hostDidUpdate');

Copilot uses AI. Check for mistakes.
}
}
13 changes: 0 additions & 13 deletions test/wdio/ts-target/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -307,8 +307,6 @@ export namespace Components {
*/
interface ExtendsRender {
}
interface ExtendsViaHostCmp {
}
/**
* WatchCmp - Demonstrates
* @Watch decorator inheritance
Expand Down Expand Up @@ -606,12 +604,6 @@ declare global {
prototype: HTMLExtendsRenderElement;
new (): HTMLExtendsRenderElement;
};
interface HTMLExtendsViaHostCmpElement extends Components.ExtendsViaHostCmp, HTMLStencilElement {
}
var HTMLExtendsViaHostCmpElement: {
prototype: HTMLExtendsViaHostCmpElement;
new (): HTMLExtendsViaHostCmpElement;
};
/**
* WatchCmp - Demonstrates
* @Watch decorator inheritance
Expand Down Expand Up @@ -708,7 +700,6 @@ declare global {
"extends-mixin-cmp": HTMLExtendsMixinCmpElement;
"extends-props-state": HTMLExtendsPropsStateElement;
"extends-render": HTMLExtendsRenderElement;
"extends-via-host-cmp": HTMLExtendsViaHostCmpElement;
"extends-watch": HTMLExtendsWatchElement;
"inheritance-checkbox-group": HTMLInheritanceCheckboxGroupElement;
"inheritance-radio-group": HTMLInheritanceRadioGroupElement;
Expand Down Expand Up @@ -906,8 +897,6 @@ declare namespace LocalJSX {
*/
interface ExtendsRender {
}
interface ExtendsViaHostCmp {
}
/**
* WatchCmp - Demonstrates
* @Watch decorator inheritance
Expand Down Expand Up @@ -986,7 +975,6 @@ declare namespace LocalJSX {
"extends-mixin-cmp": ExtendsMixinCmp;
"extends-props-state": ExtendsPropsState;
"extends-render": ExtendsRender;
"extends-via-host-cmp": ExtendsViaHostCmp;
"extends-watch": ExtendsWatch;
"inheritance-checkbox-group": InheritanceCheckboxGroup;
"inheritance-radio-group": InheritanceRadioGroup;
Expand Down Expand Up @@ -1071,7 +1059,6 @@ declare module "@stencil/core" {
* - CSS Class Inheritance: CSS classes from parent template maintained in component extension
*/
"extends-render": LocalJSX.ExtendsRender & JSXBase.HTMLAttributes<HTMLExtendsRenderElement>;
"extends-via-host-cmp": LocalJSX.ExtendsViaHostCmp & JSXBase.HTMLAttributes<HTMLExtendsViaHostCmpElement>;
/**
* WatchCmp - Demonstrates
* @Watch decorator inheritance
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component, h, State, Element, Event, EventEmitter } from '@stencil/core';
import { ReactiveControllerHost } from './reactive-controller-host.js';
import { Component, h, ReactiveControllerHost, State, Element, Event, EventEmitter } from '@stencil/core';
import { ValidationController } from './validation-controller.js';
import { FocusController } from './focus-controller.js';

Expand All @@ -15,7 +14,7 @@ export class CheckboxGroupCmp extends ReactiveControllerHost {

// Controllers via composition
private validation = new ValidationController(this);
private focus = new FocusController(this);
private focusController = new FocusController(this);

private inputId = `checkbox-group-${Math.random().toString(36).substr(2, 9)}`;
private helperTextId = `${this.inputId}-helper-text`;
Expand Down Expand Up @@ -55,16 +54,16 @@ export class CheckboxGroupCmp extends ReactiveControllerHost {
};

private handleFocus = () => {
this.focus.handleFocus();
this.focusController.handleFocus();
};

private handleBlur = () => {
this.focus.handleBlur();
this.focusController.handleBlur();
this.validation.handleBlur(this.values);
};

render() {
const focusState = this.focus.getFocusState();
const focusState = this.focusController.getFocusState();
const validationData = this.validation.getValidationMessageData(this.helperTextId, this.errorTextId);

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* 3. Provides methods to handle focus lifecycle
*/
import { forceUpdate } from '@stencil/core';
import type { ReactiveControllerHost, ReactiveController } from './reactive-controller-host.js';
import type { ReactiveControllerHost, ReactiveController } from '@stencil/core';

export class FocusController implements ReactiveController {
private host: ReactiveControllerHost;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component, h, State, Element, Event, EventEmitter } from '@stencil/core';
import { ReactiveControllerHost } from './reactive-controller-host.js';
import { Component, h, ReactiveControllerHost, State, Element, Event, EventEmitter } from '@stencil/core';
import { ValidationController } from './validation-controller.js';
import { FocusController } from './focus-controller.js';

Expand All @@ -15,7 +14,7 @@ export class RadioGroupCmp extends ReactiveControllerHost {

// Controllers via composition
private validation = new ValidationController(this);
private focus = new FocusController(this);
private focusController = new FocusController(this);

private inputId = `radio-group-${Math.random().toString(36).substr(2, 9)}`;
private helperTextId = `${this.inputId}-helper-text`;
Expand Down Expand Up @@ -50,16 +49,16 @@ export class RadioGroupCmp extends ReactiveControllerHost {
};

private handleFocus = () => {
this.focus.handleFocus();
this.focusController.handleFocus();
};

private handleBlur = () => {
this.focus.handleBlur();
this.focusController.handleBlur();
this.validation.handleBlur(this.value);
};

render() {
const focusState = this.focus.getFocusState();
const focusState = this.focusController.getFocusState();
const validationData = this.validation.getValidationMessageData(this.helperTextId, this.errorTextId);

return (
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Component, h, State, Element } from '@stencil/core';
import { ReactiveControllerHost } from './reactive-controller-host.js';
import { Component, h, ReactiveControllerHost, State, Element } from '@stencil/core';
import { ValidationController } from './validation-controller.js';
import { FocusController } from './focus-controller.js';

Expand All @@ -13,7 +12,7 @@ export class TextInputCmp extends ReactiveControllerHost {

// Controllers via composition
private validation = new ValidationController(this);
private focus = new FocusController(this);
private focusController = new FocusController(this);

private inputId = `text-input-${Math.random().toString(36).substr(2, 9)}`;
private helperTextId = `${this.inputId}-helper-text`;
Expand Down Expand Up @@ -47,16 +46,16 @@ export class TextInputCmp extends ReactiveControllerHost {
};

private handleFocus = () => {
this.focus.handleFocus();
this.focusController.handleFocus();
};

private handleBlur = () => {
this.focus.handleBlur();
this.focusController.handleBlur();
this.validation.handleBlur(this.value);
};

render() {
const focusState = this.focus.getFocusState();
const focusState = this.focusController.getFocusState();
const validationState = this.validation.getValidationState();
const validationData = this.validation.getValidationMessageData(this.helperTextId, this.errorTextId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
* 4. Runs a callback provided by the host for validation logic
*/
import { forceUpdate } from '@stencil/core';
import type { ReactiveControllerHost, ReactiveController } from './reactive-controller-host.js';
import type { ReactiveControllerHost, ReactiveController } from '@stencil/core';

export class ValidationController implements ReactiveController {
private host: ReactiveControllerHost;
Expand Down
Loading