ng-hub-ui-accordion
Version:
A flexible, accessible, and customizable accordion component for Angular 19, part of the ng-hub-ui family.
282 lines (275 loc) • 15.2 kB
JavaScript
import { NgTemplateOutlet } from '@angular/common';
import * as i0 from '@angular/core';
import { Directive, input, model, contentChild, TemplateRef, output, Component, contentChildren, effect, forwardRef, HostBinding } from '@angular/core';
import { NG_VALUE_ACCESSOR } from '@angular/forms';
class AccordionPanelHeaderDirective {
constructor(templateRef) {
this.templateRef = templateRef;
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionPanelHeaderDirective, deps: [{ token: i0.TemplateRef }], target: i0.ɵɵFactoryTarget.Directive }); }
static { this.ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "14.0.0", version: "19.2.8", type: AccordionPanelHeaderDirective, isStandalone: true, selector: "[hubAccordionPanelHeader]", ngImport: i0 }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionPanelHeaderDirective, decorators: [{
type: Directive,
args: [{
selector: '[hubAccordionPanelHeader]'
}]
}], ctorParameters: () => [{ type: i0.TemplateRef }] });
/**
* A component that represents a single panel within an accordion.
* Each panel can be expanded or collapsed independently and contains a header and content section.
*
* @example
* ```html
* <hub-accordion-panel [title]="'Panel Title'" [value]="panelValue">
* <ng-template hubAccordionPanelHeader>Custom Header</ng-template>
* Panel Content
* </hub-accordion-panel>
* ```
*
* @implements {OnInit}
*
* @property {number} index - The position of this panel within its parent accordion
* @property {any} value - The value associated with this panel
* @property {WritableSignal<boolean>} collapsed - Signal controlling the panel's collapsed state
* @property {string} title - The title text displayed in the panel's header
* @property {TemplateRef<any>} headerTpt - Template reference for custom header content
*
* @emits {collapseEvent} collapsedChange - Fired when the panel's collapsed state changes
*/
class AccordionPanelComponent {
constructor() {
this.value = input();
/**
* Signal that controls the collapsed state of the accordion panel.
* When true, the panel is collapsed. When false, the panel is expanded.
* @default true
*/
this.collapsed = model(true);
/**
* The title of the accordion panel.
*
* @type {string}
*/
this.title = input();
/**
* Reference to the header template of the accordion panel.
* Uses contentChild query to find a template marked with AccordionPanelHeaderDirective.
* The template reference is read using TemplateRef.
*/
this.headerTpt = contentChild(AccordionPanelHeaderDirective, {
read: TemplateRef
});
/**
* Event emitter that fires when the panel's collapsed state changes.
* Emits a collapseEvent object containing the collapse state and animation details.
* @event
*/
this.collapsedChange = output();
}
/**
* Toggles the collapse state of the accordion panel and emits the change event.
* The event includes the panel's index and its new collapsed state.
*
* @emits collapsedChange - Emits an object containing the panel index and collapsed state
*/
toggleCollapse() {
this.collapsed.update((value) => !value);
this.collapsedChange.emit({
index: this.index,
collapsed: this.collapsed(),
uncollapsed: !this.collapsed(),
value: this.value()
});
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionPanelComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "19.2.8", type: AccordionPanelComponent, isStandalone: true, selector: "hub-accordion-panel", inputs: { value: { classPropertyName: "value", publicName: "value", isSignal: true, isRequired: false, transformFunction: null }, collapsed: { classPropertyName: "collapsed", publicName: "collapsed", isSignal: true, isRequired: false, transformFunction: null }, title: { classPropertyName: "title", publicName: "title", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { collapsed: "collapsedChange", collapsedChange: "collapsedChange" }, host: { classAttribute: "hub-accordion-panel" }, queries: [{ propertyName: "headerTpt", first: true, predicate: AccordionPanelHeaderDirective, descendants: true, read: TemplateRef, isSignal: true }], ngImport: i0, template: "<h2 class=\"hub-accordion-header\">\n\t<button\n\t\tclass=\"hub-accordion-button\"\n\t\ttype=\"button\"\n\t\t[class.collapsed]=\"collapsed()\"\n\t\t[attr.data-bs-target]=\"'#collapse' + index\"\n\t\t[attr.aria-expanded]=\"!collapsed()\"\n\t\t[attr.aria-controls]=\"'collapse' + index\"\n\t\t(click)=\"toggleCollapse()\"\n\t>\n\t\t@if (headerTpt()) {\n\t\t\t<ng-container [ngTemplateOutlet]=\"$any(headerTpt())\"></ng-container>\n\t\t} @else if (title()) {\n\t\t\t{{ title() }}\n\t\t}\n\t</button>\n</h2>\n<div\n\t[attr.id]=\"'collapse' + index\"\n\tclass=\"hub-accordion-collapse collapse\"\n\t[class.show]=\"!collapsed()\"\n>\n\t<div class=\"hub-accordion-body\">\n\t\t<ng-content></ng-content>\n\t</div>\n</div>\n", dependencies: [{ kind: "directive", type: NgTemplateOutlet, selector: "[ngTemplateOutlet]", inputs: ["ngTemplateOutletContext", "ngTemplateOutlet", "ngTemplateOutletInjector"] }] }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionPanelComponent, decorators: [{
type: Component,
args: [{ selector: 'hub-accordion-panel', imports: [NgTemplateOutlet], host: { class: 'hub-accordion-panel' }, template: "<h2 class=\"hub-accordion-header\">\n\t<button\n\t\tclass=\"hub-accordion-button\"\n\t\ttype=\"button\"\n\t\t[class.collapsed]=\"collapsed()\"\n\t\t[attr.data-bs-target]=\"'#collapse' + index\"\n\t\t[attr.aria-expanded]=\"!collapsed()\"\n\t\t[attr.aria-controls]=\"'collapse' + index\"\n\t\t(click)=\"toggleCollapse()\"\n\t>\n\t\t@if (headerTpt()) {\n\t\t\t<ng-container [ngTemplateOutlet]=\"$any(headerTpt())\"></ng-container>\n\t\t} @else if (title()) {\n\t\t\t{{ title() }}\n\t\t}\n\t</button>\n</h2>\n<div\n\t[attr.id]=\"'collapse' + index\"\n\tclass=\"hub-accordion-collapse collapse\"\n\t[class.show]=\"!collapsed()\"\n>\n\t<div class=\"hub-accordion-body\">\n\t\t<ng-content></ng-content>\n\t</div>\n</div>\n" }]
}] });
/**
* A component that implements an accordion UI pattern, allowing users to show and hide
* sections of related content. Supports single or multiple expanded panels and flush styling.
* Implements ControlValueAccessor for form integration.
*
* @example
* ```html
* <hub-accordion [multiple]="true" [options]="{flush: true}">
* <hub-accordion-panel>...</hub-accordion-panel>
* <hub-accordion-panel>...</hub-accordion-panel>
* </hub-accordion>
* ```
*
* @implements {ControlValueAccessor}
* @extends {SelectMultipleControlValueAccessor}
*/
class AccordionComponent {
constructor() {
/**
* The current value of the accordion component.
* An array that may contain any type of data.
*/
this.value = [];
/**
* Input binding that configures the property or attribute to bind to.
* Defines the property that will be bound to the value of the accordion.
* @default undefined
*/
this.bindValue = input();
/**
* Configuration options for the accordion component.
*
* @property {Object} options - The options object for configuring the accordion.
* @property {boolean} options.flush - When true, removes some borders and rounded corners to render
* accordions edge-to-edge with their parent container. Defaults to false.
*/
this.options = input({ flush: false });
/**
* Defines a comparison function to determine if two accordion items are equal.
* Used to track identity of objects in accordion selections.
*
* @param o1 First object to compare
* @param o2 Second object to compare
* @returns {boolean} True if objects are considered equal, false otherwise
* @defaultValue Default implementation uses strict equality (===)
*/
this.compareWith = input((o1, o2) => o1 === o2);
/**
* When true, expanding an accordion item will close all other expanded items.
* Defaults to false.
*/
this.multiple = input(false);
/**
* Collection of child AccordionPanelComponent instances within this accordion.
* Uses Angular's contentChildren query to obtain all panel components.
* @type {QueryList<AccordionPanelComponent>}
*/
this.panels = contentChildren(AccordionPanelComponent);
/**
* Effect that manages panel changes and their subscriptions.
* Initializes panel indices and subscribes to collapse events for each panel.
* This effect runs whenever the panels signal changes.
*
* For each panel:
* - Sets its index based on array position
* - Subscribes to panel collapse events to handle state changes
*/
this.panelsChangeEffect = effect(() => {
this.panels().forEach((panel, index) => {
panel.index = index;
panel.collapsedChange.subscribe((event) => {
this.handlePanelCollapse(event);
});
});
});
this.onChange = () => { };
this.onTouch = () => { };
}
/**
* Gets whether the accordion has the flush styling option enabled
* @returns {boolean} True if the flush option is enabled, false otherwise
*/
get haveFlushClass() {
return this.options().flush;
}
/**
* Implements the ControlValueAccessor interface to write a new value to the form control.
* If multiple selection is not enabled, wraps the value in an array.
* @param obj The value to be written to the form control
*/
writeValue(obj) {
if (!obj) {
this.value = [];
return;
}
this.value = !this.multiple() ? [obj] : obj;
this.handleValue();
}
/**
* Registers a callback function that is invoked when the control's value changes in the UI.
* This is part of the ControlValueAccessor interface implementation.
* @param fn - The callback function to register. This function will be called with the new value when the control's value changes.
*/
registerOnChange(fn) {
this.onChange = fn;
}
/**
* Registers a callback function that is called when the control receives a touch event.
* Part of the ControlValueAccessor interface implementation.
* @param fn - The callback function to register. Gets called when the control is touched.
*/
registerOnTouched(fn) {
this.onTouch = fn;
}
/**
* Updates the collapse state of all panels based on the current value.
* Each panel is collapsed if its value is not found in the accordion's value array using the compareWith function.
* @internal
*/
handleValue() {
for (const panel of this.panels()) {
panel.collapsed.set(!this.value.find((v) => this.compareWith()(v, panel.value())));
}
}
/**
* Handles the collapse/expand event of an accordion panel.
* If the panel is being expanded and multiple is enabled,
* all other panels will be collapsed.
*
* @param {CollapseEvent} param0 - Object containing panel index and collapse state
* @param {number} param0.index - The index of the panel being toggled
* @param {boolean} param0.collapsed - The new collapse state of the panel
* @param {boolean} param0.value - The panel value
*/
handlePanelCollapse({ index, collapsed, value }) {
if (!Array.isArray(this.value)) {
this.value = [];
}
if (collapsed) {
this.value = this.value.filter((item) => !this.compareWith()(item, value));
}
else {
this.value = !this.multiple() ? [value] : [...this.value, value];
}
if (!collapsed && !this.multiple()) {
for (const panel of this.panels()) {
if (panel.index === index) {
continue;
}
panel.collapsed.set(true);
}
}
this.onChange(!this.multiple() ? (this.value.at(0) ?? null) : this.value);
}
static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionComponent, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.2.0", version: "19.2.8", type: AccordionComponent, isStandalone: true, selector: "hub-accordion", inputs: { bindValue: { classPropertyName: "bindValue", publicName: "bindValue", isSignal: true, isRequired: false, transformFunction: null }, options: { classPropertyName: "options", publicName: "options", isSignal: true, isRequired: false, transformFunction: null }, compareWith: { classPropertyName: "compareWith", publicName: "compareWith", isSignal: true, isRequired: false, transformFunction: null }, multiple: { classPropertyName: "multiple", publicName: "multiple", isSignal: true, isRequired: false, transformFunction: null } }, host: { properties: { "class.hub-accordion-flush": "this.haveFlushClass" }, classAttribute: "hub-accordion" }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AccordionComponent),
multi: true
}
], queries: [{ propertyName: "panels", predicate: AccordionPanelComponent, isSignal: true }], ngImport: i0, template: "<ng-content></ng-content>\n" }); }
}
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.8", ngImport: i0, type: AccordionComponent, decorators: [{
type: Component,
args: [{ selector: 'hub-accordion', host: { class: 'hub-accordion' }, providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => AccordionComponent),
multi: true
}
], template: "<ng-content></ng-content>\n" }]
}], propDecorators: { haveFlushClass: [{
type: HostBinding,
args: ['class.hub-accordion-flush']
}] } });
/*
* Public API Surface of accordion
*/
/**
* Generated bundle index. Do not edit.
*/
export { AccordionComponent, AccordionPanelComponent, AccordionPanelHeaderDirective };
//# sourceMappingURL=ng-hub-ui-accordion.mjs.map