├── .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 |
21 |

{{ title() }}

22 |
23 | 24 | 25 | 26 | 27 | 33 | 34 | 35 | 41 | 42 |
43 |
44 | 45 | 46 |
47 |
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 | --------------------------------------------------------------------------------