Components
In Croqueta, components are the building blocks of your application. They are based on standard Web Components but enhanced with a reactive system powered by signals and a template system.
Creating a Component
To create a component, you must extend the Component base class, define a static tag property, and implement the render method.
import { Component, html } from '@mimopo/croqueta';
export class MyComponent extends Component {
static tag = 'my-component';
protected render() {
return html`<h1>Hello World</h1>`;
}
}import { Component, html } from '@mimopo/croqueta';
export class MyComponent extends Component {
static tag = 'my-component';
render() {
return html`<h1>Hello World</h1>`;
}
}
- tag: The name of the custom element (it must be a valid custom element name).
- render(): This method must return a
Node. Using thehtmltagged template literal is the recommended way to create complex templates.
Registering a component
Before using a component you need to register it. The recommended way is to use the helper function registerComponent, it registers the custom element if it's not registered yet.
The recommended approach when nesting components is to register the child components in the parent component's constructor:
import { Component, html, registerComponent } from '@mimopo/croqueta';
class MyComponent extends Component {
static tag = 'my-component';
constructor() {
super();
registerComponent(MyChildComponent);
}
protected render(): Node {
return html`<my-child-component></my-child-component>`;
}
}import { Component, html, registerComponent } from '@mimopo/croqueta';
class MyComponent extends Component {
static tag = 'my-component';
constructor() {
super();
registerComponent(MyChildComponent);
}
render() {
return html`<my-child-component></my-child-component>`;
}
}
Template Syntax
The html tagged template literal allows you to build reactive templates with ease. By passing a signal to the template, the value printed will be updated automatically when the signal changes.
Data Binding
- Text Binding: Interpolate values or signals directly.
html`<div>Count: ${this.count}</div>`; - Property Binding: Use
[]to bind to an element property.html`<button [disabled]="${this.isDisabled}">Submit</button>`; - Attribute Binding: Standard attributes can also be reactive.
html`<div class="${this.theme}"></div>`; - Event Binding: Use
()to listen to DOM or custom events.html`<button (click)="${() => this.increment()}">Add</button>`; - Two-way Data Binding: Use
[()]for two-way synchronization between a property and a signal.html`<input [(value)]="${this.username}" />`;
List Rendering
Use the repeat function for efficient list rendering. It requires a signal-like array, a key extraction function, and a template function. It will handle updates by only updating the nodes that have changed.
import { Component, html, repeat, signal } from '@mimopo/croqueta';
interface Item {
id: number;
name: string;
}
export class MyComponent extends Component {
static tag = 'my-component';
private _items = signal<Item[]>([{ id: 1, name: 'Item 1' }]);
protected render() {
return html`
<button (click)="${() => this.addItem()}">Add</button>
<ul>
${repeat(
this._items,
(item) => item.id,
(item) =>
html`<li>
${item.name}
<button (click)="${() => this.removeItem(item.id)}">Remove</button>
</li>`
)}
</ul>
`;
}
private addItem() {
this._items.set([
...this._items.get(),
{
id: this._items.get().length + 1,
name: 'Item ' + (this._items.get().length + 1),
},
]);
}
private removeItem(id: number) {
this._items.set(this._items.get().filter((item) => item.id !== id));
}
}import { Component, html, repeat, signal } from '@mimopo/croqueta';
export class MyComponent extends Component {
static tag = 'my-component';
_items = signal([{ id: 1, name: 'Item 1' }]);
render() {
return html`
<button (click)="${() => this.addItem()}">Add</button>
<ul>
${repeat(
this._items,
(item) => item.id,
(item) =>
html`<li>
${item.name}
<button (click)="${() => this.removeItem(item.id)}">Remove</button>
</li>`
)}
</ul>
`;
}
addItem() {
this._items.set([
...this._items.get(),
{
id: this._items.get().length + 1,
name: 'Item ' + (this._items.get().length + 1),
},
]);
}
removeItem(id) {
this._items.set(this._items.get().filter((item) => item.id !== id));
}
}
Encapsulation (Shadow DOM)
By default, components use a closed shadow DOM. You can configure this in the constructor using ComponentOptions.
constructor() {
super({ shadow: 'open' }); // 'open' | 'closed' | 'none'
}- none: Renders directly in the light DOM (no encapsulation).
- open: Renders inside an open Shadow Root.
- closed: Renders inside a closed Shadow Root.
Styling
Styles are defined using the static styles property. Styles are automatically scoped if the component uses Shadow DOM, prefixed by the component tag name if not using Shadow DOM.
import { Component, html } from '@mimopo/croqueta';
export class MyComponent extends Component {
static tag = 'my-component';
static styles = `
h1 { color: blue; }
`;
protected render() {
return html`<h1>Hello World</h1>`;
}
}import { Component, html } from '@mimopo/croqueta';
export class MyComponent extends Component {
static tag = 'my-component';
static styles = `
h1 { color: blue; }
`;
render() {
return html`<h1>Hello World</h1>`;
}
}
Global styles
You can add styles globally by using the addGlobalStyles method of the StylesService. All the components will receive the same styles:
import { inject, StylesService } from '@mimopo/croqueta';
inject(StylesService).addGlobalStyles(`.hidden { display: none; }`);Lifecycle
Components use the standard Custom Elements lifecycle callbacks. There are no custom lifecycle callbacks on purpose, but you can use the standard ones depending on what you need to do:
IMPORTANTIf you override them, always call the super method to ensure the framework's internal logic (like effects cleanup or rendering) works correctly.
- If you need to run logic before rendering, override
connectedCallbackand add your logic before callingsuper.connectedCallback(). - If you need to run logic after rendering, override
attributeChangedCallbackand add your logic after callingsuper.attributeChangedCallback(). - If you need to run teardown logic, override
disconnectedCallbackand add your logic before callingsuper.disconnectedCallback().
connectedCallback() {
// your custom logic before rendering
super.connectedCallback();
// your custom logic after rendering
}
disconnectedCallback() {
super.disconnectedCallback();
// your custom logic on disconnect
}Reactivity
Croqueta uses Signals for reactivity (the signals implementation follows the TC39 Signals proposal). When a signal is used in a template, the UI updates automatically by updating only the changed nodes.
Signals
Import signal to create reactive state.
import { Component, html, signal } from '@mimopo/croqueta';
export class MyCounter extends Component {
static tag = 'my-counter';
private _count = signal<number>(0);
protected render() {
return html`Current count is: ${this._count}`;
}
}import { Component, html, signal } from '@mimopo/croqueta';
export class MyCounter extends Component {
static tag = 'my-counter';
_count = signal(0);
render() {
return html`Current count is: ${this._count}`;
}
}
Computed Signals
If you need to get values based on a signal value, use computed().
import { Component, html, signal, computed } from '@mimopo/croqueta';
interface User {
name: string;
surname: string;
}
export class MyGreeter extends Component {
static tag = 'my-greeter';
private _user = signal<User>({
name: 'John',
surname: 'Doe',
});
private _fullName = computed<string>(() => `${this._user.get().name} ${this._user.get().surname}`);
protected render() {
return html`Current count is: ${this._fullName}`;
}
}import { Component, html, signal, computed } from '@mimopo/croqueta';
export class MyGreeter extends Component {
static tag = 'my-greeter';
_user = signal({
name: 'John',
surname: 'Doe',
});
_fullName = computed(() => `${this._user.get().name} ${this._user.get().surname}`);
render() {
return html`Current count is: ${this._fullName}`;
}
}
Further information about signals can be found in the Reactivity section.
Effects
Use this.effect() to run side effects that react to signal changes. The framework automatically cleans up these effects when the component is disconnected.
import { Component, html, signal } from '@mimopo/croqueta';
export class MyCounter extends Component {
static tag = 'my-counter';
private _count = signal<number>(0);
constructor() {
super();
this.effect(() => {
console.log('Count changed to:', this._count.get());
});
}
protected render() {
return html`Current count is: ${this._count}`;
}
}import { Component, html, signal } from '@mimopo/croqueta';
export class MyCounter extends Component {
static tag = 'my-counter';
_count = signal(0);
constructor() {
super();
this.effect(() => {
console.log('Count changed to:', this._count.get());
});
}
render() {
return html`Current count is: ${this._count}`;
}
}
Communication
Inputs
Use this.input() to define reactive properties that can be set from the outside. It will return a signal and create a property getter/setter with the provided name meant to be used from outside. You can keep the signal private.
IMPORTANTRemember the signal and the property must have different names, if you forget it the framework will throw an error.
TIPChoose a convention you can stick to, for example keep the signal private by prefixing it with
#or adding a prefix like_.
import { Component, html } from '@mimopo/croqueta';
export class MyGreeter extends Component {
static tag = 'my-greeter';
private _user = this.input<string>('user', '');
protected render() {
return html`Hello ${this._user}`;
}
}import { Component, html } from '@mimopo/croqueta';
export class MyGreeter extends Component {
static tag = 'my-greeter';
_user = this.input('user', '');
render() {
return html`Hello ${this._user}`;
}
}
Outputs
Use this.output() to emit custom events to parent components. You can keep the emitter private.
import { Component, html } from '@mimopo/croqueta';
export class MyButton extends Component {
static tag = 'my-button';
private _clicked = this.output<void>('clicked');
protected render() {
return html`<button (click)="${() => this._clicked.emit()}">Click me</button>`;
}
}import { Component, html } from '@mimopo/croqueta';
export class MyButton extends Component {
static tag = 'my-button';
_clicked = this.output('clicked');
render() {
return html`<button (click)="${() => this._clicked.emit()}">Click me</button>`;
}
}
Two-way binding (input + output)
Use this.inputOutput() to create a property that is both an input and emits an [name]-change event when updated. The framework template system will handle the two-way binding automatically. You can keep the signal private.
If you call the regular set method, the signal will be updated but the parent component will not be notified. If you call the emit method, the signal will be updated and the parent component will be notified.
import { Component, html } from '@mimopo/croqueta';
class MyInput extends Component {
static tag = 'my-input';
private _value = this.inputOutput<string>('value', '');
protected render() {
return html`<input type="text" id="input" [value]="${this._value}" (keyup)="${(e: any) => this._value.emit(e.target.value)}" />`;
}
}import { Component, html } from '@mimopo/croqueta';
class MyInput extends Component {
static tag = 'my-input';
_value = this.inputOutput('value', '');
render() {
return html`<input type="text" id="input" [value]="${this._value}" (keyup)="${(e) => this._value.emit(e.target.value)}" />`;
}
}
Exporting as a Web Component
If you want to use your component as a standard web component (e.g., in a vanilla JS app or another framework), extend WebComponent instead. This class provides utilities for mapping HTML attributes to signals and how to convert them from string to the desired type and vice versa.
If you need to handle inputs, use attributeInput and add the attribute to observedAttributes.
import { WebComponent, html, computed } from '@mimopo/croqueta';
export class MyGreeter extends WebComponent {
static tag = 'my-greeter';
private _user = this.attributeInput({
name: 'user',
initialValue: { name: 'John' },
fromString: (v) => JSON.parse(v),
toString: (v) => JSON.stringify(v),
});
protected render() {
const name = computed(() => this._user.get().name);
return html`Hello ${name}`;
}
}import { WebComponent, html, computed } from '@mimopo/croqueta';
export class MyGreeter extends WebComponent {
static tag = 'my-greeter';
_user = this.attributeInput({
name: 'user',
initialValue: { name: 'John' },
fromString: (v) => JSON.parse(v),
toString: (v) => JSON.stringify(v),
});
render() {
const name = computed(() => this._user.get().name);
return html`Hello ${name}`;
}
}
If you need two-way data binding, use attributeInputOutput and add the attribute to observedAttributes.
import { WebComponent, html } from '@mimopo/croqueta';
export class MyCounter extends WebComponent {
static tag = 'my-counter';
static observedAttributes = ['count'];
private _count = this.attributeInputOutput({
name: 'count',
initialValue: 0,
fromString: (v) => Number(v),
toString: (v) => String(v),
});
protected render() {
return html`
<p>Current count is ${this._count}</p>
<button (click)="${() => this._count.set(this._count.get() + 1)}">Increment</button>
`;
}
}import { WebComponent, html } from '@mimopo/croqueta';
export class MyCounter extends WebComponent {
static tag = 'my-counter';
static observedAttributes = ['count'];
_count = this.attributeInputOutput({
name: 'count',
initialValue: 0,
fromString: (v) => Number(v),
toString: (v) => String(v),
});
render() {
return html`
<p>Current count is ${this._count}</p>
<button (click)="${() => this._count.set(this._count.get() + 1)}">Increment</button>
`;
}
}