Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement Angular signals #939

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
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
4 changes: 2 additions & 2 deletions angular.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@
"scripts": true,
"styles": {
"minify": true,
"inlineCritical": false
"inlineCritical": true
},
"fonts": false
"fonts": true
},
"outputHashing": "all",
"sourceMap": false,
Expand Down
25 changes: 7 additions & 18 deletions src/app/app.component.ts
Original file line number Diff line number Diff line change
@@ -1,29 +1,18 @@
import { Component } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { NavMenuComponent } from './nav-menu/nav-menu.component';
import enTranslations from '../locales/en.json';
import zhHantTranslations from '../locales/zh-Hant.json';
import { TranslationService } from './services/translation.service';

@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
imports: [NavMenuComponent, RouterOutlet]
imports: [NavMenuComponent, RouterOutlet],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent {
title = document.title;
constructor(public translate: TranslateService) {
translate.setTranslation('en', enTranslations);
translate.setTranslation('zh-Hant', zhHantTranslations);
translate.addLangs(['en', 'zh-Hant']);
translate.setDefaultLang('en');
const locale = localStorage.getItem('locale');
if (locale !== null) {
translate.use(locale);
} else {
const browserLang = translate.getBrowserCultureLang();
translate.use(browserLang?.match(/zh/) ? 'zh-Hant' : 'en');
}
}

constructor(private translationService: TranslationService) {}
}
38 changes: 31 additions & 7 deletions src/app/counter/counter.component.html
Original file line number Diff line number Diff line change
@@ -1,9 +1,33 @@
<h1>{{ "Counter" | translate }}</h1>
<div class="counter-container">
<h1>{{ "Counter" | translate }}</h1>

<p aria-live="polite">
{{ "Current count" | translate }}: <strong>{{ currentCount }}</strong>
</p>
<p aria-live="polite" class="mt-3">
{{ "Current count" | translate }}: <strong>{{ currentCount() }}</strong>
</p>

<button class="btn btn-primary" (click)="incrementCounter()">
{{ "Increment" | translate }}
</button>
<div class="btn-group" [attr.aria-label]="'COUNTER.CONTROLS' | translate">
<button
class="btn btn-primary"
(click)="incrementCounter()"
[attr.aria-label]="'COUNTER.INCREMENT_ARIA' | translate"
>
<i class="bi bi-plus-lg" aria-hidden="true"></i>
{{ "Increment" | translate }}
</button>

<button
class="btn btn-secondary ms-2"
(click)="resetCounter()"
[attr.aria-label]="'COUNTER.RESET_ARIA' | translate"
>
<i class="bi bi-arrow-counterclockwise" aria-hidden="true"></i>
{{ "COUNTER.RESET" | translate }}
</button>
</div>

<div class="mt-3">
<small class="text-muted">
{{ "COUNTER.KEYBOARD_HINT" | translate }}
</small>
</div>
</div>
2 changes: 1 addition & 1 deletion src/app/counter/counter.component.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ test('should render counter with 0', async () => {
});

test('should increment the counter on click', async () => {
fireEvent.click(screen.getByRole('button'));
fireEvent.click(screen.getByRole('button', { name: /Increment/i }));
expect(screen.getByText(/Current count:/).textContent).toContain(
'Current count: 1'
);
Expand Down
27 changes: 22 additions & 5 deletions src/app/counter/counter.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,33 @@
import { Component } from '@angular/core';
import { ChangeDetectionStrategy, Component, HostListener, computed, signal } from '@angular/core';
import { TranslateModule } from '@ngx-translate/core';
import { NgIf } from '@angular/common';

Check failure on line 3 in src/app/counter/counter.component.ts

View workflow job for this annotation

GitHub Actions / build (18)

'NgIf' is defined but never used

Check failure on line 3 in src/app/counter/counter.component.ts

View workflow job for this annotation

GitHub Actions / build (20)

'NgIf' is defined but never used

Check failure on line 3 in src/app/counter/counter.component.ts

View workflow job for this annotation

GitHub Actions / build (22)

'NgIf' is defined but never used

@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css'],
imports: [TranslateModule]
imports: [
TranslateModule
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class CounterComponent {
public currentCount = 0;
private readonly count = signal(0);
readonly currentCount = computed(() => this.count());

public incrementCounter() : void {
this.currentCount++;
@HostListener('window:keydown.space', ['$event'])
@HostListener('window:keydown.enter', ['$event'])
onKeyPress(event: KeyboardEvent): void {
event.preventDefault();
this.incrementCounter();
}

incrementCounter(): void {
this.count.update(value => value + 1);
}

resetCounter(): void {
this.count.set(0);
}
}
70 changes: 43 additions & 27 deletions src/app/nav-menu/nav-menu.component.html
Original file line number Diff line number Diff line change
@@ -1,79 +1,95 @@
<header>
<header role="banner">
<nav
class="
navbar navbar-expand-sm navbar-toggleable-sm navbar-light
bg-white
border-bottom
box-shadow
mb-3
"
class="navbar navbar-expand-sm navbar-toggleable-sm navbar-light bg-white border-bottom box-shadow mb-3"
role="navigation"
>
<div class="container">
<a class="navbar-brand" [routerLink]="['/']">{{ title }}</a>
<a class="navbar-brand" [routerLink]="['/']" [attr.aria-label]="title">{{ title }}</a>
<button
class="navbar-toggler"
type="button"
data-toggle="collapse"
data-target=".navbar-collapse"
aria-label="Toggle navigation"
[attr.aria-expanded]="!isCollapsed"
[attr.aria-label]="'NAV.TOGGLE_MENU' | translate"
[attr.aria-expanded]="!collapsed"
(click)="toggleCollapsed()"
>
<span class="navbar-toggler-icon"></span>
</button>
<div
class="navbar-collapse collapse d-sm-inline-flex justify-content-end"
role="menu"
[ngClass]="{ show: !isCollapsed }"
[attr.aria-expanded]="!collapsed"
[ngClass]="{ show: !collapsed }"
>
<ul class="navbar-nav flex-grow">
<li
class="nav-item"
>
<a class="nav-link" [routerLink]="['/']" [routerLinkActive]="['active']" [routerLinkActiveOptions]="{ exact: true }">Home</a>
<li class="nav-item">
<a
class="nav-link"
[routerLink]="['/']"
[routerLinkActive]="['active']"
[routerLinkActiveOptions]="{ exact: true }"
[attr.aria-label]="'NAV.HOME' | translate"
>
{{ 'NAV.HOME' | translate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['/counter']" [routerLinkActive]="['active']" >Counter</a
<a
class="nav-link"
[routerLink]="['/counter']"
[routerLinkActive]="['active']"
[attr.aria-label]="'NAV.COUNTER' | translate"
>
{{ 'NAV.COUNTER' | translate }}
</a>
</li>
<li class="nav-item">
<a class="nav-link" [routerLink]="['/todo-list']" [routerLinkActive]="['active']">Todo</a>
<a
class="nav-link"
[routerLink]="['/todo-list']"
[routerLinkActive]="['active']"
[attr.aria-label]="'NAV.TODO' | translate"
>
{{ 'NAV.TODO' | translate }}
</a>
</li>
<li class="nav-item dropdown">
<button
class="btn dropdown-toggle"
id="i18nDropdown"
data-bs-toggle="dropdown"
data-bs-auto-close="true"
aria-label="Toggle Languages"
[attr.aria-expanded]="isExpanded"
[attr.aria-label]="'NAV.LANGUAGE_SELECTOR' | translate"
[attr.aria-expanded]="expanded"
(click)="toggleExpanded()"
>
<i class="bi bi-globe"></i>
<i class="bi bi-globe" aria-hidden="true"></i>
</button>
<ul appClickOutside
class="dropdown-menu"
[ngClass]="{ show: isExpanded }"
[ngClass]="{ show: expanded }"
aria-labelledby="i18nDropdown"
(clickOutside)="onOutsideClick()"
[exclude]="'button.dropdown-toggle'"
>
<li>
<button
class="dropdown-item"
[ngClass]="{ active: isCurrentLanguage('^en') }"
[ngClass]="{ active: (currentLang$ | async) === 'en' }"
(click)="switchLanguage('en')"
[attr.aria-label]="'LANGUAGES.ENGLISH' | translate"
>
English
{{ 'LANGUAGES.ENGLISH' | translate }}
</button>
</li>
<li>
<button
class="dropdown-item"
[ngClass]="{ active: isCurrentLanguage('^zh') }"
[ngClass]="{ active: (currentLang$ | async) === 'zh-Hant' }"
(click)="switchLanguage('zh-Hant')"
[attr.aria-label]="'LANGUAGES.CHINESE' | translate"
>
中文(繁體)
{{ 'LANGUAGES.CHINESE' | translate }}
</button>
</li>
</ul>
Expand Down
31 changes: 21 additions & 10 deletions src/app/nav-menu/nav-menu.component.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,13 @@ import {
TranslateModule,
TranslateService
} from '@ngx-translate/core';
import { fireEvent, render, screen } from '@testing-library/angular';
import { fireEvent, render, screen, waitFor } from '@testing-library/angular';
import { NavMenuComponent } from './nav-menu.component';

let translate;

beforeEach(async () => {
await render(NavMenuComponent, {
const { fixture } = await render(NavMenuComponent, {
componentProperties: {
title: 'Test',
},
Expand All @@ -20,23 +22,32 @@ beforeEach(async () => {
],
providers: [TranslateService, provideRouter([])],
});
translate = fixture.debugElement.injector.get(TranslateService);
translate.setTranslation('en', {
'NAV.TOGGLE_MENU': 'Toggle navigation menu',
'NAV.LANGUAGE_SELECTOR': 'Select language',
'LANGUAGES.ENGLISH': 'English',
'LANGUAGES.CHINESE': '中文(繁體)',
});
translate.use('en');
});

test('should render with title: Test', async () => {
expect(await screen.findByText('Test')).toBeInTheDocument();
});

test('support to toggle navigation', async () => {
const navbar = await screen.findByRole('menu');
expect(navbar.getAttribute('class')).not.toContain('show');
fireEvent.click(screen.getByRole('button', { name: /Toggle navigation/i }));
expect(navbar.getAttribute('class')).toContain('show');
const navbar = screen.getByRole('navigation');
const navbarCollapse = navbar.querySelector('.navbar-collapse');
expect(navbarCollapse.getAttribute('class')).not.toContain('show');
fireEvent.click(screen.getByRole('button', { name: /NAV.TOGGLE_MENU/i }));
expect(navbarCollapse.getAttribute('class')).toContain('show');
});

test('support to switch languages', async () => {
fireEvent.click(await screen.findByRole('button', { name: /Toggle Languages/i }));
fireEvent.click(screen.getByRole('button', { name: /English/i }));
expect(localStorage.getItem('locale')).toBe('en');
fireEvent.click(screen.getByRole('button', { name: /中文/i }));
expect(localStorage.getItem('locale')).toBe('zh-Hant');
fireEvent.click(screen.getByRole('button', { name: /LANGUAGES.CHINESE/i }));
await waitFor(() => {
expect(localStorage.getItem('locale')).toBe('zh-Hant');
});
});
54 changes: 36 additions & 18 deletions src/app/nav-menu/nav-menu.component.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,63 @@
import { Component, Input } from '@angular/core';
import { TranslateService, TranslationChangeEvent } from '@ngx-translate/core';
import { ChangeDetectionStrategy, Component, Input, signal } from '@angular/core';
import { ClickOutsideDirective } from '../click-outside.directive';
import { NgClass } from '@angular/common';
import { AsyncPipe, NgClass } from '@angular/common';
import { RouterLink, RouterLinkActive } from '@angular/router';
import { TranslationService } from '../services/translation.service';
import { TranslateModule } from '@ngx-translate/core';

type Language = 'en' | 'zh-Hant';

@Component({
selector: 'app-nav-menu',
templateUrl: './nav-menu.component.html',
styleUrls: ['./nav-menu.component.css'],
imports: [RouterLink, NgClass, RouterLinkActive, ClickOutsideDirective]
imports: [
RouterLink,
NgClass,
RouterLinkActive,
ClickOutsideDirective,
AsyncPipe,
TranslateModule
],
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class NavMenuComponent {
@Input() title: string | undefined;

isCollapsed = true;
isExpanded = false;
private readonly isCollapsed = signal(true);
private readonly isExpanded = signal(false);

readonly currentLang$ = this.translationService.currentLang$;

constructor(private translationService: TranslationService) {}

constructor(private translate: TranslateService) {
translate.onLangChange.subscribe((event: TranslationChangeEvent) => {
localStorage.setItem('locale', event.lang);
});
get collapsed(): boolean {
return this.isCollapsed();
}

get expanded(): boolean {
return this.isExpanded();
}

toggleCollapsed(): void {
this.isCollapsed = !this.isCollapsed;
this.isCollapsed.update(value => !value);
}

toggleExpanded(): void {
this.isExpanded = !this.isExpanded;
this.isExpanded.update(value => !value);
}

onOutsideClick(): void {
this.isExpanded = false;
this.isExpanded.set(false);
}

isCurrentLanguage(pattern: string): boolean {
return new RegExp(pattern).test(this.translate.currentLang);
return new RegExp(pattern).test(this.translationService.instant('LANGUAGE'));
}

switchLanguage = (lang: string): void => {
this.translate.use(lang);
this.isExpanded = false;
};
switchLanguage(lang: Language): void {
this.translationService.setLanguage(lang);
this.isExpanded.set(false);
}
}
Loading
Loading