This commit is contained in:
Federico Volpini 2025-12-10 17:51:46 +01:00
parent 252d6786af
commit 6c903fc03b
225 changed files with 24604 additions and 0 deletions

View File

@ -0,0 +1,17 @@
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.ts]
quote_type = single
ij_typescript_use_double_quotes = false
[*.md]
max_line_length = off
trim_trailing_whitespace = false

View File

@ -0,0 +1,44 @@
# See https://docs.github.com/get-started/getting-started-with-git/ignoring-files for more about ignoring files.
# Compiled output
/dist
/tmp
/out-tsc
/bazel-out
# Node
/node_modules
npm-debug.log
yarn-error.log
# IDEs and editors
.idea/
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# Visual Studio Code
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
.history/*
# Miscellaneous
/.angular/cache
.sass-cache/
/connect.lock
/coverage
/libpeerconnection.log
testem.log
/typings
__screenshots__/
**/exercises
# System files
.DS_Store
Thumbs.db

View File

@ -0,0 +1,4 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
"recommendations": ["angular.ng-template"]
}

View File

@ -0,0 +1,20 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "ng serve",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: start",
"url": "http://localhost:4200/"
},
{
"name": "ng test",
"type": "chrome",
"request": "launch",
"preLaunchTask": "npm: test",
"url": "http://localhost:9876/debug.html"
}
]
}

View File

@ -0,0 +1,42 @@
{
// For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "start",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
},
{
"type": "npm",
"script": "test",
"isBackground": true,
"problemMatcher": {
"owner": "typescript",
"pattern": "$tsc",
"background": {
"activeOnStart": true,
"beginsPattern": {
"regexp": "(.*?)"
},
"endsPattern": {
"regexp": "bundle generation complete"
}
}
}
}
]
}

View File

@ -0,0 +1,59 @@
# Samples20
This project was generated using [Angular CLI](https://github.com/angular/angular-cli) version 20.3.9.
## Development server
To start a local development server, run:
```bash
ng serve
```
Once the server is running, open your browser and navigate to `http://localhost:4200/`. The application will automatically reload whenever you modify any of the source files.
## Code scaffolding
Angular CLI includes powerful code scaffolding tools. To generate a new component, run:
```bash
ng generate component component-name
```
For a complete list of available schematics (such as `components`, `directives`, or `pipes`), run:
```bash
ng generate --help
```
## Building
To build the project run:
```bash
ng build
```
This will compile your project and store the build artifacts in the `dist/` directory. By default, the production build optimizes your application for performance and speed.
## Running unit tests
To execute unit tests with the [Karma](https://karma-runner.github.io) test runner, use the following command:
```bash
ng test
```
## Running end-to-end tests
For end-to-end (e2e) testing, run:
```bash
ng e2e
```
Angular CLI does not come with an end-to-end testing framework by default. You can choose one that suits your needs.
## Additional Resources
For more information on using the Angular CLI, including detailed command references, visit the [Angular CLI Overview and Command Reference](https://angular.dev/tools/cli) page.

View File

@ -0,0 +1,87 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"newProjectRoot": "projects",
"projects": {
"samples-20": {
"projectType": "application",
"schematics": {},
"root": "",
"sourceRoot": "src",
"prefix": "app",
"architect": {
"build": {
"builder": "@angular/build:application",
"options": {
"browser": "src/main.ts",
"tsConfig": "tsconfig.app.json",
"assets": [
"src/favicon.ico",
"src/assets",
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
},
"configurations": {
"production": {
"budgets": [
{
"type": "initial",
"maximumWarning": "500kB",
"maximumError": "1MB"
},
{
"type": "anyComponentStyle",
"maximumWarning": "4kB",
"maximumError": "8kB"
}
],
"outputHashing": "all"
},
"development": {
"optimization": false,
"extractLicenses": false,
"sourceMap": true
}
},
"defaultConfiguration": "production"
},
"serve": {
"builder": "@angular/build:dev-server",
"configurations": {
"production": {
"buildTarget": "samples-20:build:production"
},
"development": {
"buildTarget": "samples-20:build:development"
}
},
"defaultConfiguration": "development"
},
"extract-i18n": {
"builder": "@angular/build:extract-i18n"
},
"test": {
"builder": "@angular/build:karma",
"options": {
"tsConfig": "tsconfig.spec.json",
"assets": [
{
"glob": "**/*",
"input": "public"
}
],
"styles": [
"src/styles.css"
]
}
}
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,50 @@
{
"name": "samples-20",
"version": "0.0.0",
"scripts": {
"ng": "ng",
"start": "ng serve",
"build": "ng build",
"watch": "ng build --watch --configuration development",
"test": "ng test"
},
"prettier": {
"printWidth": 100,
"singleQuote": true,
"overrides": [
{
"files": "*.html",
"options": {
"parser": "angular"
}
}
]
},
"private": true,
"dependencies": {
"@angular/common": "^20.3.0",
"@angular/compiler": "^20.3.0",
"@angular/core": "^20.3.0",
"@angular/forms": "^20.3.0",
"@angular/platform-browser": "^20.3.0",
"@angular/router": "^20.3.0",
"bootstrap": "^5.3.8",
"date-fns": "^4.1.0",
"ngx-toastr": "^19.1.0",
"rxjs": "~7.8.0",
"tslib": "^2.3.0"
},
"devDependencies": {
"@angular/build": "^20.3.9",
"@angular/cli": "^20.3.9",
"@angular/compiler-cli": "^20.3.0",
"@types/jasmine": "~5.1.0",
"jasmine-core": "~5.9.0",
"karma": "~6.4.0",
"karma-chrome-launcher": "~3.2.0",
"karma-coverage": "~2.2.0",
"karma-jasmine": "~5.1.0",
"karma-jasmine-html-reporter": "~2.1.0",
"typescript": "~5.9.2"
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@ -0,0 +1,26 @@
import {
ApplicationConfig,
InjectionToken,
provideBrowserGlobalErrorListeners,
provideZonelessChangeDetection,
} from '@angular/core';
import { provideRouter } from '@angular/router';
import { routes } from './app.routes';
import { provideHttpClient, withInterceptors } from '@angular/common/http';
import { authInterceptor } from './interceptors/auth-interceptor';
import { loggingInterceptor } from './interceptors/logging-interceptor';
import { TasksService } from './samples/tasks/tasks.service';
import { taskStatusOptionsProvider } from './samples/tasks/task.model';
export const TasksServiceToken = new InjectionToken<TasksService>('tasks-service-token');
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZonelessChangeDetection(),
provideRouter(routes),
provideHttpClient(withInterceptors([authInterceptor, loggingInterceptor])),
taskStatusOptionsProvider,
],
};

View File

@ -0,0 +1 @@
<router-outlet />

View File

@ -0,0 +1,155 @@
import { Routes } from '@angular/router';
import { Homepage } from './homepage/homepage';
import { Counters } from './samples/counters/counters';
import { Users } from './samples/users/users';
import { ForSample } from './samples/for-sample/for-sample';
import { IfSample } from './samples/if-sample/if-sample';
import { DynamicStyling } from './samples/dynamic-styling/dynamic-styling';
import { CommonPipes } from './samples/common-pipes/common-pipes';
import { CustomPipe } from './samples/custom-pipe/custom-pipe';
import { Parent } from './exercises/exercise-1/parent/parent';
import { Exercise2 } from './exercises/exercise-2/exercise-2';
import { Exercise3 } from './exercises/exercise-3/exercise-3';
import { Exercise4 } from './exercises/exercise-4/exercise-4';
import { Exercise5 } from './exercises/exercise-5/exercise-5';
import { Exercise6 } from './exercises/exercise-6/exercise-6';
import { InputSample } from './samples/input-sample/input-sample';
import { LoginForm } from './samples/login-form/login-form';
import { ReactiveLoginForm } from './samples/reactive-login-form/reactive-login-form';
import { CustomValidatedForm } from './samples/custom-validated-form/custom-validated-form';
import { LifeCycleSample } from './samples/life-cycle-sample/life-cycle-sample';
import { Exercise7 } from './exercises/exercise-7/exercise-7';
import { NestedFormSample } from './samples/nested-form-sample/nested-form-sample';
import { UserForm } from './samples/nested-form-advanced/user-form/user-form';
import { DirectivesSample } from './samples/directives-sample/directives-sample';
import { Exercise8 } from './exercises/exercise-8/exercise-8';
import { Tasks } from './samples/tasks/tasks';
import { CustomersList } from './samples/customers-list/customers-list';
import { Exercise9 } from './exercises/exercise-9/exercise-9';
import { ObservableSample } from './samples/observable-sample/observable-sample';
import { Exercise10 } from './exercises/exercise-10/exercise-10';
import { ObservableSubject } from './samples/observable-subject/observable-subject';
export const routes: Routes = [
{
component: Homepage,
path: '',
},
{
component: Counters,
path: 'counters',
},
{
component: Users,
path: 'users',
},
{
component: Parent,
path: 'exercise-1',
},
{
component: Exercise2,
path: 'exercise-2',
},
{
component: Exercise3,
path: 'exercise-3',
},
{
component: ForSample,
path: 'for-sample',
},
{
component: IfSample,
path: 'if-sample',
},
{
component: DynamicStyling,
path: 'dynamic-styling',
},
{
component: Exercise4,
path: 'exercise-4',
},
{
component: CommonPipes,
path: 'common-pipes',
},
{
component: CustomPipe,
path: 'custom-pipe',
},
{
component: Exercise5,
path: 'exercise-5',
},
{
component: InputSample,
path: 'input-sample',
},
{
component: Exercise6,
path: 'exercise-6',
},
{
component: LoginForm,
path: 'login-sample',
},
{
component: ReactiveLoginForm,
path: 'reactive-login-sample',
},
{
component: CustomValidatedForm,
path: 'custom-validated-form',
},
{
component: NestedFormSample,
path: 'nested-form-sample',
},
{
component: UserForm,
path: 'nested-form-advanced-sample',
},
{
component: Exercise7,
path: 'exercise-7',
},
{
component: LifeCycleSample,
path: 'life-cycle-sample',
},
{
component: DirectivesSample,
path: 'directive-sample',
},
{
component: Exercise8,
path: 'exercise-8',
},
{
component: Tasks,
path: 'tasks',
},
{
component: Exercise9,
path: 'exercise-9',
},
{
component: CustomersList,
path: 'customers-list',
},
{
component: Exercise10,
path: 'exercise-10',
},
{
component: ObservableSample,
path: 'observable-sample',
},
{
component: ObservableSubject,
path: 'observable-subject',
}
];

View File

@ -0,0 +1,25 @@
import { provideZonelessChangeDetection } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import { App } from './app';
describe('App', () => {
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [App],
providers: [provideZonelessChangeDetection()]
}).compileComponents();
});
it('should create the app', () => {
const fixture = TestBed.createComponent(App);
const app = fixture.componentInstance;
expect(app).toBeTruthy();
});
it('should render title', () => {
const fixture = TestBed.createComponent(App);
fixture.detectChanges();
const compiled = fixture.nativeElement as HTMLElement;
expect(compiled.querySelector('h1')?.textContent).toContain('Hello, samples-20');
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { RouterOutlet } from '@angular/router';
@Component({
selector: 'app-root',
imports: [RouterOutlet],
templateUrl: './app.html',
styleUrl: './app.css'
})
export class App {
title = 'samples-20';
}

View File

@ -0,0 +1,9 @@
import { Phone, PhoneType } from "../types/phone";
export const DUMMY_PHONES: Phone[] = [
{ id: 1, number: "333-1234567", type: PhoneType.partner },
{ id: 2, number: "02-7654321", type: PhoneType.home },
{ id: 3, number: "333-9876543", type: PhoneType.work },
{ id: 4, number: "06-1234567", type: PhoneType.home },
{ id: 5, number: "333-5555555", type: PhoneType.work },
]

View File

@ -0,0 +1,9 @@
import { Product } from "../types/product";
export const DUMMY_PRODUCTS: Product[] = [
{ id: 1, name: 'Laptop', price: 999.99, stock: 200 },
{ id: 2, name: 'Smartphone', price: 699.99, stock: 1 },
{ id: 3, name: 'Tablet', price: 399.99, stock: 10 },
{ id: 4, name: 'Headphones', price: 199.99, stock: 0 },
{ id: 5, name: 'Smartwatch', price: 299.99, stock: 1988 },
];

View File

@ -0,0 +1,36 @@
import { User } from '../types/user';
export const DUMMY_USERS: User[] = [
{
id: 1,
name: 'Bob',
email: 'bob@gmail.com',
avatar: 'avatar1.jpg',
isAdmin: false,
isGuest: false,
},
{
id: 2,
name: 'Alice',
email: 'alice@gmail.com',
avatar: 'avatar2.png',
isAdmin: false,
isGuest: false,
},
{
id: 3,
name: 'Jhon',
email: 'jhon@gmail.com',
avatar: 'avatar3.jpg',
isAdmin: false,
isGuest: true,
},
{
id: 4,
name: 'Paul',
email: 'paul@gmail.com',
avatar: 'avatar4.png',
isAdmin: true,
isGuest: false,
},
];

View File

@ -0,0 +1,50 @@
<div class="container mt-4">
<form #customerForm="ngForm" (ngSubmit)="onSubmit(customerForm)">
<div class="mb-3">
<label for="firstname" class="form-label">First Name</label>
<!-- is invalid da applicare solo se
vuoto(quindi con errore -> required)
e submitted
-->
<input
type="text"
class="form-control"
[class.is-invalid]="customerForm.submitted && hasErrors(firstname)"
name="firstname"
id="firstname"
placeholder="Insert First Name"
required
#firstname="ngModel"
ngModel
/>
@if(customerForm.submitted && firstname.errors?.['required']) {
<small class="form-text text-danger">First Name Required.</small>
}
</div>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input
type="email"
class="form-control"
name="email"
id="email"
[class.is-invalid]="customerForm.submitted && hasErrors(email)"
email
required
placeholder="Insert your email"
ngModel
#email="ngModel"
/>
@if(customerForm.submitted) {
@if(email.errors?.["required"]) {
<small id="helpId" class="form-text text-danger">Email is required.</small>
}@if(email.errors?.["email"]) {
<small id="helpId" class="form-text text-danger">Email is invalid.</small>
}
}
</div>
<button type="submit" [disabled]="customerForm.invalid" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-warning">Reset</button>
</form>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { TemplateDrivenForm } from './template-driven-form';
describe('TemplateDrivenForm', () => {
let component: TemplateDrivenForm;
let fixture: ComponentFixture<TemplateDrivenForm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [TemplateDrivenForm]
})
.compileComponents();
fixture = TestBed.createComponent(TemplateDrivenForm);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { FormsModule, NgForm, NgModel, ValidationErrors } from '@angular/forms';
@Component({
selector: 'app-template-driven-form',
imports: [FormsModule],
templateUrl: './template-driven-form.html',
styleUrl: './template-driven-form.css',
})
export class TemplateDrivenForm {
onSubmit(form: NgForm) {
console.log(form.controls["firstname"]);
console.log(form.value);
}
hasErrors(control: NgModel) {
if(control.errors) {
return Object.keys(control.errors).length > 0
}
return false;
}
}

View File

@ -0,0 +1,8 @@
import { Demo } from './demo';
describe('Demo', () => {
it('should create an instance', () => {
const directive = new Demo();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,20 @@
import { Directive, input, Input, output, SimpleChanges } from '@angular/core';
@Directive({
selector: '[appDemo]'
})
export class Demo {
value = input.required<string>({ alias: "appDemo" })
valueEmit = output<string>({ alias: "appDemoChange" })
// @Input({ required: true, alias: "appDemo" }) value: string = ""
constructor() { }
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
if(changes["value"].currentValue === "secret") {
this.valueEmit.emit("ciao");
}
}
}

View File

@ -0,0 +1,8 @@
import { HighlightEmpty } from './highlight-empty';
describe('HighlightEmpty', () => {
it('should create an instance', () => {
const directive = new HighlightEmpty();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import { DestroyRef, Directive, ElementRef, HostListener, inject, Renderer2 } from '@angular/core';
@Directive({
selector: '[appHighlightEmpty]',
})
export class HighlightEmpty {
private readonly emptyBorder = '3px solid #d9534f'; // red
private readonly normalBorder = '3px solid #ced4da'; // default-ish
destroyRef = inject(DestroyRef);
constructor(private el: ElementRef<HTMLInputElement>, private renderer: Renderer2) {
const listener = ({ target }: Event) => {
const value = (target as any).value;
this.updateBorder(value);
};
this.el.nativeElement.addEventListener('input', listener);
this.destroyRef.onDestroy(() => {
// clean up delle subscription
this.el.nativeElement.removeEventListener('input', listener);
});
}
ngOnInit(): void {
// Run once on init to set the correct state based on initial value
this.updateBorder(this.el.nativeElement.value);
}
@HostListener('input', ['$event.target'])
onInput(target: EventTarget | null): void {
const value = (target as any).value;
this.updateBorder(value);
}
private updateBorder(value: string): void {
const trimmed = value?.trim() ?? '';
if (trimmed === '') {
// Empty → red border
this.renderer.setStyle(this.el.nativeElement, 'border', this.emptyBorder);
} else {
// Has content → normal border
this.renderer.setStyle(this.el.nativeElement, 'border', this.normalBorder);
}
}
}

View File

@ -0,0 +1,8 @@
import { Interval } from './interval';
describe('Interval', () => {
it('should create an instance', () => {
const directive = new Interval();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { Directive, EventEmitter, Output } from '@angular/core';
@Directive({
selector: '[appInterval]',
})
export class Interval {
@Output() everySecond = new EventEmitter<string>();
@Output() everyFiveSecs = new EventEmitter<string>();
firstInterval = 0;
secondInterval = 0;
ngOnInit() {
setInterval(() => this.everySecond.emit('event 1000'), 1000);
setInterval(() => this.everyFiveSecs.emit('event 5000'), 5000);
}
ngOnDestroy() {
clearInterval(this.firstInterval);
clearInterval(this.secondInterval);
}
}

View File

@ -0,0 +1,8 @@
import { MustBeUppercase } from './must-be-uppercase';
describe('MustBeUppercase', () => {
it('should create an instance', () => {
const directive = new MustBeUppercase();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,21 @@
import { Directive } from '@angular/core';
import { AbstractControl, NG_VALIDATORS, Validator } from '@angular/forms';
@Directive({
selector: '[uppercase]',
providers: [{ provide: NG_VALIDATORS, useExisting: MustBeUppercase, multi: true }],
})
export class MustBeUppercase implements Validator {
constructor() {}
validate(c: AbstractControl) {
console.log('Validating uppercase directive');
if (!c.value) {
return null;
}
if (c.value !== c.value.toUpperCase()) {
return { mustBeUppercase: c.value };
}
return null;
}
}

View File

@ -0,0 +1,8 @@
import { Quantity } from './quantity';
describe('Quantity', () => {
it('should create an instance', () => {
const directive = new Quantity();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,67 @@
import {
Directive,
ElementRef,
EventEmitter,
HostListener,
Input,
Output,
SimpleChanges,
} from '@angular/core';
@Directive({
selector: '[appQuantity]',
})
export class Quantity {
@Input('appQuantity') quantity: number = 1;
@Output('appQuantityChange') quantityChange = new EventEmitter<number>();
@Input() min: number = 1;
@Input() max: number = 99;
constructor(private el: ElementRef<HTMLInputElement>) {}
ngOnChanges(changes: SimpleChanges): void {
if ('quantity' in changes) {
this.writeValue(this.quantity);
}
}
// ascolta gli input dell'utente
@HostListener('input', ['$event.target'])
onInput(target: EventTarget | null) {
const rawValue = (target as any).value;
console.log(rawValue);
const parsed = Number(rawValue);
// se NaN, non emetto niente, ma non rompo il padre
if (Number.isNaN(parsed)) {
this.updateQuantity(0);
return;
}
this.updateQuantity(parsed);
}
@HostListener('blur')
onBlur() {
this.updateQuantity(this.quantity);
}
private updateQuantity(value: number) {
let newValue = value;
if (newValue < this.min) {
newValue = this.min;
}
if (newValue > this.max) {
newValue = this.max;
}
this.quantity = newValue;
this.writeValue(newValue);
this.quantityChange.emit(newValue);
}
private writeValue(value: number) {
this.el.nativeElement.value = String(value);
}
}

View File

@ -0,0 +1,8 @@
import { StockStatus } from './stock-status';
describe('StockStatus', () => {
it('should create an instance', () => {
const directive = new StockStatus();
expect(directive).toBeTruthy();
});
});

View File

@ -0,0 +1,32 @@
import { Directive, ElementRef, Input, Renderer2, SimpleChanges } from '@angular/core';
@Directive({
selector: '[appStockStatus]'
})
export class StockStatus {
@Input({ alias: 'appStockStatus', required: true }) stock!: number; // one-way binding
constructor(private el: ElementRef, private renderer: Renderer2) {}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes)
if ('stock' in changes) {
this.updateStyles();
}
}
private updateStyles(): void {
// reset classi
this.renderer.removeClass(this.el.nativeElement, 'stock-low');
this.renderer.removeClass(this.el.nativeElement, 'stock-medium');
this.renderer.removeClass(this.el.nativeElement, 'stock-high');
if (this.stock <= 0) {
this.renderer.addClass(this.el.nativeElement, 'stock-low');
} else if (this.stock < 10) {
this.renderer.addClass(this.el.nativeElement, 'stock-medium');
} else {
this.renderer.addClass(this.el.nativeElement, 'stock-high');
}
}
}

View File

@ -0,0 +1,19 @@
<div class="container p-4">
<h1>Angular Course Examples and Exercises</h1>
<p>Select to navigate.</p>
<div class="d-flex flex-row justify-content-between">
<!-- Hover added -->
<div class="flex-1 list-group">
@for (ex of examples; track $index) {
<a [routerLink]="ex.path" class="list-group-item list-group-item-action">{{ ex.title }}</a>
}
</div>
<div class="flex-1 list-group">
@for (ex of exercises; track $index) {
<a [routerLink]="ex.path" class="list-group-item list-group-item-action">{{ ex.title }}</a>
}
</div>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Homepage } from './homepage';
describe('Homepage', () => {
let component: Homepage;
let fixture: ComponentFixture<Homepage>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Homepage]
})
.compileComponents();
fixture = TestBed.createComponent(Homepage);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import { Component } from '@angular/core';
import { RouterLink } from '@angular/router';
@Component({
selector: 'app-homepage',
imports: [RouterLink],
templateUrl: './homepage.html',
styleUrl: './homepage.css',
})
export class Homepage {
exercises = [
{ title: 'Exercise 1', path: '/exercise-1', done: true },
{ title: 'Exercise 2', path: '/exercise-2', done: true },
{ title: 'Exercise 3', path: '/exercise-3', done: true },
{ title: 'Exercise 4', path: '/exercise-4', done: true },
{ title: 'Exercise 5', path: '/exercise-5', done: true },
{ title: 'Exercise 6', path: '/exercise-6', done: true },
{ title: 'Exercise 7', path: '/exercise-7', done: false },
{ title: 'Exercise 8', path: '/exercise-8', done: false },
{ title: 'Exercise 9', path: '/exercise-9', done: false },
{ title: 'Exercise 10', path: '/exercise-10', done: false },
]
examples = [
{ title: 'Counters', path: '/counters' },
{ title: 'Users', path: '/users' },
{ title: 'For Sample', path: '/for-sample' },
{ title: 'If Sample', path: '/if-sample' },
{ title: 'Dynamic Styling', path: '/dynamic-styling' },
{ title: 'Common Pipes', path: '/common-pipes' },
{ title: 'Custom Pipe', path: '/custom-pipe' },
{ title: 'Input Sample', path: '/input-sample' },
{ title: 'Login Form Sample', path: '/login-sample' },
{ title: 'Login Reactive Form Sample', path: '/reactive-login-sample' },
{ title: 'Custom Validated Form Sample', path: '/custom-validated-form' },
{ title: 'Nested Form Sample', path: '/nested-form-sample' },
{ title: 'Nested Form Advanced Sample', path: '/nested-form-advanced-sample' },
{ title: 'Life Cycle Sample', path: '/life-cycle-sample' },
{ title: 'Directive Sample', path: '/directive-sample' },
{ title: 'Tasks', path: '/tasks' },
{ title: 'Customers List', path: '/customers-list' },
{ title: 'Observable Sample', path: '/observable-sample' },
{ title: 'Observable Subject', path: '/observable-subject' },
// { title: 'Routing Avanzato', path: '/esempi/routing' },
// { title: 'RxJS e Observables', path: '/esempi/rxjs' },
// { title: 'Forms: Template Driven', path: '/esempi/forms-template' },
// { title: 'Forms: Reactive Forms', path: '/esempi/forms-reactive' }
];
}

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { HttpInterceptorFn } from '@angular/common/http';
import { authInterceptor } from './auth-interceptor';
describe('authInterceptor', () => {
const interceptor: HttpInterceptorFn = (req, next) =>
TestBed.runInInjectionContext(() => authInterceptor(req, next));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(interceptor).toBeTruthy();
});
});

View File

@ -0,0 +1,9 @@
import { HttpInterceptorFn } from '@angular/common/http';
import { inject } from '@angular/core';
import { Auth } from '../services/auth';
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(Auth);
console.log(authService.user);
return next(req);
};

View File

@ -0,0 +1,17 @@
import { TestBed } from '@angular/core/testing';
import { HttpInterceptorFn } from '@angular/common/http';
import { loggingInterceptor } from './logging-interceptor';
describe('loggingInterceptor', () => {
const interceptor: HttpInterceptorFn = (req, next) =>
TestBed.runInInjectionContext(() => loggingInterceptor(req, next));
beforeEach(() => {
TestBed.configureTestingModule({});
});
it('should be created', () => {
expect(interceptor).toBeTruthy();
});
});

View File

@ -0,0 +1,6 @@
import { HttpInterceptorFn } from '@angular/common/http';
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
console.log(req);
return next(req);
};

View File

@ -0,0 +1,8 @@
import { SexDecodePipe } from '../sex-decode-pipe';
describe('SexDecodePipe', () => {
it('create an instance', () => {
const pipe = new SexDecodePipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,16 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'sexDecode',
})
export class SexDecodePipe implements PipeTransform {
transform(value: string, ...args: unknown[]): unknown {
switch (value) {
case 'M':
return 'Male';
case 'F':
return 'Female';
}
return '??';
}
}

View File

@ -0,0 +1,8 @@
import { UserStatusPipe } from './user-status-pipe';
describe('UserStatusPipe', () => {
it('create an instance', () => {
const pipe = new UserStatusPipe();
expect(pipe).toBeTruthy();
});
});

View File

@ -0,0 +1,19 @@
import { Pipe, PipeTransform } from '@angular/core';
@Pipe({
name: 'userStatus'
})
export class UserStatusPipe implements PipeTransform {
transform(status: 'active' | 'inactive'): string {
switch (status) {
case 'active':
return '🟢 Active user';
case 'inactive':
return '🔴 Inactive user';
default:
return status;
}
}
}

View File

@ -0,0 +1,36 @@
<div class="container mt-4">
<h1>Percentages</h1>
<p>{{ 0.04 | percent : '.2' }}</p>
<p>{{ 0.05 | percent : '2.2' }}</p>
<h1>Decimals</h1>
<p>{{ 0.6 | number : '2.1-2' }}</p>
<p>{{ 12341.25123 | number : '4.2-4' }}</p>
<h1>Async</h1>
<p>{{ promise | async | json }}</p>
<h1>Date</h1>
<!--https://angular.io/docs/ts/latest/api/common/DatePipe-class.html-->
<p>{{ dob | date }}</p>
<p>{{ dob | date : 'medium' }}</p>
<p>{{ dob | date : 'dd/MM/yy' }}</p>
<h1>JSON</h1>
<p>Without JSON pipe: {{ obj }}</p>
<p>With JSON pipe: {{ obj | json }}</p>
<h1>Slice</h1>
<p>{{ str }}[0:4]: '{{ str | slice : 0 : 4 }}' - output is expected to be 'abcd'</p>
<p>{{ str }}[4:0]: '{{ str | slice : 4 : 0 }}' - output is expected to be ''</p>
<p>{{ str }}[-4]: '{{ str | slice : -4 }}' - output is expected to be 'ghij'</p>
<p>{{ str }}[-4:-1]: '{{ str | slice : -4 : -1 }}' - output is expected to be 'gh'</p>
<p>{{ str }}[-100]: '{{ str | slice : -99 }}' - output is expected to be 'abcdefghij'</p>
<p>{{ str }}[100]: '{{ str | slice : 100 }}' - output is expected to be ''</p>
<h1>Currency</h1>
<p>{{ 1234.567 | currency }}</p>
<p>{{ 1234.567 | currency : 'EUR' }}</p>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CommonPipes } from './common-pipes';
describe('CommonPipes', () => {
let component: CommonPipes;
let fixture: ComponentFixture<CommonPipes>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CommonPipes]
})
.compileComponents();
fixture = TestBed.createComponent(CommonPipes);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,47 @@
import {
AsyncPipe,
CurrencyPipe,
DatePipe,
DecimalPipe,
JsonPipe,
PercentPipe,
SlicePipe,
} from '@angular/common';
import { Component } from '@angular/core';
@Component({
selector: 'app-common-pipes',
imports: [
SlicePipe,
JsonPipe,
AsyncPipe,
DatePipe,
CurrencyPipe,
DecimalPipe,
PercentPipe,
],
templateUrl: './common-pipes.html',
styleUrl: './common-pipes.css',
})
export class CommonPipes {
obj: { firstName: string; lastName: string };
str: string = 'abcdefghij';
dob: Date = new Date();
promise: Promise<string>;
constructor() {
// this.promise = fetch("https://jsonplaceholder.typicode.com/todos/1").then(res => {
// console.log(res);
// return res.json();
// });
this.promise = new Promise(function (resolve, reject) {
setTimeout(function () {
resolve("Hey, I'm from a promise.");
}, 2000);
});
// this.promise
// .then((myString) => { return "Pippo"; })
// .then((myString) => { console.log(myString); });
this.obj = { firstName: 'Daniele', lastName: 'Teti' };
}
}

View File

@ -0,0 +1,32 @@
<div class="p-2">
<div class="d-flex flex-column gap-2">
<h1>Simple Counter</h1>
<div class="d-flex flex-row align-items-center gap-5">
<button class="btn btn-danger" (click)="counter.decrement()">-</button>
<h4>{{ counter.value }}</h4>
<button class="btn btn-primary" (click)="counter.increment()">+</button>
</div>
<p>counter greater than 10: {{ counterGreaterThan10 }}</p>
<p>counter.value > 10: {{ counter.value > 10 }}</p>
</div>
<hr />
<div class="d-flex flex-column gap-2">
<h1>Signal Counter</h1>
<div class="d-flex flex-row align-items-center gap-5">
<button class="btn btn-danger" (click)="signalCounter.decrement()">-</button>
<h4>{{ signalCounter.value() }}</h4>
<button class="btn btn-primary" (click)="signalCounter.increment()">+</button>
</div>
<p>signalGreaterThan10: {{ signalGreaterThan10() }}</p>
</div>
<hr />
<!-- <div class="d-flex flex-column gap-2">
<h1>RxJS Counter</h1>
<div class="d-flex flex-row align-items-center gap-5">
<button class="btn btn-danger" (click)="rxjsCounter.decrement()">-</button>
<h4>{{ rxjsCounter.value$ | async }}</h4>
<button class="btn btn-primary" (click)="rxjsCounter.increment()">+</button>
</div>
<p>signalGreaterThan10: {{ signalGreaterThan10() }}</p>
</div> -->
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { Counters } from './counters';
describe('Counters', () => {
let component: Counters;
let fixture: ComponentFixture<Counters>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [Counters]
})
.compileComponents();
fixture = TestBed.createComponent(Counters);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,57 @@
// import { AsyncPipe } from '@angular/common';
import { Component, computed, signal, effect } from '@angular/core';
// import { BehaviorSubject } from 'rxjs';
@Component({
selector: 'app-counters',
// imports: [AsyncPipe],
templateUrl: './counters.html',
styleUrl: './counters.css',
})
export class Counters {
counter = {
value: 0,
increment: function () {
this.value += 1;
},
decrement: function () {
this.value -= 1;
},
};
counterGreaterThan10 = this.counter.value > 10;
signalCounter = {
value: signal(0),
increment: function () {
console.log('Changing signal value...');
this.value.update((v) => v + 1);
},
decrement: function () {
console.log('Changing signal value...');
this.value.update((v) => v - 1);
},
};
constructor() {
effect((onCleanup) => {
console.log('onChangedValue: value=', this.signalCounter.value());
onCleanup(() => {
console.log('onChangingValue or onDestroyComponent: value=', this.signalCounter.value());
});
});
}
signalGreaterThan10 = computed(() => this.signalCounter.value() > 10);
// private rxjsCounterObservable = new BehaviorSubject<number>(0);
// rxjsCounter = {
// value$: this.rxjsCounterObservable.asObservable(),
// increment: () => {
// this.rxjsCounterObservable.next(this.rxjsCounterObservable.value + 1);
// },
// decrement: () => {
// this.rxjsCounterObservable.next(this.rxjsCounterObservable.value - 1);
// },
// };
}

View File

@ -0,0 +1,5 @@
<div>
{{person.fullname}}
{{person.sex}}
{{person.sex | sexDecode }}
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomPipe } from './custom-pipe';
describe('CustomPipe', () => {
let component: CustomPipe;
let fixture: ComponentFixture<CustomPipe>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CustomPipe]
})
.compileComponents();
fixture = TestBed.createComponent(CustomPipe);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,18 @@
import { Component } from '@angular/core';
import { SexDecodePipe } from '../../pipes/sex-decode-pipe';
@Component({
selector: 'app-custom-pipe',
imports: [SexDecodePipe],
templateUrl: './custom-pipe.html',
styleUrl: './custom-pipe.css',
})
export class CustomPipe {
person: {
fullname: string;
sex: string;
} = {
fullname: 'Daniele',
sex: 'M',
};
}

View File

@ -0,0 +1,45 @@
<div class="container mt-4">
<form [formGroup]="form" #formDir="ngForm" (ngSubmit)="onSubmit()">
<h2>Custom Validated Form - Login</h2>
<div class="mb-3">
<label for="email" class="form-label">Email</label>
<input
type="email"
class="form-control"
name="email"
id="email"
autocomplete="off"
[class.is-invalid]="emailIsInvalid"
[formControl]="form.controls.email"
aria-describedby="emailHelpId"
placeholder="abc@mail.com"
/>
<!-- uppercase -->
@if(emailIsInvalid) { @if(form.controls.email.errors?.["required"]) {
<small class="form-text text-danger">Email is required.</small>
} @else if (form.controls.email.errors?.["mustBeUppercase"]) {
<small class="form-text text-danger">Email must be uppercase.</small>
} @else if(form.controls.email.errors?.["email"]) {
<small class="form-text text-danger">Invalid email format.</small>
} }
</div>
<div class="mb-3">
<label for="password" class="form-label">Password</label>
<input
type="password"
class="form-control"
name="password"
id="password"
autocomplete="off"
[class.is-invalid]="passwordIsInvalid && formDir.submitted"
formControlName="password"
placeholder="Insert password"
/>
</div>
<div class="d-flex flex-row gap-2">
<button type="submit" [disabled]="form.invalid" class="btn btn-primary">Submit</button>
<button type="submit" class="btn btn-primary">Submit</button>
<button type="reset" class="btn btn-warning">Reset</button>
</div>
</form>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomValidatedForm } from './custom-validated-form';
describe('CustomValidatedForm', () => {
let component: CustomValidatedForm;
let fixture: ComponentFixture<CustomValidatedForm>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CustomValidatedForm]
})
.compileComponents();
fixture = TestBed.createComponent(CustomValidatedForm);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,54 @@
import { Component } from '@angular/core';
import {
AbstractControl,
FormControl,
FormGroup,
FormsModule,
ReactiveFormsModule,
Validators,
} from '@angular/forms';
// import { MustBeUppercase } from '../../directives/must-be-uppercase';
const mustBeUppercase = (control: AbstractControl) => {
const value = control.value as string;
if (value !== value?.toUpperCase()) {
return { mustBeUppercase: value };
}
return null;
};
@Component({
selector: 'app-custom-validated-form',
imports: [ReactiveFormsModule],
templateUrl: './custom-validated-form.html',
styleUrl: './custom-validated-form.css',
})
export class CustomValidatedForm {
form = new FormGroup({
email: new FormControl('', {
validators: [Validators.required, Validators.email, mustBeUppercase],
}),
password: new FormControl('', {
validators: [Validators.required, Validators.minLength(6), Validators.maxLength(18)],
}),
});
get emailIsInvalid() {
return (
this.form.controls.email.invalid &&
this.form.controls.email.touched &&
this.form.controls.email.dirty
);
}
get passwordIsInvalid() {
return (
this.form.controls.password.invalid &&
this.form.controls.password.touched &&
this.form.controls.password.dirty
);
}
onSubmit() {
console.log(this.form.valid);
console.log(this.form);
}
}

View File

@ -0,0 +1,32 @@
<div class="container my-4">
<h2 class="mb-4">Customer List</h2>
<table class="table table-striped table-hover align-middle">
<thead class="table-dark">
<tr>
<th scope="col">ID</th>
<th scope="col">Full Name</th>
<th scope="col">Email</th>
<th scope="col">City</th>
<th scope="col">Created At</th>
</tr>
</thead>
<tbody>
@for(customer of (customers$ | async); track customer.id) {
<tr>
<td>{{ customer.id }}</td>
<td>{{ customer.name }}</td>
<td>{{ customer.email }}</td>
<td>{{ customer.city }}</td>
<td>{{ customer.created_at | date : 'medium' }}</td>
</tr>
} @empty {
<div class="alert alert-info mt-3">No customers found.</div>
}
</tbody>
</table>
<!-- @if(customers().length === 0) {
} -->
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { CustomersList } from './customers-list';
describe('CustomersList', () => {
let component: CustomersList;
let fixture: ComponentFixture<CustomersList>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [CustomersList]
})
.compileComponents();
fixture = TestBed.createComponent(CustomersList);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,27 @@
import { Component, DestroyRef, inject, signal } from '@angular/core';
import { Customers } from '../../services/customers';
// import { Customer } from '../../types/customer';
import { AsyncPipe, DatePipe } from '@angular/common';
@Component({
selector: 'app-customers-list',
imports: [DatePipe, AsyncPipe],
templateUrl: './customers-list.html',
styleUrl: './customers-list.css',
})
export class CustomersList {
// customers = signal<Customer[]>([])
private customerService = inject(Customers);
customers$ = this.customerService.getCustomers();
// destroyRef = inject(DestroyRef)
ngOnInit() {
// const subscription = this.customerService.getCustomers().subscribe({
// next: (v) => this.customers.set(v)
// })
// this.destroyRef.onDestroy(() => subscription.unsubscribe())
}
}

View File

@ -0,0 +1,18 @@
.stock-badge {
padding: 0.2rem 0.5rem;
border-radius: 4px;
color: white;
font-size: 0.8rem;
}
.stock-low {
background-color: #d9534f; /* rosso */
}
.stock-medium {
background-color: #f0ad4e; /* arancione */
}
.stock-high {
background-color: #5cb85c; /* verde */
}

View File

@ -0,0 +1,54 @@
<div class="container p-4">
@for(product of products; track product.id) {
<li class="d-flex flex-row gap-2 align-items-center mb-2">
{{ product.name }} in stock:
<div>
<input
type="text"
class="form-control"
name="stock"
[(ngModel)]="product.stock"
id="stock"
aria-describedby="helpId"
placeholder="Insert how many items in stock"
[appStockStatus]="product.stock"
/>
</div>
<span class="stock-badge" [appStockStatus]="product.stock"> {{ product.stock }} pezzi </span>
<div class="form-check">
<input
class="form-check-input"
type="checkbox"
[checked]="selectedProduct?.id === product.id"
[value]="selectedProduct?.id === product.id"
[id]="product.id"
(click)="onSelect(product)"
/>
<label class="form-check-label" [for]="product.id">Select</label>
</div>
</li>
}
<div appInterval (everyFiveSecs)="onFiveSeconds()" (everySecond)="everySecond()"></div>
@if(selectedProduct) {
<div class="item-row">
<span>{{ selectedProduct.name }}</span>
<input
type="text"
class="form-control"
aria-describedby="helpId"
[(appQuantity)]="selectedProduct.quantity"
[appStockStatus]="selectedProduct.stock"
[min]="1"
[max]="selectedProduct.stock"
/>
<span>Selected: {{ selectedProduct.quantity }}</span>
</div>
}
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DirectivesSample } from './directives-sample';
describe('DirectivesSample', () => {
let component: DirectivesSample;
let fixture: ComponentFixture<DirectivesSample>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DirectivesSample]
})
.compileComponents();
fixture = TestBed.createComponent(DirectivesSample);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,35 @@
import { Component } from '@angular/core';
import { StockStatus } from '../../directives/stock-status';
import { DUMMY_PRODUCTS } from '../../data/dummy-products';
import { Interval } from '../../directives/interval';
import { FormsModule } from '@angular/forms';
import { Product } from '../../types/product';
import { Quantity } from "../../directives/quantity";
interface ProductWithQuantity extends Product {
quantity: number;
}
@Component({
selector: 'app-directives-sample',
imports: [StockStatus, Interval, FormsModule, Quantity],
templateUrl: './directives-sample.html',
styleUrl: './directives-sample.css',
})
export class DirectivesSample {
products = DUMMY_PRODUCTS;
value = ""
selectedProduct: ProductWithQuantity | null = null;
onSelect(product: Product) {
this.selectedProduct = { ...product, quantity: 0 };
}
onFiveSeconds() {
console.log('every five seconds event');
}
everySecond() {
console.log('every second event');
}
}

View File

@ -0,0 +1,3 @@
.highlight {
border: 2px solid #ffc107 !important;
}

View File

@ -0,0 +1,44 @@
<div class="container mt-4">
<div class="controls mb-3">
<button
class="btn btn-primary me-2"
[disabled]="opacity() == 0"
[style.opacity]="opacity()"
(click)="decreaseFont()"
>
Decrease font
</button>
<button
class="btn btn-primary me-2"
[disabled]="opacity2() == 0"
[style.opacity]="opacity2()"
(click)="increaseFont()"
>
Increase font
</button>
<button class="btn btn-warning me-2" (click)="toggleHighlight()">Toggle highlight</button>
<button class="btn btn-danger" (click)="changeColor()">Set text to a random color</button>
</div>
<!-- ⭐ Dynamic Styling Examples -->
<!-- Dynamic class toggle -->
<!-- Dynamic single style property -->
<!-- Dynamic styles with complex values -->
<!-- Multiple dynamic styles -->
<div
class="card p-3"
[ngStyle]="{
border: '1px solid #ccc',
transition:
'all 0.3s ease'
}"
[class.highlight]="highlight()"
[style.color]="textColor()"
[style.fontSize.px]="fontSize()"
>
<h3>Dynamic Styled Card</h3>
<p>
The text color, border, and font size change dynamically using Angular bindings and signals.
</p>
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { DynamicStyling } from './dynamic-styling';
describe('DynamicStyling', () => {
let component: DynamicStyling;
let fixture: ComponentFixture<DynamicStyling>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [DynamicStyling]
})
.compileComponents();
fixture = TestBed.createComponent(DynamicStyling);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,50 @@
import { NgStyle } from '@angular/common';
import { Component, computed, signal } from '@angular/core';
import { getRandomColor } from '../../utilities';
@Component({
selector: 'app-dynamic-styling',
imports: [NgStyle],
templateUrl: './dynamic-styling.html',
styleUrl: './dynamic-styling.css',
})
export class DynamicStyling {
textColor = signal('black');
fontSize = signal(16);
highlight = signal(false);
opacity = computed(() => {
const size = this.fontSize();
if (size > 10) {
return 1;
}
const steps = Math.floor((10 - size) / 2);
const opacity = 1 - steps * 0.2;
return Math.max(0, Math.min(1, opacity));
});
opacity2 = computed(() => {
const size = this.fontSize();
if (size <= 22) {
return 1;
}
const steps = Math.floor((size - 22) / 2);
const opacity = 1 - steps * 0.2;
return Math.max(0, Math.min(1, opacity));
});
increaseFont() {
this.fontSize.update((v) => v + 2);
}
decreaseFont() {
this.fontSize.update((v) => (v - 2 <= 0 ? 0 : v - 2));
}
toggleHighlight() {
this.highlight.update((v) => !v);
}
changeColor() {
this.textColor.set(getRandomColor());
}
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { FirstComponent } from './first-component';
describe('FirstComponent', () => {
let component: FirstComponent;
let fixture: ComponentFixture<FirstComponent>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [FirstComponent]
})
.compileComponents();
fixture = TestBed.createComponent(FirstComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,11 @@
import { Component } from '@angular/core';
@Component({
selector: 'app-first-component',
imports: [],
templateUrl: './first-component.html',
styleUrl: './first-component.css',
})
export class FirstComponent {
}

View File

@ -0,0 +1,37 @@
<div class="container pt-2">
<ul class="list-unstyled d-flex flex-column gap-2">
@for (user of users; track user.id) {
<li class="d-flex flex-column gap-2">
<div class="d-flex align-items-center gap-2">
{{ $index }} -
<app-userv2
[isSelected]="selectedUsers.includes(user.id)"
[id]="user.id"
[avatar]="user.avatar"
[name]="user.name"
(select)="selectUser($event)"
/>
</div>
<ul>
<li>
<p>how many items: {{ $count }}</p>
</li>
<li>
<p>is first: {{ $first }}</p>
</li>
<li>
<p>is last: {{ $last }}</p>
</li>
<li>
<p>is even: {{ $even }}</p>
</li>
<li>
<p>is odd: {{ $odd }}</p>
</li>
</ul>
</li>
} @empty {
<div>No users found.</div>
}
</ul>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ForSample } from './for-sample';
describe('ForSample', () => {
let component: ForSample;
let fixture: ComponentFixture<ForSample>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ForSample]
})
.compileComponents();
fixture = TestBed.createComponent(ForSample);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,22 @@
import { Component } from '@angular/core';
import { Userv2 } from "../userv2/userv2";
import { DUMMY_USERS } from '../../data/dummy-users';
import { User } from '../../types/user';
@Component({
selector: 'app-for-sample',
imports: [Userv2],
templateUrl: './for-sample.html',
styleUrl: './for-sample.css',
})
export class ForSample {
users: User[] = [];
selectedUsers: number[] = [];
selectUser (id: number) {
if(this.selectedUsers.includes(id)) {
this.selectedUsers = this.selectedUsers.filter(userId => userId !== id);
return;
}
this.selectedUsers.push(id);
}
}

View File

@ -0,0 +1,19 @@
<div>
@if(true) {
<div>condition is true</div>
} @else {
<div>condition is false</div>
}
</div>
<div>
@for(user of users; track user.id) {
@if(user.isAdmin) {
<div>Welcome, admin {{ user.name }}!</div>
} @else if(user.isGuest) {
<div>Welcome, guest! Please log in.</div>
} @else {
<div>Welcome, {{ user.name }}!</div>
}
}
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { IfSample } from './if-sample';
describe('IfSample', () => {
let component: IfSample;
let fixture: ComponentFixture<IfSample>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [IfSample]
})
.compileComponents();
fixture = TestBed.createComponent(IfSample);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,12 @@
import { Component } from '@angular/core';
import { DUMMY_USERS } from '../../data/dummy-users';
@Component({
selector: 'app-if-sample',
imports: [],
templateUrl: './if-sample.html',
styleUrl: './if-sample.css',
})
export class IfSample {
users = DUMMY_USERS
}

View File

@ -0,0 +1,32 @@
<div class="container mt-4">
<div>
<label for="username-show" class="form-label">Username Show</label>
<input
id="username-show"
type="text"
class="form-control"
[value]="username"
name="username-show"
/>
</div>
<div>
<label for="username-edit" class="form-label">Username Edit</label>
<input
id="username-edit"
type="text"
class="form-control"
name="username edit"
(input)="username = $event.target.value"
/>
</div>
<hr />
<div>
<label for="title-show" class="form-label">Title Show(& edit)</label>
<input id="title" type="text" class="form-control" [(ngModel)]="title" name="title-show" />
</div>
<div>
<label for="title-edit" class="form-label">Title Edit(& show)</label>
<input id="title" type="text" class="form-control" [(ngModel)]="title" name="title-edit" />
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { InputSample } from './input-sample';
describe('InputSample', () => {
let component: InputSample;
let fixture: ComponentFixture<InputSample>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [InputSample]
})
.compileComponents();
fixture = TestBed.createComponent(InputSample);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,13 @@
import { Component } from '@angular/core';
import { FormsModule } from '@angular/forms';
@Component({
selector: 'app-input-sample',
imports: [FormsModule],
templateUrl: './input-sample.html',
styleUrl: './input-sample.css',
})
export class InputSample {
username: string = "";
title: string = "";
}

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { ChildChanges } from './child-changes';
describe('ChildChanges', () => {
let component: ChildChanges;
let fixture: ComponentFixture<ChildChanges>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [ChildChanges]
})
.compileComponents();
fixture = TestBed.createComponent(ChildChanges);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,30 @@
import { Component, effect, input, Input } from '@angular/core';
@Component({
selector: 'app-child-changes',
imports: [],
templateUrl: './child-changes.html',
styleUrl: './child-changes.css',
})
export class ChildChanges {
// @Input() prop: string = ""
prop = input<string>("")
constructor() {
effect(() => {
console.log("value in effect: ", this.prop())
})
}
ngOnChanges(changes: Object) {
// Called right after our bindings have been checked but only
// if one of our bindings has changed.
//
// changes is an object of the format:
// {
// 'prop': PropertyUpdate
// }
console.log('ngOnChanges', changes);
}
}

View File

@ -0,0 +1,22 @@
<div class="container mt-4">
<h1>Life Cycle Sample</h1>
<h3>Check the console log</h3>
<div class="d-flex flex-column gap-2 align-items-start">
<input [(ngModel)]="property" />
<span class="d-flex flex-row gap-2 align-items-center">
<button class="btn btn-primary" (click)="doClick()">Click Me</button>
<p class="mb-0">
The value is
<strong>{{ value }}</strong>
</p>
</span>
<span class="d-flex flex-row gap-2 align-items-center">
<button class="btn btn-primary" (click)="onUpdateSignal()">Update Signal</button>The signal
<p class="mb-0">
value is
<strong>{{ test() }}</strong>
</p>
</span>
<app-child-changes [prop]="property" />
</div>
</div>

View File

@ -0,0 +1,23 @@
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { LifeCycleSample } from './life-cycle-sample';
describe('LifeCycleSample', () => {
let component: LifeCycleSample;
let fixture: ComponentFixture<LifeCycleSample>;
beforeEach(async () => {
await TestBed.configureTestingModule({
imports: [LifeCycleSample]
})
.compileComponents();
fixture = TestBed.createComponent(LifeCycleSample);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
});

View File

@ -0,0 +1,67 @@
import { Component, signal } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { ChildChanges } from "./child-changes/child-changes";
@Component({
selector: 'app-life-cycle-sample',
imports: [FormsModule, ChildChanges],
templateUrl: './life-cycle-sample.html',
styleUrl: './life-cycle-sample.css',
})
export class LifeCycleSample {
value: string = 'default value';
test = signal<number>(0);
property = '';
constructor() {}
doClick() {
this.value = (Math.random() * 1000).toString();
}
onUpdateSignal() {
this.test.update((v) => v + 1);
}
ngOnInit() {
console.log('ngOnInit');
}
ngOnDestroy() {
console.log('ngOnDestroy');
}
ngDoCheck() {
console.log('ngDoCheck');
}
ngOnChanges(changes: Object) {
// Called right after our bindings have been checked but only
// if one of our bindings has changed.
//
// changes is an object of the format:
// {
// 'prop': PropertyUpdate
// }
console.log('ngOnChanges', changes);
}
ngAfterContentInit() {
// Component content has been initialized
console.log('ngAfterContentInit');
}
ngAfterContentChecked() {
// Component content has been Checked
console.log('ngAfterContentChecked');
}
ngAfterViewInit() {
// Component views are initialized
console.log('ngAfterViewInit');
}
ngAfterViewChecked() {
// Component views have been checked
console.log('ngAfterViewChecked');
}
}

Some files were not shown because too many files have changed in this diff Show More