├── .editorconfig
├── .gitignore
├── .vscode
├── extensions.json
├── launch.json
└── tasks.json
├── README.md
├── angular.json
├── package-lock.json
├── package.json
├── src
├── app
│ ├── app.component.ts
│ ├── app.config.ts
│ ├── app.routes.ts
│ ├── components
│ │ ├── add-contact.component.ts
│ │ ├── contact-form.component.ts
│ │ ├── contacts-list.component.ts
│ │ └── edit-contact.component.ts
│ ├── model
│ │ └── contact.model.ts
│ └── services
│ │ └── api.service.ts
├── assets
│ └── .gitkeep
├── favicon.ico
├── index.html
├── main.ts
└── styles.scss
├── tsconfig.app.json
├── tsconfig.json
└── tsconfig.spec.json
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Editor configuration, see https://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | charset = utf-8
6 | indent_style = space
7 | indent_size = 2
8 | insert_final_newline = true
9 | trim_trailing_whitespace = true
10 |
11 | [*.ts]
12 | quote_type = single
13 |
14 | [*.md]
15 | max_line_length = off
16 | trim_trailing_whitespace = false
17 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See http://help.github.com/ignore-files/ for more about ignoring files.
2 |
3 | # Compiled output
4 | /dist
5 | /tmp
6 | /out-tsc
7 | /bazel-out
8 |
9 | # Node
10 | /node_modules
11 | npm-debug.log
12 | yarn-error.log
13 |
14 | # IDEs and editors
15 | .idea/
16 | .project
17 | .classpath
18 | .c9/
19 | *.launch
20 | .settings/
21 | *.sublime-workspace
22 |
23 | # Visual Studio Code
24 | .vscode/*
25 | !.vscode/settings.json
26 | !.vscode/tasks.json
27 | !.vscode/launch.json
28 | !.vscode/extensions.json
29 | .history/*
30 |
31 | # Firebase
32 | /src/app/firebaseConfig.ts
33 |
34 | # Miscellaneous
35 | /.angular/cache
36 | .sass-cache/
37 | /connect.lock
38 | /coverage
39 | /libpeerconnection.log
40 | testem.log
41 | /typings
42 |
43 | # System files
44 | .DS_Store
45 | Thumbs.db
46 |
--------------------------------------------------------------------------------
/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846
3 | "recommendations": ["angular.ng-template"]
4 | }
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
3 | "version": "0.2.0",
4 | "configurations": [
5 | {
6 | "name": "ng serve",
7 | "type": "chrome",
8 | "request": "launch",
9 | "preLaunchTask": "npm: start",
10 | "url": "http://localhost:4200/"
11 | },
12 | {
13 | "name": "ng test",
14 | "type": "chrome",
15 | "request": "launch",
16 | "preLaunchTask": "npm: test",
17 | "url": "http://localhost:9876/debug.html"
18 | }
19 | ]
20 | }
21 |
--------------------------------------------------------------------------------
/.vscode/tasks.json:
--------------------------------------------------------------------------------
1 | {
2 | // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558
3 | "version": "2.0.0",
4 | "tasks": [
5 | {
6 | "type": "npm",
7 | "script": "start",
8 | "isBackground": true,
9 | "problemMatcher": {
10 | "owner": "typescript",
11 | "pattern": "$tsc",
12 | "background": {
13 | "activeOnStart": true,
14 | "beginsPattern": {
15 | "regexp": "(.*?)"
16 | },
17 | "endsPattern": {
18 | "regexp": "bundle generation complete"
19 | }
20 | }
21 | }
22 | },
23 | {
24 | "type": "npm",
25 | "script": "test",
26 | "isBackground": true,
27 | "problemMatcher": {
28 | "owner": "typescript",
29 | "pattern": "$tsc",
30 | "background": {
31 | "activeOnStart": true,
32 | "beginsPattern": {
33 | "regexp": "(.*?)"
34 | },
35 | "endsPattern": {
36 | "regexp": "bundle generation complete"
37 | }
38 | }
39 | }
40 | }
41 | ]
42 | }
43 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # CRUD application with Angular Signals V16
2 |
3 | This app is a basic contacts app showing CRUD operations using the Angular Signals implementation - which has just been released in v16. Services are used to add artificial delay to mimic an API call - will be updating this to showcase more of signals and what they bring to the table with Angular.
4 |
5 | Cheers :)
6 |
--------------------------------------------------------------------------------
/angular.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json",
3 | "version": 1,
4 | "newProjectRoot": "projects",
5 | "projects": {
6 | "angular-signals-crud": {
7 | "projectType": "application",
8 | "schematics": {
9 | "@schematics/angular:component": {
10 | "style": "scss",
11 | "standalone": true,
12 | "inlineStyle": true,
13 | "inlineTemplate": true,
14 | "flat": true,
15 | "skipTests": true
16 | },
17 | "@schematics/angular:directive": {
18 | "standalone": true
19 | },
20 | "@schematics/angular:pipe": {
21 | "standalone": true
22 | }
23 | },
24 | "root": "",
25 | "sourceRoot": "src",
26 | "prefix": "app",
27 | "architect": {
28 | "build": {
29 | "builder": "@angular-devkit/build-angular:application",
30 | "options": {
31 | "outputPath": {
32 | "base": "dist/angular-signals-crud"
33 | },
34 | "index": "src/index.html",
35 | "polyfills": ["zone.js"],
36 | "tsConfig": "tsconfig.app.json",
37 | "inlineStyleLanguage": "scss",
38 | "assets": ["src/favicon.ico", "src/assets"],
39 | "styles": ["src/styles.scss"],
40 | "scripts": [],
41 | "browser": "src/main.ts"
42 | },
43 | "configurations": {
44 | "production": {
45 | "budgets": [
46 | {
47 | "type": "initial",
48 | "maximumWarning": "500kb",
49 | "maximumError": "1mb"
50 | },
51 | {
52 | "type": "anyComponentStyle",
53 | "maximumWarning": "2kb",
54 | "maximumError": "4kb"
55 | }
56 | ],
57 | "outputHashing": "all"
58 | },
59 | "development": {
60 | "optimization": false,
61 | "extractLicenses": false,
62 | "sourceMap": true,
63 | "namedChunks": true
64 | }
65 | },
66 | "defaultConfiguration": "production"
67 | },
68 | "serve": {
69 | "builder": "@angular-devkit/build-angular:dev-server",
70 | "configurations": {
71 | "production": {
72 | "buildTarget": "angular-signals-crud:build:production"
73 | },
74 | "development": {
75 | "buildTarget": "angular-signals-crud:build:development"
76 | }
77 | },
78 | "defaultConfiguration": "development"
79 | },
80 | "extract-i18n": {
81 | "builder": "@angular-devkit/build-angular:extract-i18n",
82 | "options": {
83 | "buildTarget": "angular-signals-crud:build"
84 | }
85 | },
86 | "test": {
87 | "builder": "@angular-devkit/build-angular:karma",
88 | "options": {
89 | "polyfills": ["zone.js", "zone.js/testing"],
90 | "tsConfig": "tsconfig.spec.json",
91 | "inlineStyleLanguage": "scss",
92 | "assets": ["src/favicon.ico", "src/assets"],
93 | "styles": [
94 | "@angular/material/prebuilt-themes/indigo-pink.css",
95 | "src/styles.scss"
96 | ],
97 | "scripts": []
98 | }
99 | }
100 | }
101 | }
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "angular-signals-crud",
3 | "version": "2.0.0",
4 | "scripts": {
5 | "ng": "ng",
6 | "start": "ng serve",
7 | "build": "ng build",
8 | "watch": "ng build --watch --configuration development",
9 | "test": "ng test"
10 | },
11 | "private": true,
12 | "dependencies": {
13 | "@angular/animations": "^19.1.5",
14 | "@angular/cdk": "^19.1.3",
15 | "@angular/common": "^19.1.5",
16 | "@angular/compiler": "^19.1.5",
17 | "@angular/core": "^19.1.5",
18 | "@angular/forms": "^19.1.5",
19 | "@angular/material": "^19.1.3",
20 | "@angular/platform-browser": "^19.1.5",
21 | "@angular/platform-browser-dynamic": "^19.1.5",
22 | "@angular/router": "^19.1.5",
23 | "rxjs": "~7.8.0",
24 | "tslib": "^2.3.0",
25 | "zone.js": "~0.15.0"
26 | },
27 | "devDependencies": {
28 | "@angular-devkit/build-angular": "^19.1.6",
29 | "@angular/cli": "~19.1.6",
30 | "@angular/compiler-cli": "^19.1.5",
31 | "@types/jasmine": "~4.3.0",
32 | "jasmine-core": "~4.6.0",
33 | "karma": "~6.4.0",
34 | "karma-chrome-launcher": "~3.2.0",
35 | "karma-coverage": "~2.2.0",
36 | "karma-jasmine": "~5.1.0",
37 | "karma-jasmine-html-reporter": "~2.0.0",
38 | "typescript": "~5.5.4"
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/src/app/app.component.ts:
--------------------------------------------------------------------------------
1 | import { Component } from '@angular/core';
2 | import { CommonModule } from '@angular/common';
3 | import { RouterModule, RouterOutlet } from '@angular/router';
4 | import { MatToolbarModule } from '@angular/material/toolbar';
5 | import { MatButtonModule } from '@angular/material/button';
6 | import { MatIconModule } from '@angular/material/icon';
7 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
8 | import { MatSnackBarModule } from '@angular/material/snack-bar';
9 |
10 | @Component({
11 | selector: 'app-root',
12 | imports: [
13 | CommonModule,
14 | RouterOutlet,
15 | MatToolbarModule,
16 | MatButtonModule,
17 | MatIconModule,
18 | RouterModule,
19 | MatProgressSpinnerModule,
20 | MatSnackBarModule,
21 | ],
22 | template: `
23 | My Contacts
25 |
28 |
29 |
30 | `,
31 | styles: [
32 | `
33 | @use '@angular/material' as mat;
34 |
35 | mat-toolbar {
36 | justify-content: space-between;
37 |
38 | @include mat.toolbar-overrides(
39 | (
40 | container-background-color: var(--mat-sys-primary),
41 | container-text-color: var(--mat-sys-on-primary),
42 | )
43 | );
44 |
45 | @include mat.icon-button-overrides(
46 | (
47 | icon-color: var(--mat-sys-on-primary),
48 | )
49 | );
50 | }
51 |
52 | .container {
53 | position: relative;
54 | }
55 | `,
56 | ],
57 | })
58 | export class AppComponent {
59 | title = 'Angular Signals Crud';
60 | }
61 |
--------------------------------------------------------------------------------
/src/app/app.config.ts:
--------------------------------------------------------------------------------
1 | import { ApplicationConfig } from '@angular/core';
2 | import { provideRouter, withComponentInputBinding } from '@angular/router';
3 |
4 | import { routes } from './app.routes';
5 | import { provideAnimationsAsync } from '@angular/platform-browser/animations/async';
6 |
7 | export const appConfig: ApplicationConfig = {
8 | providers: [
9 | provideRouter(routes, withComponentInputBinding()),
10 | provideAnimationsAsync(),
11 | ],
12 | };
13 |
--------------------------------------------------------------------------------
/src/app/app.routes.ts:
--------------------------------------------------------------------------------
1 | import { Routes } from '@angular/router';
2 | import { ContactsListComponent } from './components/contacts-list.component';
3 | import { AddContactComponent } from './components/add-contact.component';
4 | import { EditContactComponent } from './components/edit-contact.component';
5 |
6 | export const routes: Routes = [
7 | {
8 | path: '',
9 | pathMatch: 'full',
10 | component: ContactsListComponent,
11 | },
12 | {
13 | path: 'add',
14 | component: AddContactComponent,
15 | },
16 | {
17 | path: 'edit/:id',
18 | component: EditContactComponent,
19 | },
20 | ];
21 |
--------------------------------------------------------------------------------
/src/app/components/add-contact.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, inject, signal } from '@angular/core';
2 | import { Router } from '@angular/router';
3 | import { ContactFormComponent } from './contact-form.component';
4 | import { Contact } from '../model/contact.model';
5 | import { ApiService } from '../services/api.service';
6 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
7 |
8 | @Component({
9 | selector: 'app-add-contact',
10 | standalone: true,
11 | imports: [ContactFormComponent, MatProgressSpinnerModule],
12 | template: `
13 |
14 | @if (saving()) {
15 |
19 | }
20 | `,
21 | })
22 | export class AddContactComponent {
23 | private router = inject(Router);
24 | private api = inject(ApiService);
25 |
26 | saving = signal(false);
27 |
28 | async addContact(newContact: Contact) {
29 | this.saving.set(true);
30 | await this.api.addContact(newContact);
31 | this.saving.set(false);
32 | this.router.navigate(['/']);
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/src/app/components/contact-form.component.ts:
--------------------------------------------------------------------------------
1 | import { Component, input, linkedSignal, output } from '@angular/core';
2 | import { FormsModule } from '@angular/forms';
3 | import { MatFormFieldModule } from '@angular/material/form-field';
4 | import { MatInputModule } from '@angular/material/input';
5 | import { MatButtonModule } from '@angular/material/button';
6 | import { Contact } from '../model/contact.model';
7 | import { RouterLink } from '@angular/router';
8 |
9 | @Component({
10 | selector: 'app-contact-form',
11 | standalone: true,
12 | imports: [
13 | FormsModule,
14 | MatFormFieldModule,
15 | MatInputModule,
16 | MatButtonModule,
17 | RouterLink,
18 | ],
19 | template: `
20 |
48 | `,
49 | styles: [
50 | `
51 | .container {
52 | padding: 24px;
53 | }
54 | .fields {
55 | display: grid;
56 | grid-template-columns: repeat(2, 1fr);
57 | gap: 8px;
58 | }
59 | .actions {
60 | display: flex;
61 | gap: 8px;
62 | margin-top: 16px;
63 | }
64 | `,
65 | ],
66 | })
67 | export class ContactFormComponent {
68 | title = input('');
69 | contact = input();
70 |
71 | name = linkedSignal(() => this.contact()?.name ?? '');
72 | email = linkedSignal(() => this.contact()?.email ?? '');
73 | phone = linkedSignal(() => this.contact()?.phone ?? '');
74 |
75 | save = output();
76 |
77 | onSubmit() {
78 | this.save.emit({
79 | id: this.contact()?.id ?? '',
80 | name: this.name(),
81 | email: this.email(),
82 | phone: this.phone(),
83 | });
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/src/app/components/contacts-list.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | computed,
4 | effect,
5 | inject,
6 | resource,
7 | signal,
8 | } from '@angular/core';
9 | import { MatListModule } from '@angular/material/list';
10 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
11 | import { MatButtonModule } from '@angular/material/button';
12 | import { MatIconModule } from '@angular/material/icon';
13 | import { RouterModule } from '@angular/router';
14 | import { ApiService } from '../services/api.service';
15 | import { MatSnackBar } from '@angular/material/snack-bar';
16 |
17 | @Component({
18 | selector: 'app-contacts-list',
19 | standalone: true,
20 | imports: [
21 | MatListModule,
22 | MatButtonModule,
23 | MatIconModule,
24 | MatProgressSpinnerModule,
25 | RouterModule,
26 | ],
27 | template: `
28 |
29 | @for (contact of contactsResource.value(); track contact.id) {
30 |
31 | {{ contact.name }}
32 | {{ contact.email }}
33 |
34 |
37 |
40 |
41 |
42 | }
43 |
44 | @if (loading()) {
45 |
49 | }
50 | `,
51 | })
52 | export class ContactsListComponent {
53 | apiService = inject(ApiService);
54 |
55 | contactsResource = resource({
56 | loader: () => this.apiService.getContacts(),
57 | });
58 |
59 | deleting = signal(false);
60 |
61 | loading = computed(
62 | () => this.deleting() || this.contactsResource.isLoading()
63 | );
64 |
65 | async deleteContact(id: string) {
66 | this.deleting.set(true);
67 | await this.apiService.deleteContact(id);
68 | this.deleting.set(false);
69 | this.contactsResource.reload();
70 | }
71 |
72 | snackbar = inject(MatSnackBar);
73 | showError = effect(() => {
74 | const error = this.contactsResource.error() as Error;
75 | if (error) {
76 | this.snackbar.open(error.message, 'Close', {
77 | verticalPosition: 'top',
78 | });
79 | }
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/src/app/components/edit-contact.component.ts:
--------------------------------------------------------------------------------
1 | import {
2 | Component,
3 | input,
4 | inject,
5 | resource,
6 | computed,
7 | signal,
8 | } from '@angular/core';
9 | import { ContactFormComponent } from './contact-form.component';
10 | import { ApiService } from '../services/api.service';
11 | import { Contact } from '../model/contact.model';
12 | import { Router } from '@angular/router';
13 | import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
14 | import { MatIconModule } from '@angular/material/icon';
15 | import { MatButtonModule } from '@angular/material/button';
16 |
17 | @Component({
18 | selector: 'app-edit-contact',
19 | standalone: true,
20 | imports: [
21 | ContactFormComponent,
22 | MatProgressSpinnerModule,
23 | MatIconModule,
24 | MatButtonModule,
25 | ],
26 | template: `
27 | @if (loading()) {
28 |
29 | } @if (contactResource.value(); as contact) {
30 |
35 | }
36 | `,
37 | })
38 | export class EditContactComponent {
39 | id = input.required();
40 | private router = inject(Router);
41 |
42 | private apiService = inject(ApiService);
43 |
44 | saving = signal(false);
45 |
46 | loading = computed(() => this.contactResource.isLoading() || this.saving());
47 |
48 | contactResource = resource({
49 | request: this.id,
50 | loader: ({ request: id }) => this.apiService.getContact(id),
51 | });
52 |
53 | async updateContact(contact: Contact) {
54 | this.saving.set(true);
55 | await this.apiService.updateContact(contact);
56 | this.saving.set(false);
57 | this.router.navigate(['/']);
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/app/model/contact.model.ts:
--------------------------------------------------------------------------------
1 | export interface Contact {
2 | id: string;
3 | name: string;
4 | phone: string;
5 | email: string;
6 | }
7 |
--------------------------------------------------------------------------------
/src/app/services/api.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@angular/core';
2 | import { Contact } from '../model/contact.model';
3 |
4 | @Injectable({
5 | providedIn: 'root',
6 | })
7 | export class ApiService {
8 | private delay = 1000; // Simulate network delay
9 | private contacts: Contact[] = [
10 | {
11 | id: '1',
12 | name: 'Bibbye Gutcher',
13 | phone: '885-131-9176',
14 | email: 'bgutcher0@smh.com.au',
15 | },
16 | {
17 | id: '2',
18 | name: 'John Smith',
19 | phone: '555-123-4567',
20 | email: 'john.smith@email.com',
21 | },
22 | {
23 | id: '3',
24 | name: 'Sarah Johnson',
25 | phone: '555-234-5678',
26 | email: 'sarah.j@email.com',
27 | },
28 | {
29 | id: '4',
30 | name: 'Michael Williams',
31 | phone: '555-345-6789',
32 | email: 'mwilliams@email.com',
33 | },
34 | {
35 | id: '5',
36 | name: 'Emma Brown',
37 | phone: '555-456-7890',
38 | email: 'emma.b@email.com',
39 | },
40 | {
41 | id: '6',
42 | name: 'James Davis',
43 | phone: '555-567-8901',
44 | email: 'james.d@email.com',
45 | },
46 | {
47 | id: '7',
48 | name: 'Lisa Garcia',
49 | phone: '555-678-9012',
50 | email: 'lisa.g@email.com',
51 | },
52 | {
53 | id: '8',
54 | name: 'David Miller',
55 | phone: '555-789-0123',
56 | email: 'david.m@email.com',
57 | },
58 | {
59 | id: '9',
60 | name: 'Jennifer Wilson',
61 | phone: '555-890-1234',
62 | email: 'jwilson@email.com',
63 | },
64 | {
65 | id: '10',
66 | name: 'Robert Taylor',
67 | phone: '555-901-2345',
68 | email: 'rtaylor@email.com',
69 | },
70 | {
71 | id: '11',
72 | name: 'Maria Martinez',
73 | phone: '555-012-3456',
74 | email: 'maria.m@email.com',
75 | },
76 | {
77 | id: '12',
78 | name: 'Daniel Anderson',
79 | phone: '555-123-7890',
80 | email: 'dan.a@email.com',
81 | },
82 | {
83 | id: '13',
84 | name: 'Patricia Thomas',
85 | phone: '555-234-8901',
86 | email: 'pat.t@email.com',
87 | },
88 | {
89 | id: '14',
90 | name: 'Kevin Lee',
91 | phone: '555-345-9012',
92 | email: 'kevin.l@email.com',
93 | },
94 | {
95 | id: '15',
96 | name: 'Nancy White',
97 | phone: '555-456-0123',
98 | email: 'nancy.w@email.com',
99 | },
100 | {
101 | id: '16',
102 | name: 'Christopher Moore',
103 | phone: '555-567-1234',
104 | email: 'chris.m@email.com',
105 | },
106 | {
107 | id: '17',
108 | name: 'Amanda Jackson',
109 | phone: '555-678-2345',
110 | email: 'amanda.j@email.com',
111 | },
112 | {
113 | id: '18',
114 | name: 'Joseph Martin',
115 | phone: '555-789-3456',
116 | email: 'joe.m@email.com',
117 | },
118 | {
119 | id: '19',
120 | name: 'Michelle Thompson',
121 | phone: '555-890-4567',
122 | email: 'michelle.t@email.com',
123 | },
124 | {
125 | id: '20',
126 | name: 'Ryan Rodriguez',
127 | phone: '555-901-5678',
128 | email: 'ryan.r@email.com',
129 | },
130 | {
131 | id: '21',
132 | name: 'Sandra Lewis',
133 | phone: '555-012-6789',
134 | email: 'sandra.l@email.com',
135 | },
136 | ];
137 |
138 | private generateUniqueId(): string {
139 | const existingIds = this.contacts.map((c) => parseInt(c.id));
140 | const maxId = Math.max(...existingIds);
141 | return (maxId + 1).toString();
142 | }
143 |
144 | async getContacts(): Promise {
145 | await this.simulateDelay();
146 |
147 | // throw new Error('Error fetching contacts');
148 |
149 | return [...this.contacts];
150 | }
151 |
152 | async addContact(contact: Contact): Promise {
153 | await this.simulateDelay();
154 | const newContact = {
155 | ...contact,
156 | id: this.generateUniqueId(),
157 | };
158 | this.contacts = [newContact, ...this.contacts];
159 | return newContact;
160 | }
161 |
162 | async deleteContact(id: string): Promise {
163 | await this.simulateDelay();
164 | this.contacts = this.contacts.filter((c) => c.id !== id);
165 | }
166 |
167 | async updateContact(updatedContact: Contact): Promise {
168 | await this.simulateDelay();
169 | const index = this.contacts.findIndex((c) => c.id === updatedContact.id);
170 | if (index === -1) {
171 | throw new Error('Contact not found');
172 | }
173 | this.contacts[index] = updatedContact;
174 | return updatedContact;
175 | }
176 |
177 | async getContact(id: string): Promise {
178 | await this.simulateDelay();
179 | const contact = this.contacts.find((c) => c.id === id);
180 | if (!contact) {
181 | throw new Error('Contact not found');
182 | }
183 | return contact;
184 | }
185 |
186 | private simulateDelay(): Promise {
187 | return new Promise((resolve) => setTimeout(resolve, this.delay));
188 | }
189 | }
190 |
--------------------------------------------------------------------------------
/src/assets/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thisiszoaib/angular-signals-crud/a1cfa3f8759d722cda5bc23ad41fe3a4c34a92df/src/assets/.gitkeep
--------------------------------------------------------------------------------
/src/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/thisiszoaib/angular-signals-crud/a1cfa3f8759d722cda5bc23ad41fe3a4c34a92df/src/favicon.ico
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | AngularSignalsCrud
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/main.ts:
--------------------------------------------------------------------------------
1 | import { bootstrapApplication } from '@angular/platform-browser';
2 | import { appConfig } from './app/app.config';
3 | import { AppComponent } from './app/app.component';
4 |
5 | bootstrapApplication(AppComponent, appConfig)
6 | .catch((err) => console.error(err));
7 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | /* You can add global styles to this file, and also import other style files */
2 |
3 | @use "@angular/material" as mat;
4 |
5 | html {
6 | @include mat.theme(
7 | (
8 | color: mat.$azure-palette,
9 | typography: Roboto,
10 | )
11 | );
12 | }
13 |
14 | html,
15 | body {
16 | height: 100%;
17 | }
18 | body {
19 | margin: 0;
20 | font-family: Roboto, "Helvetica Neue", sans-serif;
21 | }
22 |
23 | mat-progress-spinner {
24 | position: absolute !important;
25 | top: 50%;
26 | left: 50%;
27 | transform: translate(-50%, -50%);
28 | }
29 |
--------------------------------------------------------------------------------
/tsconfig.app.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/app",
6 | "types": []
7 | },
8 | "files": [
9 | "src/main.ts"
10 | ],
11 | "include": [
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "compileOnSave": false,
4 | "compilerOptions": {
5 | "baseUrl": "./",
6 | "outDir": "./dist/out-tsc",
7 | "forceConsistentCasingInFileNames": true,
8 | "esModuleInterop": true,
9 | "strict": true,
10 | "noImplicitOverride": true,
11 | "noPropertyAccessFromIndexSignature": true,
12 | "noImplicitReturns": true,
13 | "noFallthroughCasesInSwitch": true,
14 | "sourceMap": true,
15 | "declaration": false,
16 | "experimentalDecorators": true,
17 | "moduleResolution": "node",
18 | "importHelpers": true,
19 | "target": "ES2022",
20 | "module": "ES2022",
21 | "useDefineForClassFields": false,
22 | "lib": [
23 | "ES2022",
24 | "dom"
25 | ],
26 | },
27 | "angularCompilerOptions": {
28 | "enableI18nLegacyMessageIdFormat": false,
29 | "strictInjectionParameters": true,
30 | "strictInputAccessModifiers": true,
31 | "strictTemplates": true
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/tsconfig.spec.json:
--------------------------------------------------------------------------------
1 | /* To learn more about this file see: https://angular.io/config/tsconfig. */
2 | {
3 | "extends": "./tsconfig.json",
4 | "compilerOptions": {
5 | "outDir": "./out-tsc/spec",
6 | "types": [
7 | "jasmine"
8 | ]
9 | },
10 | "include": [
11 | "src/**/*.spec.ts",
12 | "src/**/*.d.ts"
13 | ]
14 | }
15 |
--------------------------------------------------------------------------------