stuff
This commit is contained in:
parent
252d6786af
commit
6c903fc03b
|
|
@ -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
|
||||||
|
|
@ -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
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
|
||||||
|
"recommendations": ["angular.ng-template"]
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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
|
|
@ -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 |
|
|
@ -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,
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<router-outlet />
|
||||||
|
|
@ -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',
|
||||||
|
}
|
||||||
|
];
|
||||||
|
|
@ -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');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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';
|
||||||
|
}
|
||||||
|
|
@ -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 },
|
||||||
|
]
|
||||||
|
|
@ -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 },
|
||||||
|
];
|
||||||
|
|
@ -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,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Demo } from './demo';
|
||||||
|
|
||||||
|
describe('Demo', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new Demo();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { HighlightEmpty } from './highlight-empty';
|
||||||
|
|
||||||
|
describe('HighlightEmpty', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new HighlightEmpty();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Interval } from './interval';
|
||||||
|
|
||||||
|
describe('Interval', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new Interval();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { MustBeUppercase } from './must-be-uppercase';
|
||||||
|
|
||||||
|
describe('MustBeUppercase', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new MustBeUppercase();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { Quantity } from './quantity';
|
||||||
|
|
||||||
|
describe('Quantity', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new Quantity();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { StockStatus } from './stock-status';
|
||||||
|
|
||||||
|
describe('StockStatus', () => {
|
||||||
|
it('should create an instance', () => {
|
||||||
|
const directive = new StockStatus();
|
||||||
|
expect(directive).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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' }
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,6 @@
|
||||||
|
import { HttpInterceptorFn } from '@angular/common/http';
|
||||||
|
|
||||||
|
export const loggingInterceptor: HttpInterceptorFn = (req, next) => {
|
||||||
|
console.log(req);
|
||||||
|
return next(req);
|
||||||
|
};
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { SexDecodePipe } from '../sex-decode-pipe';
|
||||||
|
|
||||||
|
describe('SexDecodePipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new SexDecodePipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 '??';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,8 @@
|
||||||
|
import { UserStatusPipe } from './user-status-pipe';
|
||||||
|
|
||||||
|
describe('UserStatusPipe', () => {
|
||||||
|
it('create an instance', () => {
|
||||||
|
const pipe = new UserStatusPipe();
|
||||||
|
expect(pipe).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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' };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
// },
|
||||||
|
// };
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
<div>
|
||||||
|
{{person.fullname}}
|
||||||
|
{{person.sex}}
|
||||||
|
{{person.sex | sexDecode }}
|
||||||
|
</div>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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())
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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 */
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,3 @@
|
||||||
|
.highlight {
|
||||||
|
border: 2px solid #ffc107 !important;
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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 = "";
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
<p>{{ prop() }}</p>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@ -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
Loading…
Reference in New Issue