Skip to content

Commit

Permalink
feat: implement Angular signals
Browse files Browse the repository at this point in the history
  • Loading branch information
akunzai committed Dec 27, 2024
1 parent 347f7c0 commit ea5b902
Show file tree
Hide file tree
Showing 15 changed files with 625 additions and 178 deletions.
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>
28 changes: 23 additions & 5 deletions src/app/counter/counter.component.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,34 @@
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';

@Component({
selector: 'app-counter',
templateUrl: './counter.component.html',
styleUrls: ['./counter.component.css'],
imports: [TranslateModule]
imports: [
TranslateModule,
NgIf
],
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
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);
}
}
57 changes: 57 additions & 0 deletions src/app/services/translation.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import { Injectable } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
providedIn: 'root'
})
export class TranslationService {
private currentLang = new BehaviorSubject<string>('en');
currentLang$ = this.currentLang.asObservable();

constructor(private translate: TranslateService) {
this.initializeLanguage();
}

private async initializeLanguage() {
// Set available languages
this.translate.addLangs(['en', 'zh-Hant']);
this.translate.setDefaultLang('en');

// Load saved language or detect browser language
const savedLang = localStorage.getItem('locale');
const langToUse = savedLang || this.getBrowserLanguage();

await this.setLanguage(langToUse);
}

private getBrowserLanguage(): string {
const browserLang = this.translate.getBrowserCultureLang();
return browserLang?.match(/zh/) ? 'zh-Hant' : 'en';
}

async setLanguage(lang: string): Promise<void> {
if (!this.translate.getLangs().includes(lang)) {
lang = 'en';
}

// Dynamically import translations
try {
const translations = await import(`../../locales/${lang}.json`);
this.translate.setTranslation(lang, translations.default);
await this.translate.use(lang).toPromise();
localStorage.setItem('locale', lang);
this.currentLang.next(lang);
} catch (error) {
console.error(`Failed to load translations for ${lang}`, error);
// Fallback to English
if (lang !== 'en') {
await this.setLanguage('en');
}
}
}

instant(key: string, params?: object): string {
return this.translate.instant(key, params);
}
}
Loading

0 comments on commit ea5b902

Please sign in to comment.