import { animate, state, style, transition, trigger } from '@angular/animations';
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output } from '@angular/core';
import { IconComponent } from '../icon/icon.component';
@Component({
selector: 'app-accordion',
standalone: true,
imports: [CommonModule, IconComponent],
templateUrl: './accordion.component.html',
styleUrl: './accordion.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
trigger('slideToggle', [
state('void', style({ height: '0px', overflow: 'hidden' })),
state('active', style({ height: '*', overflow: 'hidden' })),
transition('void <=> active', animate('300ms ease-in-out'))
])
]
})
export class AccordionComponent {
@Input() public title: string;
@Input() public desc: string;
@Input() public icon: string = 'icon-chevron-down';
@Input() public isActive: boolean;
@Output() public toggled = new EventEmitter<void>();
public toggleAccordion(): void {
this.toggled.emit();
}
}
<div class="accordion">
<div class="accordion-item">
<div class="accordion-item-title" (click)="toggleAccordion()">
<div class="accordion-item-title-text" [ngClass]="{ active: isActive }">
{{ title }}
</div>
<div class="accordion-icon" [ngClass]="{ active: isActive }">
<app-icon [icon]="icon" [size]="24" />
</div>
</div>
<div class="accordion-item-panel" [@slideToggle]="isActive ? 'active' : 'void'">
<div class="accordion-item-panel-content" [innerHTML]="desc"></div>
</div>
</div>
</div>
import { Injectable } from '@angular/core';
import { Card } from '@app/shared';
@Injectable({
providedIn: 'root'
})
export class FaqService {
public getData(): readonly Card[] {
return [
{ title: 'FAQ_1.Q', desc: 'FAQ_1.A' },
{ title: 'FAQ_2.Q', desc: 'FAQ_2.A' },
{ title: 'FAQ_3.Q', desc: 'FAQ_3.A' },
];
}
}
import { Link } from './link.model';
export type Card = Readonly<{
readonly title: string;
readonly desc?: string;
readonly cta?: Link;
}>;
import { CommonModule } from '@angular/common';
import { ChangeDetectionStrategy, Component, inject, OnInit } from '@angular/core';
import { AccordionComponent, Card } from '@app/shared';
import { TranslocoPipe } from '@jsverse/transloco';
import { FaqService } from '../../shared/services/faq.service';
@Component({
selector: 'app-faq',
standalone: true,
imports: [CommonModule, TranslocoPipe, AccordionComponent],
templateUrl: './faq.component.html',
styleUrl: './faq.component.scss',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FaqComponent implements OnInit {
private readonly _faqService = inject(FaqService);
public activeAccordionIndex: number | null = null;
public data: readonly Card[];
public ngOnInit(): void {
this.data = this._faqService.getData();
}
public onAccordionToggle(index: number): void {
if (this.activeAccordionIndex === index) {
this.activeAccordionIndex = null;
} else {
this.activeAccordionIndex = index;
}
}
}
<section class="faq">
<div class="container">
<div class="heading">
<h2 class="heading-2">{{ 'FAQ.TITLE' | transloco }}</h2>
<p class="subtitle">{{ 'FAQ.SUBTITLE' | transloco }}</p>
</div>
<div class="accordion">
<ng-container *ngFor="let item of data; let i = index">
<app-accordion
[title]="item.title | transloco"
[desc]="item.desc | transloco"
[isActive]="activeAccordionIndex === i"
(toggled)="onAccordionToggle(i)"
/>
</ng-container>
</div>
</div>
</section>
@use 'shared' as *;
:host {
display: block;
}
.accordion {
width: 100%;
.grid-2 {
gap: 80px;
}
.title.desktop {
display: none;
@include media-breakpoint-up(md) {
display: block;
}
}
.title.mobile {
display: block;
margin-bottom: -40px;
@include media-breakpoint-up(md) {
display: none;
}
}
&-caption {
order: 2;
@include media-breakpoint-up(md) {
order: 1;
}
.title {
margin-bottom: 80px;
}
}
}
.accordion {
&-icon {
width: 24px;
height: 24px;
transition: transform 0.3s ease-in-out;
&.active {
transform: rotate(180deg);
}
}
&-item {
background-color: var(--color-white);
border: 1px solid var(--color-grey-300);
border-radius: 8px;
margin-bottom: 16px;
&-title {
display: flex;
align-items: center;
justify-content: space-between;
cursor: pointer;
padding: 16px;
&-text {
color: var(--color-primary-950);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 120%;
max-width: 94%;
}
}
&-panel {
overflow: hidden;
color: var(--color-primary-950);
font-size: 14px;
font-style: normal;
font-weight: 400;
line-height: 120%;
&-content {
padding: 0 16px 16px;
}
&.active {
border-top: 1px solid var(--color-grey-300);
}
}
}
}
main.ts
import { bootstrapApplication } from '@angular/platform-browser';
import { provideAnimations } from '@angular/platform-browser/animations';
import { AppComponent } from './app/app.component';
import { appConfig } from './app/app.config';
bootstrapApplication(AppComponent, {
...appConfig,
providers: [provideAnimations(), ...(appConfig.providers || [])]
}).catch((err) => console.error(err));