9.3 KiB
Native Web Components Implementation
This framework now uses native browser Web Components API with custom elements registration.
Key Features
- ✅ Uses
customElements.define()for component registration - ✅ Components extend native
HTMLElement - ✅ Automatic lifecycle management with
connectedCallbackanddisconnectedCallback - ✅ Safe registration with
tryRegister()method that prevents duplicate registration errors - ✅ Support for Shadow DOM, Emulated, and None encapsulation modes
How It Works
1. Component Definition
Components are defined using the @Component decorator, just like before:
import { Component } from '@quarc/core';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrl: './app.component.scss',
})
export class AppComponent {
constructor() {
// Your component logic
}
}
2. Native Custom Element Registration
The WebComponentFactory.tryRegister() method:
- Converts the selector (e.g.,
'app-root') to a valid custom element tag name (e.g.,'app-root') - Creates a custom element class that extends
HTMLElement - Registers it using
customElements.define() - Returns
trueif registration succeeds,falseif already registered - Catches and logs any registration errors
const success = WebComponentFactory.tryRegister(componentInstance);
// Returns false if already registered, preventing errors
3. Component Instantiation
Components can be created in three ways:
a) Auto-create and append to body
const webComponent = WebComponentFactory.create(componentInstance);
b) Create inside a parent element
const parent = document.getElementById('container');
const webComponent = WebComponentFactory.createInElement(componentInstance, parent);
c) Replace an existing element
const existingElement = document.querySelector('.my-element');
const webComponent = WebComponentFactory.createFromElement(componentInstance, existingElement);
4. Native Custom Elements in HTML
Once registered, components can be used directly in HTML:
<!DOCTYPE html>
<html>
<body>
<!-- Native custom element - automatically recognized by browser -->
<app-root></app-root>
<script src="main.js"></script>
</body>
</html>
API Reference
WebComponentFactory
registerWithDependencies(component: IComponent): boolean
Recursively registers a component and all its imported dependencies as native custom elements. This ensures that all child components are registered before the parent component renders.
@Component({
selector: 'parent-component',
template: '<child-component></child-component>',
imports: [ChildComponent], // Automatically registered before parent
})
export class ParentComponent {}
// All imports are registered recursively
WebComponentFactory.registerWithDependencies(parentComponentInstance);
How it works:
- Iterates through the
importsarray in component metadata - Creates instances of imported components using the Injector
- Recursively calls
registerWithDependencieson each imported component - Finally registers the parent component
This method is automatically called by create(), createInElement(), and createFromElement().
tryRegister(component: IComponent): boolean
Safely registers a single component as a native custom element without checking dependencies. Returns false if already registered.
const isNewRegistration = WebComponentFactory.tryRegister(myComponent);
Note: Use registerWithDependencies() instead if your component has imports.
create(component: IComponent, selector?: string): WebComponent
Creates and returns a web component instance. If no element with the selector exists, creates one and appends to body.
createInElement(component: IComponent, parent: HTMLElement): WebComponent
Creates a web component and appends it to the specified parent element.
createFromElement(component: IComponent, element: HTMLElement): WebComponent
Converts an existing HTML element to a web component or replaces it with the component's custom element.
isRegistered(selector: string): boolean
Checks if a component with the given selector is already registered.
getRegisteredTagName(selector: string): string | undefined
Returns the registered tag name for a selector, or undefined if not registered.
WebComponent Class
The WebComponent class now extends HTMLElement and implements:
Lifecycle Callbacks
connectedCallback()- Called when element is added to DOM (protected against multiple initialization)disconnectedCallback()- Called when element is removed from DOM
Note: The component uses an internal _initialized flag to prevent multiple renderings. Even if initialize() is called multiple times (e.g., from both setComponentInstance() and connectedCallback()), the component will only render once.
Methods
setComponentInstance(component: IComponent, componentType: ComponentType<IComponent>)- Sets the component instance and component type, then initializesgetComponentOptions()- Returns the component options from the static_quarcComponentpropertyrenderComponent()- Renders the component template and stylesgetHostElement()- Returns the element itself (since it IS the host)getShadowRoot()- Returns the shadow root if using Shadow DOM encapsulationgetAttributes()- Returns all attributes as an arraygetChildElements()- Returns all child elements with metadataquerySelector(selector)- Queries within the component's render targetquerySelectorAll(selector)- Queries all matching elementsdestroy()- Removes all child nodes
Component Dependencies & Import Registration
The framework automatically handles component dependencies through the imports property. When a component is registered, all its imported components are recursively registered first.
Example: Parent-Child Components
// Child component
@Component({
selector: 'user-card',
template: '<div class="card">User Info</div>',
style: '.card { border: 1px solid #ccc; }',
})
export class UserCardComponent {}
// Parent component
@Component({
selector: 'user-list',
template: `
<div>
<user-card></user-card>
<user-card></user-card>
</div>
`,
imports: [UserCardComponent], // Child component will be registered first
})
export class UserListComponent {}
// Bootstrap
Core.bootstrap(UserListComponent);
Registration Flow
- UserListComponent is bootstrapped
registerWithDependencies()is called- Framework finds
UserCardComponentin imports - UserCardComponent is registered as
<user-card>custom element - UserListComponent is registered as
<user-list>custom element - Template renders with
<user-card>elements working natively
Nested Dependencies
Dependencies can be nested multiple levels deep:
@Component({
selector: 'icon',
template: '<svg>...</svg>',
})
export class IconComponent {}
@Component({
selector: 'button',
template: '<button><icon></icon> Click</button>',
imports: [IconComponent],
})
export class ButtonComponent {}
@Component({
selector: 'form',
template: '<form><button></button></form>',
imports: [ButtonComponent], // IconComponent is also registered automatically
})
export class FormComponent {}
Registration order: IconComponent → ButtonComponent → FormComponent
Benefits
✅ Automatic dependency resolution - No manual registration needed ✅ Prevents registration errors - Components registered in correct order ✅ Supports deep nesting - Recursive registration handles any depth ✅ Lazy loading ready - Only registers components that are actually used ✅ Type-safe - TypeScript ensures imports are valid component types
Encapsulation Modes
ViewEncapsulation.ShadowDom
Uses native Shadow DOM for true style encapsulation:
@Component({
selector: 'my-component',
template: '<div>Content</div>',
style: 'div { color: red; }',
encapsulation: ViewEncapsulation.ShadowDom,
})
ViewEncapsulation.Emulated
Angular-style scoped attributes (_nghost-*, _ngcontent-*):
@Component({
selector: 'my-component',
template: '<div>Content</div>',
style: 'div { color: red; }',
encapsulation: ViewEncapsulation.Emulated, // Default
})
ViewEncapsulation.None
No encapsulation - styles are global:
@Component({
selector: 'my-component',
template: '<div>Content</div>',
style: 'div { color: red; }',
encapsulation: ViewEncapsulation.None,
})
Migration from Old System
The old system created wrapper <div> elements with data-component attributes. The new system:
- ✅ Registers actual custom elements with the browser
- ✅ Uses native lifecycle callbacks
- ✅ Allows direct HTML usage without JavaScript
- ✅ Better performance and browser integration
- ✅ Proper custom element naming (must contain hyphen)
Browser Compatibility
Native Web Components are supported in all modern browsers:
- Chrome 54+
- Firefox 63+
- Safari 10.1+
- Edge 79+
For older browsers, polyfills may be required.