├── apm-begin ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── images │ │ │ └── logo.jpg │ ├── favicon.ico │ ├── app │ │ ├── cart │ │ │ ├── cart.ts │ │ │ ├── cart.service.ts │ │ │ ├── cart-total │ │ │ │ ├── cart-total.component.ts │ │ │ │ └── cart-total.component.html │ │ │ ├── cart-list │ │ │ │ ├── cart-list.component.ts │ │ │ │ └── cart-list.component.html │ │ │ ├── cart-shell │ │ │ │ └── cart-shell.component.ts │ │ │ └── cart-item │ │ │ │ ├── cart-item.component.ts │ │ │ │ └── cart-item.component.html │ │ ├── reviews │ │ │ ├── review.ts │ │ │ ├── review.service.ts │ │ │ └── review-data.ts │ │ ├── app.component.css │ │ ├── home │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── products │ │ │ ├── product.service.ts │ │ │ ├── product.ts │ │ │ ├── product-detail │ │ │ │ ├── product-detail.component.ts │ │ │ │ └── product-detail.component.html │ │ │ ├── product-list │ │ │ │ ├── product-list.component.ts │ │ │ │ └── product-list.component.html │ │ │ └── product-data.ts │ │ ├── utilities │ │ │ ├── page-not-found.component.ts │ │ │ └── http-error.service.ts │ │ ├── app-data.ts │ │ ├── app.config.ts │ │ ├── app.component.ts │ │ ├── app.routes.ts │ │ └── app.component.html │ ├── main.ts │ ├── styles.css │ └── index.html ├── .vscode │ ├── extensions.json │ ├── settings.json │ ├── launch.json │ └── tasks.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── angular.json ├── apm-final ├── src │ ├── assets │ │ ├── .gitkeep │ │ └── images │ │ │ └── logo.jpg │ ├── favicon.ico │ ├── app │ │ ├── cart │ │ │ ├── cart.ts │ │ │ ├── cart-shell │ │ │ │ └── cart-shell.component.ts │ │ │ ├── cart-list │ │ │ │ ├── cart-list.component.ts │ │ │ │ └── cart-list.component.html │ │ │ ├── cart-total │ │ │ │ ├── cart-total.component.ts │ │ │ │ └── cart-total.component.html │ │ │ ├── cart-item │ │ │ │ ├── cart-item.component.html │ │ │ │ └── cart-item.component.ts │ │ │ └── cart.service.ts │ │ ├── reviews │ │ │ ├── review.ts │ │ │ ├── review.service.ts │ │ │ └── review-data.ts │ │ ├── app.component.css │ │ ├── home │ │ │ ├── home.component.ts │ │ │ └── home.component.html │ │ ├── utilities │ │ │ ├── page-not-found.component.ts │ │ │ └── http-error.service.ts │ │ ├── products │ │ │ ├── product.ts │ │ │ ├── product-list │ │ │ │ ├── product-list.component.html │ │ │ │ └── product-list.component.ts │ │ │ ├── product-detail │ │ │ │ ├── product-detail.component.ts │ │ │ │ └── product-detail.component.html │ │ │ ├── product-data.ts │ │ │ └── product.service.ts │ │ ├── app-data.ts │ │ ├── app.config.ts │ │ ├── app.component.ts │ │ ├── app.routes.ts │ │ └── app.component.html │ ├── main.ts │ ├── styles.css │ └── index.html ├── .vscode │ ├── extensions.json │ ├── settings.json │ ├── launch.json │ └── tasks.json ├── tsconfig.app.json ├── tsconfig.spec.json ├── .editorconfig ├── .gitignore ├── tsconfig.json ├── README.md ├── package.json └── angular.json ├── faq.md ├── LICENSE ├── README.md └── links.md /apm-begin/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apm-final/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /apm-begin/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/angular-rxjs-signals-fundamentals/HEAD/apm-begin/src/favicon.ico -------------------------------------------------------------------------------- /apm-final/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/angular-rxjs-signals-fundamentals/HEAD/apm-final/src/favicon.ico -------------------------------------------------------------------------------- /apm-begin/src/assets/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/angular-rxjs-signals-fundamentals/HEAD/apm-begin/src/assets/images/logo.jpg -------------------------------------------------------------------------------- /apm-final/src/assets/images/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DeborahK/angular-rxjs-signals-fundamentals/HEAD/apm-final/src/assets/images/logo.jpg -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../products/product"; 2 | 3 | export interface CartItem { 4 | product: Product; 5 | quantity: number; 6 | } 7 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart.ts: -------------------------------------------------------------------------------- 1 | import { Product } from "../products/product"; 2 | 3 | export interface CartItem { 4 | product: Product; 5 | quantity: number; 6 | } 7 | -------------------------------------------------------------------------------- /apm-begin/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from "@angular/core"; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class CartService { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /apm-final/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 3 | "recommendations": ["angular.ng-template"] 4 | } 5 | -------------------------------------------------------------------------------- /apm-begin/src/app/reviews/review.ts: -------------------------------------------------------------------------------- 1 | /* Defines a product review */ 2 | export interface Review { 3 | id: number; 4 | productId: number; 5 | userName: string; 6 | title: string; 7 | text: string; 8 | } 9 | -------------------------------------------------------------------------------- /apm-final/src/app/reviews/review.ts: -------------------------------------------------------------------------------- 1 | /* Defines a product review */ 2 | export interface Review { 3 | id: number; 4 | productId: number; 5 | userName: string; 6 | title: string; 7 | text: string; 8 | } 9 | -------------------------------------------------------------------------------- /apm-begin/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | font-size: large; 3 | cursor: pointer; 4 | } 5 | 6 | .navbar-light .navbar-nav .nav-link.active { 7 | color: #007ACC 8 | } 9 | 10 | .navbar-brand { 11 | margin-left: 10px; 12 | } -------------------------------------------------------------------------------- /apm-final/src/app/app.component.css: -------------------------------------------------------------------------------- 1 | .nav-link { 2 | font-size: large; 3 | cursor: pointer; 4 | } 5 | 6 | .navbar-light .navbar-nav .nav-link.active { 7 | color: #007ACC 8 | } 9 | 10 | .navbar-brand { 11 | margin-left: 10px; 12 | } -------------------------------------------------------------------------------- /apm-begin/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.autoSave": "afterDelay", 3 | "html.format.wrapAttributes": "force-aligned", 4 | // "editor.quickSuggestionsDelay": 1000, 5 | //"editor.hover.enabled": false, 6 | // "editor.parameterHints": false, 7 | } -------------------------------------------------------------------------------- /apm-final/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "files.autoSave": "afterDelay", 3 | "html.format.wrapAttributes": "force-aligned", 4 | // "editor.quickSuggestionsDelay": 1000, 5 | //"editor.hover.enabled": false, 6 | // "editor.parameterHints": false, 7 | } -------------------------------------------------------------------------------- /apm-begin/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './home.component.html', 5 | standalone: true 6 | }) 7 | export class HomeComponent { 8 | public pageTitle = 'Welcome'; 9 | } 10 | -------------------------------------------------------------------------------- /apm-final/src/app/home/home.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | templateUrl: './home.component.html', 5 | standalone: true 6 | }) 7 | export class HomeComponent { 8 | public pageTitle = 'Welcome'; 9 | } 10 | -------------------------------------------------------------------------------- /apm-begin/src/app/products/product.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ProductService { 7 | // Just enough here for the code to compile 8 | private productsUrl = 'api/products'; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /apm-begin/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 | -------------------------------------------------------------------------------- /apm-final/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 | -------------------------------------------------------------------------------- /apm-begin/src/app/utilities/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

This is not the page you were looking for!

6 | `, 7 | standalone: true 8 | }) 9 | export class PageNotFoundComponent { } 10 | -------------------------------------------------------------------------------- /apm-final/src/app/utilities/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | template: ` 5 |

This is not the page you were looking for!

6 | `, 7 | standalone: true 8 | }) 9 | export class PageNotFoundComponent { } 10 | -------------------------------------------------------------------------------- /apm-begin/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~bootstrap/dist/css/bootstrap.min.css"; 3 | 4 | div.card-header { 5 | font-size: large; 6 | } 7 | 8 | div.card { 9 | margin-top: 20px; 10 | margin-left: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /apm-final/src/styles.css: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~bootstrap/dist/css/bootstrap.min.css"; 3 | 4 | div.card-header { 5 | font-size: large; 6 | } 7 | 8 | div.card { 9 | margin-top: 20px; 10 | margin-left: 10px; 11 | } 12 | -------------------------------------------------------------------------------- /apm-begin/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 | -------------------------------------------------------------------------------- /apm-final/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 | -------------------------------------------------------------------------------- /apm-begin/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 | -------------------------------------------------------------------------------- /apm-final/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 | -------------------------------------------------------------------------------- /apm-begin/.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 | -------------------------------------------------------------------------------- /apm-begin/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Apm 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apm-final/.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 | -------------------------------------------------------------------------------- /apm-final/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Apm 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /apm-begin/src/app/products/product.ts: -------------------------------------------------------------------------------- 1 | import { Review } from "../reviews/review"; 2 | 3 | /* Defines the product entity */ 4 | export interface Product { 5 | id: number; 6 | productName: string; 7 | productCode: string; 8 | description: string; 9 | price: number; 10 | quantityInStock?: number; 11 | hasReviews?: boolean; 12 | reviews?: Review[]; 13 | } 14 | -------------------------------------------------------------------------------- /apm-final/src/app/reviews/review.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ReviewService { 7 | private reviewsUrl = 'api/reviews'; 8 | 9 | getReviewUrl(productId: number): string { 10 | // Use appropriate regular expression syntax to 11 | // get an exact match on the id 12 | return this.reviewsUrl + '?productId=^' + productId + '$'; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /apm-final/src/app/products/product.ts: -------------------------------------------------------------------------------- 1 | import { Review } from "../reviews/review"; 2 | 3 | /* Defines the product entity */ 4 | export interface Product { 5 | id: number; 6 | productName: string; 7 | productCode: string; 8 | description: string; 9 | price: number; 10 | quantityInStock?: number; 11 | hasReviews?: boolean; 12 | reviews?: Review[]; 13 | } 14 | 15 | export interface Result { 16 | data: T | undefined; 17 | error?: string; 18 | } 19 | -------------------------------------------------------------------------------- /apm-begin/src/app/reviews/review.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable({ 4 | providedIn: 'root' 5 | }) 6 | export class ReviewService { 7 | // Just enough here for the code to compile 8 | private reviewsUrl = 'api/reviews'; 9 | 10 | getReviewUrl(productId: number): string { 11 | // Use appropriate regular expression syntax to 12 | // get an exact match on the id 13 | return this.reviewsUrl + '?productId=^' + productId + '$'; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apm-begin/.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 | -------------------------------------------------------------------------------- /apm-begin/src/app/app-data.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 2 | 3 | import { ProductData } from './products/product-data'; 4 | import { ReviewData } from './reviews/review-data'; 5 | import { Product } from './products/product'; 6 | import { Review } from './reviews/review'; 7 | 8 | export class AppData implements InMemoryDbService { 9 | 10 | createDb(): { products: Product[], reviews: Review[]} { 11 | const products = ProductData.products; 12 | const reviews = ReviewData.reviews; 13 | return { products, reviews }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apm-final/.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 | -------------------------------------------------------------------------------- /apm-final/src/app/app-data.ts: -------------------------------------------------------------------------------- 1 | import { InMemoryDbService } from 'angular-in-memory-web-api'; 2 | 3 | import { ProductData } from './products/product-data'; 4 | import { ReviewData } from './reviews/review-data'; 5 | import { Product } from './products/product'; 6 | import { Review } from './reviews/review'; 7 | 8 | export class AppData implements InMemoryDbService { 9 | 10 | createDb(): { products: Product[], reviews: Review[]} { 11 | const products = ProductData.products; 12 | const reviews = ReviewData.reviews; 13 | return { products, reviews }; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-total/cart-total.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgIf, CurrencyPipe } from '@angular/common'; 3 | 4 | @Component({ 5 | selector: 'sw-cart-total', 6 | templateUrl: './cart-total.component.html', 7 | standalone: true, 8 | imports: [NgIf, CurrencyPipe] 9 | }) 10 | export class CartTotalComponent { 11 | // Just enough here for the template to compile 12 | cartItems = []; 13 | 14 | subTotal = 100; 15 | deliveryFee = 20; 16 | tax = 10; 17 | totalPrice = this.subTotal + this.deliveryFee + this.tax; 18 | 19 | } 20 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-list/cart-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { NgFor, NgIf } from '@angular/common'; 3 | import { CartItem } from '../cart'; 4 | import { CartItemComponent } from '../cart-item/cart-item.component'; 5 | 6 | @Component({ 7 | selector: 'sw-cart-list', 8 | standalone: true, 9 | imports: [CartItemComponent, NgFor, NgIf], 10 | templateUrl: 'cart-list.component.html' 11 | }) 12 | export class CartListComponent { 13 | // Just enough here for the template to compile 14 | pageTitle = 'Cart'; 15 | 16 | cartItems: CartItem[] = []; 17 | } 18 | -------------------------------------------------------------------------------- /apm-begin/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; 4 | import { AppData } from './app-data'; 5 | import { provideRouter } from '@angular/router'; 6 | import { routes } from './app.routes'; 7 | 8 | export const appConfig: ApplicationConfig = { 9 | providers: [ 10 | importProvidersFrom( 11 | FormsModule, 12 | InMemoryWebApiModule.forRoot(AppData, { delay: 1000 }) 13 | ), 14 | provideRouter(routes) 15 | ] 16 | }; 17 | -------------------------------------------------------------------------------- /apm-begin/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | //import 'zone.js/dist/zone'; // Required for Stackblitz 2 | import { Component } from '@angular/core'; 3 | import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; 4 | 5 | @Component({ 6 | selector: 'pm-root', 7 | standalone: true, 8 | imports: [RouterLinkActive, RouterLink, RouterOutlet], 9 | templateUrl: './app.component.html', 10 | styleUrls: ['./app.component.css'] 11 | }) 12 | export class AppComponent { 13 | // Just enough here for the template to compile 14 | pageTitle = 'Acme Product Management'; 15 | 16 | cartCount = 0; 17 | 18 | } 19 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-shell/cart-shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CartTotalComponent } from '../cart-total/cart-total.component'; 3 | import { CartListComponent } from '../cart-list/cart-list.component'; 4 | 5 | @Component({ 6 | standalone: true, 7 | imports: [CartListComponent, CartTotalComponent], 8 | template: ` 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | ` 18 | }) 19 | export class CartShellComponent { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-shell/cart-shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { CartTotalComponent } from '../cart-total/cart-total.component'; 3 | import { CartListComponent } from '../cart-list/cart-list.component'; 4 | 5 | @Component({ 6 | standalone: true, 7 | imports: [CartListComponent, CartTotalComponent], 8 | template: ` 9 |
10 | 11 |
12 |
13 |
14 | 15 |
16 |
17 | ` 18 | }) 19 | export class CartShellComponent { 20 | 21 | } 22 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-list/cart-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { NgFor, NgIf } from '@angular/common'; 3 | import { CartItemComponent } from '../cart-item/cart-item.component'; 4 | import { CartService } from '../cart.service'; 5 | 6 | @Component({ 7 | selector: 'sw-cart-list', 8 | standalone: true, 9 | imports: [CartItemComponent, NgFor, NgIf], 10 | templateUrl: 'cart-list.component.html' 11 | }) 12 | export class CartListComponent { 13 | pageTitle = 'Cart'; 14 | 15 | private cartService = inject(CartService); 16 | 17 | cartItems = this.cartService.cartItems; 18 | } 19 | -------------------------------------------------------------------------------- /apm-final/src/app/app.config.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationConfig, importProvidersFrom } from '@angular/core'; 2 | import { FormsModule } from '@angular/forms'; 3 | import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; 4 | import { AppData } from './app-data'; 5 | import { provideRouter } from '@angular/router'; 6 | import { routes } from './app.routes'; 7 | import { provideHttpClient } from '@angular/common/http'; 8 | 9 | export const appConfig: ApplicationConfig = { 10 | providers: [ 11 | provideHttpClient(), 12 | importProvidersFrom( 13 | FormsModule, 14 | InMemoryWebApiModule.forRoot(AppData, { delay: 1000 }) 15 | ), 16 | provideRouter(routes) 17 | ] 18 | }; 19 | -------------------------------------------------------------------------------- /apm-final/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | //import 'zone.js/dist/zone'; // Required for Stackblitz 2 | import { Component, inject } from '@angular/core'; 3 | import { RouterLinkActive, RouterLink, RouterOutlet } from '@angular/router'; 4 | import { CartService } from './cart/cart.service'; 5 | 6 | @Component({ 7 | selector: 'pm-root', 8 | standalone: true, 9 | imports: [RouterLinkActive, RouterLink, RouterOutlet], 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.css'] 12 | }) 13 | export class AppComponent { 14 | pageTitle = 'Acme Product Management'; 15 | 16 | private cartService = inject(CartService); 17 | 18 | cartCount = this.cartService.cartCount; 19 | 20 | } 21 | -------------------------------------------------------------------------------- /faq.md: -------------------------------------------------------------------------------- 1 | # Frequently Asked Questions 2 | 3 | #### Q: How do I set up a provider (for HttpClient)? 4 | 5 | **A:** If you are using standalone components, including standalone bootstrapping to launch your application, pull in the HTTPClient service provider as part of the application configuration. That's in the app.config.ts file. 6 | 7 | If you are using NgModules to bootstrap the application instead, import HttpClientModule into the AppModule. In the app.module.ts file, add HttpClientModule to the imports array. 8 | 9 | **Demo:** [Here is the link to the location in the course that demonstrates setting up a provider.](https://app.pluralsight.com/course-player?clipId=b7e13b5c-64d2-4404-ac38-c6f8a4b221ad&startTime=25.829) 10 | -------------------------------------------------------------------------------- /apm-begin/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { HomeComponent } from './home/home.component'; 3 | import { PageNotFoundComponent } from './utilities/page-not-found.component'; 4 | 5 | export const routes: Routes = [ 6 | { path: 'welcome', component: HomeComponent }, 7 | { 8 | path: 'products', 9 | loadComponent: () => import('./products/product-list/product-list.component').then(c => c.ProductListComponent) 10 | }, 11 | { 12 | path: 'cart', 13 | loadComponent: () => import('./cart/cart-shell/cart-shell.component').then(c => c.CartShellComponent) 14 | }, 15 | { path: '', redirectTo: 'welcome', pathMatch: 'full' }, 16 | { path: '**', component: PageNotFoundComponent }]; -------------------------------------------------------------------------------- /apm-final/src/app/app.routes.ts: -------------------------------------------------------------------------------- 1 | import { Routes } from '@angular/router'; 2 | import { HomeComponent } from './home/home.component'; 3 | import { PageNotFoundComponent } from './utilities/page-not-found.component'; 4 | 5 | export const routes: Routes = [ 6 | { path: 'welcome', component: HomeComponent }, 7 | { 8 | path: 'products', 9 | loadComponent: () => import('./products/product-list/product-list.component').then(c => c.ProductListComponent) 10 | }, 11 | { 12 | path: 'cart', 13 | loadComponent: () => import('./cart/cart-shell/cart-shell.component').then(c => c.CartShellComponent) 14 | }, 15 | { path: '', redirectTo: 'welcome', pathMatch: 'full' }, 16 | { path: '**', component: PageNotFoundComponent }]; -------------------------------------------------------------------------------- /apm-begin/.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 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /apm-final/.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 | # Miscellaneous 32 | /.angular/cache 33 | .sass-cache/ 34 | /connect.lock 35 | /coverage 36 | /libpeerconnection.log 37 | testem.log 38 | /typings 39 | 40 | # System files 41 | .DS_Store 42 | Thumbs.db 43 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-total/cart-total.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | import { NgIf, CurrencyPipe } from '@angular/common'; 3 | import { CartService } from '../cart.service'; 4 | 5 | @Component({ 6 | selector: 'sw-cart-total', 7 | templateUrl: './cart-total.component.html', 8 | standalone: true, 9 | imports: [NgIf, CurrencyPipe] 10 | }) 11 | export class CartTotalComponent { 12 | 13 | private cartService = inject(CartService); 14 | 15 | // Reference the service signals for binding 16 | cartItems = this.cartService.cartItems; 17 | subTotal = this.cartService.subTotal; 18 | deliveryFee = this.cartService.deliveryFee; 19 | tax = this.cartService.tax; 20 | totalPrice = this.cartService.totalPrice; 21 | 22 | } 23 | -------------------------------------------------------------------------------- /apm-begin/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{pageTitle}} 4 |
5 |
6 |
7 |
8 | 11 |
12 | 13 |
Developed by:
14 |
15 |

Deborah Kurata

16 |
17 | 18 |
@deborahkurata
19 |
20 | YouTube Channel 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /apm-final/src/app/home/home.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{pageTitle}} 4 |
5 |
6 |
7 |
8 | 11 |
12 | 13 |
Developed by:
14 |
15 |

Deborah Kurata

16 |
17 | 18 |
@deborahkurata
19 |
20 | YouTube Channel 21 |
22 |
23 |
24 |
-------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-list/cart-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ pageTitle }} 4 |
5 | 6 |
7 |
8 |
9 |
Product
10 |
Price
11 |
Quantity
12 |
Extended Price
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | No items in cart 23 |
24 | -------------------------------------------------------------------------------- /apm-begin/src/app/products/product-detail/product-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | 3 | import { NgIf, NgFor, CurrencyPipe } from '@angular/common'; 4 | import { Product } from '../product'; 5 | 6 | @Component({ 7 | selector: 'pm-product-detail', 8 | templateUrl: './product-detail.component.html', 9 | standalone: true, 10 | imports: [NgIf, NgFor, CurrencyPipe] 11 | }) 12 | export class ProductDetailComponent { 13 | // Just enough here for the template to compile 14 | @Input() productId: number = 0; 15 | errorMessage = ''; 16 | 17 | // Product to display 18 | product: Product | null = null; 19 | 20 | // Set the page title 21 | pageTitle = this.product ? `Product Detail for: ${this.product.productName}` : 'Product Detail'; 22 | 23 | addToCart(product: Product) { 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-list/cart-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | {{ pageTitle }} 4 |
5 | 6 |
7 |
8 |
9 |
Product
10 |
Price
11 |
Quantity
12 |
Extended Price
13 |
14 | 15 | 16 |
17 | 18 |
19 |
20 |
21 | 22 | No items in cart 23 |
-------------------------------------------------------------------------------- /apm-begin/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 |
-------------------------------------------------------------------------------- /apm-final/src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 23 | 24 |
25 | 26 |
-------------------------------------------------------------------------------- /apm-begin/src/app/products/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | import { NgIf, NgFor, NgClass } from '@angular/common'; 4 | import { Product } from '../product'; 5 | import { ProductDetailComponent } from '../product-detail/product-detail.component'; 6 | 7 | @Component({ 8 | selector: 'pm-product-list', 9 | templateUrl: './product-list.component.html', 10 | standalone: true, 11 | imports: [NgIf, NgFor, NgClass, ProductDetailComponent] 12 | }) 13 | export class ProductListComponent { 14 | // Just enough here for the template to compile 15 | pageTitle = 'Products'; 16 | errorMessage = ''; 17 | 18 | // Products 19 | products: Product[] = []; 20 | 21 | // Selected product id to highlight the entry 22 | selectedProductId: number = 0; 23 | 24 | onSelected(productId: number): void { 25 | this.selectedProductId = productId; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-item/cart-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input } from '@angular/core'; 2 | import { CurrencyPipe, NgFor, NgIf } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { CartItem } from '../cart'; 6 | 7 | @Component({ 8 | selector: 'sw-cart-item', 9 | standalone: true, 10 | imports: [CurrencyPipe, FormsModule, NgFor, NgIf], 11 | templateUrl: './cart-item.component.html' 12 | }) 13 | export class CartItemComponent { 14 | 15 | @Input({ required: true }) cartItem!: CartItem; 16 | 17 | // Quantity available (hard-coded to 8) 18 | // Mapped to an array from 1-8 19 | qtyArr = [...Array(8).keys()].map(x => x + 1); 20 | 21 | // Calculate the extended price 22 | exPrice = this.cartItem?.quantity * this.cartItem?.product.price; 23 | 24 | onQuantitySelected(quantity: number): void { 25 | 26 | } 27 | 28 | removeFromCart(): void { 29 | 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /apm-final/src/app/products/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {{pageTitle}} 7 |
8 | 9 |
11 |
12 | 19 |
20 |
21 |
22 |
24 | {{errorMessage()}} 25 |
26 |
27 | 28 |
29 | 30 |
31 |
-------------------------------------------------------------------------------- /apm-begin/src/app/products/product-list/product-list.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 | 4 |
5 |
6 | {{pageTitle}} 7 |
8 | 9 |
11 |
12 | 19 |
20 |
21 |
22 |
24 | {{errorMessage}} 25 |
26 |
27 | 28 |
29 | 30 |
31 |
-------------------------------------------------------------------------------- /apm-final/src/app/products/product-list/product-list.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, inject } from '@angular/core'; 2 | 3 | import { NgIf, NgFor, NgClass, AsyncPipe } from '@angular/common'; 4 | import { ProductDetailComponent } from '../product-detail/product-detail.component'; 5 | import { ProductService } from '../product.service'; 6 | 7 | @Component({ 8 | selector: 'pm-product-list', 9 | templateUrl: './product-list.component.html', 10 | standalone: true, 11 | imports: [AsyncPipe, NgIf, NgFor, NgClass, ProductDetailComponent] 12 | }) 13 | export class ProductListComponent { 14 | pageTitle = 'Products'; 15 | 16 | private productService = inject(ProductService); 17 | 18 | // Products 19 | products = this.productService.products; 20 | errorMessage = this.productService.productsError; 21 | 22 | // Selected product id to highlight the entry 23 | selectedProductId = this.productService.selectedProductId; 24 | 25 | onSelected(productId: number): void { 26 | this.productService.productSelected(productId); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /apm-begin/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 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 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 | -------------------------------------------------------------------------------- /apm-final/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 | "strict": true, 9 | "noImplicitOverride": true, 10 | "noPropertyAccessFromIndexSignature": true, 11 | "noImplicitReturns": true, 12 | "noFallthroughCasesInSwitch": true, 13 | "sourceMap": true, 14 | "declaration": false, 15 | "downlevelIteration": true, 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 | -------------------------------------------------------------------------------- /apm-begin/src/app/utilities/http-error.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from "@angular/common/http"; 2 | import { Injectable } from "@angular/core"; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class HttpErrorService { 8 | 9 | formatError(err: HttpErrorResponse): string { 10 | return this.httpErrorFormatter(err); 11 | } 12 | 13 | private httpErrorFormatter(err: HttpErrorResponse): string { 14 | // In a real world app, we may send the error to some remote logging infrastructure 15 | // instead of just logging it to the console 16 | // console.error(err); 17 | let errorMessage = ''; 18 | if (err.error instanceof ErrorEvent) { 19 | // A client-side or network error occurred. Handle it accordingly. 20 | errorMessage = `An error occurred: ${err.error.message}`; 21 | } else { 22 | // The backend returned an unsuccessful response code. 23 | errorMessage = `Server returned code: ${err.status}, error message is: ${err.statusText}`; 24 | } 25 | return errorMessage; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apm-final/src/app/utilities/http-error.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpErrorResponse } from "@angular/common/http"; 2 | import { Injectable } from "@angular/core"; 3 | 4 | @Injectable({ 5 | providedIn: 'root', 6 | }) 7 | export class HttpErrorService { 8 | 9 | formatError(err: HttpErrorResponse): string { 10 | return this.httpErrorFormatter(err); 11 | } 12 | 13 | private httpErrorFormatter(err: HttpErrorResponse): string { 14 | // In a real world app, we may send the error to some remote logging infrastructure 15 | // instead of just logging it to the console 16 | // console.error(err); 17 | let errorMessage = ''; 18 | if (err.error instanceof ErrorEvent) { 19 | // A client-side or network error occurred. Handle it accordingly. 20 | errorMessage = `An error occurred: ${err.error.message}`; 21 | } else { 22 | // The backend returned an unsuccessful response code. 23 | errorMessage = `Server returned code: ${err.status}, error message is: ${err.statusText}`; 24 | } 25 | return errorMessage; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-item/cart-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {{ cartItem.product.productName }} 6 |
7 |
8 | {{ cartItem.product.price | currency }} 9 |
10 |
11 | 20 |
21 |
22 | {{ exPrice | currency }} 23 |
24 |
25 | 29 |
30 |
31 | 32 |
-------------------------------------------------------------------------------- /apm-begin/src/app/cart/cart-total/cart-total.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 | Cart Total 6 |
7 | 8 |
9 |
10 |
Subtotal:
11 |
{{subTotal | currency}}
12 |
13 |
14 |
Delivery:
15 |
{{deliveryFee | currency}}
17 |
Free
20 |
21 | 22 |
23 |
Estimated Tax:
24 |
{{tax | currency}}
25 |
26 | 27 |
28 |
Total:
29 |
{{totalPrice | currency}}
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-item/cart-item.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 |
5 | {{ cartItem.product.productName }} 6 |
7 |
8 | {{ cartItem.product.price | currency }} 9 |
10 |
11 | 20 |
21 |
22 | {{ exPrice() | currency }} 23 |
24 |
25 | 29 |
30 |
31 | 32 |
-------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-total/cart-total.component.html: -------------------------------------------------------------------------------- 1 |
3 | 4 |
5 | Cart Total 6 |
7 | 8 |
9 |
10 |
Subtotal:
11 |
{{subTotal() | currency}}
12 |
13 |
14 |
Delivery:
15 |
{{deliveryFee() | currency}}
17 |
Free
20 |
21 | 22 |
23 |
Estimated Tax:
24 |
{{tax() | currency}}
25 |
26 | 27 |
28 |
Total:
29 |
{{totalPrice() | currency}}
30 |
31 |
32 |
33 | -------------------------------------------------------------------------------- /apm-begin/.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 | -------------------------------------------------------------------------------- /apm-final/.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 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Deborah Kurata 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /apm-begin/README.md: -------------------------------------------------------------------------------- 1 | # Apm 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /apm-final/README.md: -------------------------------------------------------------------------------- 1 | # Apm 2 | 3 | This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 16.2.0. 4 | 5 | ## Development server 6 | 7 | Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The application will automatically reload if you change any of the source files. 8 | 9 | ## Code scaffolding 10 | 11 | Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|guard|interface|enum|module`. 12 | 13 | ## Build 14 | 15 | Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. 16 | 17 | ## Running unit tests 18 | 19 | Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). 20 | 21 | ## Running end-to-end tests 22 | 23 | Run `ng e2e` to execute the end-to-end tests via a platform of your choice. To use this command, you need to first add a package that implements end-to-end testing capabilities. 24 | 25 | ## Further help 26 | 27 | To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI Overview and Command Reference](https://angular.io/cli) page. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # angular-rxjs-signals-fundamentals 2 | Sample code for the ["RxJS and Angular Signal Fundamentals" course at Pluralsight](https://www.pluralsight.com/library/courses/rxjs-angular-signals-fundamentals) 3 | 4 | Using RxJS and signals with reactive programming techniques, our applications are more responsive to user actions and state changes, providing a better user experience with less code. 5 | 6 | In this course, we learn the fundamentals of RxJS and signals. And how they are powerful together, improving our state management and reactivity. 7 | 8 | Some of the major topics that we cover include: 9 | 10 | 1. Demystifying RxJS, breaking down its terms, syntax, and operators 11 | 2. Harnessing the power of Subjects to react effectively to user actions 12 | 3. Leveraging signals for easier state management and improved change detection 13 | 4. And using RxJS and Signals together, to get the best from both 14 | 15 | By the end of this course, you’ll have the skills and knowledge to better manage state and build more reactive Angular applications using RxJS and signals. 16 | 17 | I hope you’ll join me on this exciting journey to learn RxJS and signals with the "RxJS and Angular Signals Fundamentals" course, at Pluralsight. 18 | -------------------------------------------------------------------------------- /apm-final/src/app/products/product-detail/product-detail.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, computed, inject } from '@angular/core'; 2 | 3 | import { NgIf, NgFor, CurrencyPipe, AsyncPipe } from '@angular/common'; 4 | import { Product } from '../product'; 5 | import { ProductService } from '../product.service'; 6 | import { CartService } from 'src/app/cart/cart.service'; 7 | 8 | @Component({ 9 | selector: 'pm-product-detail', 10 | templateUrl: './product-detail.component.html', 11 | standalone: true, 12 | imports: [AsyncPipe, NgIf, NgFor, CurrencyPipe] 13 | }) 14 | export class ProductDetailComponent { 15 | 16 | private productService = inject(ProductService); 17 | private cartService = inject(CartService); 18 | 19 | // Product to display 20 | product = this.productService.product; 21 | errorMessage = this.productService.productError; 22 | 23 | // Set the page title 24 | pageTitle = computed(() => 25 | this.product() 26 | ? `Product Detail for: ${this.product()?.productName}` 27 | : 'Product Detail') 28 | 29 | // This does not currently prevent the user from 30 | // ordering more quantity than available in inventory 31 | addToCart(product: Product) { 32 | this.cartService.addToCart(product); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /apm-begin/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apm", 3 | "version": "0.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": "^16.2.0", 14 | "@angular/common": "^16.2.0", 15 | "@angular/compiler": "^16.2.0", 16 | "@angular/core": "^16.2.0", 17 | "@angular/forms": "^16.2.0", 18 | "@angular/platform-browser": "^16.2.0", 19 | "@angular/platform-browser-dynamic": "^16.2.0", 20 | "@angular/router": "^16.2.0", 21 | "angular-in-memory-web-api": "^0.16.0", 22 | "bootstrap": "^5.3.1", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.13.0" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^16.2.0", 29 | "@angular/cli": "~16.2.0", 30 | "@angular/compiler-cli": "^16.2.0", 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.1.0", 38 | "typescript": "~5.1.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apm-final/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "apm", 3 | "version": "0.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": "^16.2.0", 14 | "@angular/common": "^16.2.0", 15 | "@angular/compiler": "^16.2.0", 16 | "@angular/core": "^16.2.0", 17 | "@angular/forms": "^16.2.0", 18 | "@angular/platform-browser": "^16.2.0", 19 | "@angular/platform-browser-dynamic": "^16.2.0", 20 | "@angular/router": "^16.2.0", 21 | "angular-in-memory-web-api": "^0.16.0", 22 | "bootstrap": "^5.3.1", 23 | "rxjs": "~7.8.0", 24 | "tslib": "^2.3.0", 25 | "zone.js": "~0.13.0" 26 | }, 27 | "devDependencies": { 28 | "@angular-devkit/build-angular": "^16.2.0", 29 | "@angular/cli": "~16.2.0", 30 | "@angular/compiler-cli": "^16.2.0", 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.1.0", 38 | "typescript": "~5.1.3" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /apm-begin/src/app/products/product-data.ts: -------------------------------------------------------------------------------- 1 | import { Product } from './product'; 2 | 3 | export class ProductData { 4 | 5 | static products: Product[] = [ 6 | { 7 | id: 1, 8 | productName: 'Leaf Rake', 9 | productCode: 'GDN-0011', 10 | description: 'Leaf rake with 48-inch wooden handle', 11 | price: 19.95, 12 | quantityInStock: 15, 13 | hasReviews: true 14 | }, 15 | { 16 | id: 2, 17 | productName: 'Garden Cart', 18 | productCode: 'GDN-0023', 19 | description: '15 gallon capacity rolling garden cart', 20 | price: 32.99, 21 | quantityInStock: 2, 22 | hasReviews: true 23 | }, 24 | { 25 | id: 5, 26 | productName: 'Hammer', 27 | productCode: 'TBX-0048', 28 | description: 'Curved claw steel hammer', 29 | price: 8.9, 30 | quantityInStock: 8, 31 | hasReviews: true 32 | }, 33 | { 34 | id: 8, 35 | productName: 'Saw', 36 | productCode: 'TBX-0022', 37 | description: '15-inch steel blade hand saw', 38 | price: 11.55, 39 | quantityInStock: 6, 40 | hasReviews: false 41 | }, 42 | { 43 | id: 10, 44 | productName: 'Video Game Controller', 45 | productCode: 'GMG-0042', 46 | description: 'Standard two-button video game controller', 47 | price: 35.95, 48 | quantityInStock: 12, 49 | hasReviews: true 50 | } 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /apm-final/src/app/products/product-data.ts: -------------------------------------------------------------------------------- 1 | import { Product } from './product'; 2 | 3 | export class ProductData { 4 | 5 | static products: Product[] = [ 6 | { 7 | id: 1, 8 | productName: 'Leaf Rake', 9 | productCode: 'GDN-0011', 10 | description: 'Leaf rake with 48-inch wooden handle', 11 | price: 19.95, 12 | quantityInStock: 15, 13 | hasReviews: true 14 | }, 15 | { 16 | id: 2, 17 | productName: 'Garden Cart', 18 | productCode: 'GDN-0023', 19 | description: '15 gallon capacity rolling garden cart', 20 | price: 32.99, 21 | quantityInStock: 2, 22 | hasReviews: true 23 | }, 24 | { 25 | id: 5, 26 | productName: 'Hammer', 27 | productCode: 'TBX-0048', 28 | description: 'Curved claw steel hammer', 29 | price: 8.9, 30 | quantityInStock: 8, 31 | hasReviews: true 32 | }, 33 | { 34 | id: 8, 35 | productName: 'Saw', 36 | productCode: 'TBX-0022', 37 | description: '15-inch steel blade hand saw', 38 | price: 11.55, 39 | quantityInStock: 6, 40 | hasReviews: false 41 | }, 42 | { 43 | id: 10, 44 | productName: 'Video Game Controller', 45 | productCode: 'GMG-0042', 46 | description: 'Standard two-button video game controller', 47 | price: 35.95, 48 | quantityInStock: 12, 49 | hasReviews: true 50 | } 51 | ]; 52 | } 53 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart-item/cart-item.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, Input, computed, inject, signal } from '@angular/core'; 2 | import { CurrencyPipe, NgFor, NgIf } from '@angular/common'; 3 | import { FormsModule } from '@angular/forms'; 4 | 5 | import { CartItem } from '../cart'; 6 | import { CartService } from '../cart.service'; 7 | 8 | @Component({ 9 | selector: 'sw-cart-item', 10 | standalone: true, 11 | imports: [CurrencyPipe, FormsModule, NgFor, NgIf], 12 | templateUrl: './cart-item.component.html' 13 | }) 14 | export class CartItemComponent { 15 | 16 | // Use a setter to set the signal 17 | // when the item is passed in from the parent component 18 | @Input({ required: true }) set cartItem(ci: CartItem) { 19 | this.item.set(ci); 20 | } 21 | 22 | private cartService = inject(CartService); 23 | 24 | item = signal(undefined!); 25 | 26 | // Quantity available (hard-coded to 8) 27 | // Mapped to an array from 1-8 28 | // qtyArr = [...Array(8).keys()].map(x => x + 1); 29 | 30 | // Build an array of numbers from 1 to qty available 31 | qtyArr = computed(() => 32 | [...Array(this.item().product.quantityInStock).keys()].map(x => x + 1)); 33 | 34 | // Calculate the extended price 35 | exPrice = computed(() => this.item().quantity * this.item().product.price); 36 | 37 | onQuantitySelected(quantity: number): void { 38 | this.cartService.updateQuantity(this.item(), Number(quantity)); 39 | } 40 | 41 | removeFromCart(): void { 42 | this.cartService.removeFromCart(this.item()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /apm-begin/src/app/products/product-detail/product-detail.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | {{pageTitle}} 5 |
6 | 7 |
8 | 9 |
10 |
Name:
11 |
{{product.productName}}
12 |
14 |
15 |
16 |
Code:
17 |
{{product.productCode}}
18 |
19 |
20 |
Description:
21 |
{{product.description}}
22 |
23 |
24 |
Price:
25 |
{{product.price|currency:"USD":"symbol"}}
26 |
27 |
28 |
In Stock:
29 |
{{product.quantityInStock ?? 0}}
30 |
31 | 32 |
33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
ReviewUsernameText
{{ review.title }} {{ review.userName }}{{ review.text }}
50 | 51 | No reviews for this product 52 | 53 |
54 | 55 |
56 |
57 | 58 |
60 |
61 | Product Detail 62 |
63 |
64 |
66 | {{errorMessage}} 67 | 68 |
-------------------------------------------------------------------------------- /apm-final/src/app/products/product-detail/product-detail.component.html: -------------------------------------------------------------------------------- 1 |
3 |
4 | {{pageTitle()}} 5 |
6 | 7 |
8 | 9 |
10 |
Name:
11 |
{{product.productName}}
12 |
14 |
15 |
16 |
Code:
17 |
{{product.productCode}}
18 |
19 |
20 |
Description:
21 |
{{product.description}}
22 |
23 |
24 |
Price:
25 |
{{product.price|currency:"USD":"symbol"}}
26 |
27 |
28 |
In Stock:
29 |
{{product.quantityInStock ?? 0}}
30 |
31 | 32 |
33 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
ReviewUsernameText
{{ review.title }} {{ review.userName }}{{ review.text }}
50 | 51 | No reviews for this product 52 | 53 |
54 | 55 |
56 |
57 | 58 |
60 |
61 | Product Detail 62 |
63 |
64 |
66 | {{errorMessage()}} 67 | 68 |
-------------------------------------------------------------------------------- /apm-begin/src/app/reviews/review-data.ts: -------------------------------------------------------------------------------- 1 | import { Review } from './review'; 2 | 3 | export class ReviewData { 4 | 5 | static reviews: Review[] = [ 6 | { 7 | id: 1, 8 | productId: 10, 9 | userName: 'jackharkness', 10 | title: 'Works great', 11 | text: "I've beat every level faster with this controller" 12 | }, 13 | { 14 | id: 2, 15 | productId: 5, 16 | userName: 'thor364', 17 | title: "Didn't work as I expected", 18 | text: "I summon this hammer, and it does not heed my call" 19 | }, 20 | 21 | { 22 | id: 3, 23 | productId: 5, 24 | userName: 'allthumbs', 25 | title: "Dangerous!", 26 | text: "I almost injured myself with this product" 27 | }, 28 | { 29 | id: 4, 30 | productId: 2, 31 | userName: 'mom42', 32 | title: 'Great for the kiddos', 33 | text: 'My kids love to play with this cart' 34 | }, 35 | { 36 | id: 5, 37 | productId: 5, 38 | userName: 'theoden', 39 | title: 'Now for wrath. Now for ruin', 40 | text: 'This hammer (and a dinner bell) worked even better than a horn for drawing attention' 41 | }, 42 | { 43 | id: 6, 44 | productId: 5, 45 | userName: 'glamdring', 46 | title: 'This was no foe-hammer', 47 | text: 'Product was much smaller than expected' 48 | }, 49 | { 50 | id: 7, 51 | productId: 10, 52 | userName: 'grima', 53 | title: 'Nothing but a herald of woe', 54 | text: 'I played no better with this controller than my old one' 55 | }, 56 | { 57 | id: 8, 58 | productId: 1, 59 | userName: 'hama', 60 | title: 'Has no evil purpose', 61 | text: 'This rake is worthy of honor' 62 | }, 63 | { 64 | id: 9, 65 | productId: 1, 66 | userName: 'hama', 67 | title: 'More than a tool', 68 | text: 'The rake in the hand of a wizard may be more than a tool for the garden' 69 | }, 70 | { 71 | id: 10, 72 | productId: 1, 73 | userName: 'eowyn', 74 | title: 'A necessity!', 75 | text: 'Those without rakes can still die upon them' 76 | } 77 | ]; 78 | } 79 | -------------------------------------------------------------------------------- /apm-final/src/app/reviews/review-data.ts: -------------------------------------------------------------------------------- 1 | import { Review } from './review'; 2 | 3 | export class ReviewData { 4 | 5 | static reviews: Review[] = [ 6 | { 7 | id: 1, 8 | productId: 10, 9 | userName: 'jackharkness', 10 | title: 'Works great', 11 | text: "I've beat every level faster with this controller" 12 | }, 13 | { 14 | id: 2, 15 | productId: 5, 16 | userName: 'thor364', 17 | title: "Didn't work as I expected", 18 | text: "I summon this hammer, and it does not heed my call" 19 | }, 20 | 21 | { 22 | id: 3, 23 | productId: 5, 24 | userName: 'allthumbs', 25 | title: "Dangerous!", 26 | text: "I almost injured myself with this product" 27 | }, 28 | { 29 | id: 4, 30 | productId: 2, 31 | userName: 'mom42', 32 | title: 'Great for the kiddos', 33 | text: 'My kids love to play with this cart' 34 | }, 35 | { 36 | id: 5, 37 | productId: 5, 38 | userName: 'theoden', 39 | title: 'Now for wrath. Now for ruin', 40 | text: 'This hammer (and a dinner bell) worked even better than a horn for drawing attention' 41 | }, 42 | { 43 | id: 6, 44 | productId: 5, 45 | userName: 'glamdring', 46 | title: 'This was no foe-hammer', 47 | text: 'Product was much smaller than expected' 48 | }, 49 | { 50 | id: 7, 51 | productId: 10, 52 | userName: 'grima', 53 | title: 'Nothing but a herald of woe', 54 | text: 'I played no better with this controller than my old one' 55 | }, 56 | { 57 | id: 8, 58 | productId: 1, 59 | userName: 'hama', 60 | title: 'Has no evil purpose', 61 | text: 'This rake is worthy of honor' 62 | }, 63 | { 64 | id: 9, 65 | productId: 1, 66 | userName: 'hama', 67 | title: 'More than a tool', 68 | text: 'The rake in the hand of a wizard may be more than a tool for the garden' 69 | }, 70 | { 71 | id: 10, 72 | productId: 1, 73 | userName: 'eowyn', 74 | title: 'A necessity!', 75 | text: 'Those without rakes can still die upon them' 76 | } 77 | ]; 78 | } 79 | -------------------------------------------------------------------------------- /apm-final/src/app/cart/cart.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, computed, effect, signal } from "@angular/core"; 2 | import { CartItem } from "./cart"; 3 | import { Product } from "../products/product"; 4 | 5 | @Injectable({ 6 | providedIn: 'root' 7 | }) 8 | export class CartService { 9 | // Manage state with signals 10 | cartItems = signal([]); 11 | 12 | // Number of items in the cart 13 | cartCount = computed(() => this.cartItems() 14 | .reduce((accQty, item) => accQty + item.quantity, 0) 15 | ); 16 | 17 | // Total up the extended price for each item 18 | subTotal = computed(() => this.cartItems().reduce((accTotal, item) => 19 | accTotal + (item.quantity * item.product.price), 0)); 20 | 21 | // Delivery is free if spending more than $50 22 | deliveryFee = computed(() => this.subTotal() < 50 ? 5.99 : 0); 23 | 24 | // Tax could be based on shipping address zip code 25 | tax = computed(() => Math.round(this.subTotal() * 10.75) / 100); 26 | 27 | // Total price 28 | totalPrice = computed(() => this.subTotal() + this.deliveryFee() + this.tax()); 29 | 30 | eLength = effect(() => console.log('Cart array length:', this.cartItems().length)); 31 | 32 | // Add the vehicle to the cart 33 | // If the item is already in the cart, increase the quantity 34 | addToCart(product: Product): void { 35 | const index = this.cartItems().findIndex(item => 36 | item.product.id === product.id); 37 | if (index === -1) { 38 | // Not already in the cart, so add with default quantity of 1 39 | this.cartItems.update(items => [...items, { product, quantity: 1 }]); 40 | } else { 41 | // Already in the cart, so increase the quantity by 1 42 | this.cartItems.update(items => 43 | [ 44 | ...items.slice(0, index), 45 | { ...items[index], quantity: items[index].quantity + 1 }, 46 | ...items.slice(index + 1) 47 | ]); 48 | } 49 | } 50 | 51 | // Remove the item from the cart 52 | removeFromCart(cartItem: CartItem): void { 53 | // Update the cart with a new array containing 54 | // all but the filtered out deleted item 55 | this.cartItems.update(items => 56 | items.filter(item => item.product.id !== cartItem.product.id)); 57 | } 58 | 59 | // Update the cart quantity 60 | updateQuantity(cartItem: CartItem, quantity: number): void { 61 | // Update the cart with a new array containing 62 | // the updated item and all other original items 63 | this.cartItems.update(items => 64 | items.map(item => item.product.id === cartItem.product.id ? 65 | { ...item, quantity } : item)); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /apm-begin/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "apm": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": true 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": true 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "pm", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/apm", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": [ 30 | "zone.js" 31 | ], 32 | "tsConfig": "tsconfig.app.json", 33 | "assets": [ 34 | "src/favicon.ico", 35 | "src/assets" 36 | ], 37 | "styles": [ 38 | "src/styles.css" 39 | ], 40 | "scripts": [] 41 | }, 42 | "configurations": { 43 | "production": { 44 | "budgets": [ 45 | { 46 | "type": "initial", 47 | "maximumWarning": "500kb", 48 | "maximumError": "1mb" 49 | }, 50 | { 51 | "type": "anyComponentStyle", 52 | "maximumWarning": "2kb", 53 | "maximumError": "4kb" 54 | } 55 | ], 56 | "outputHashing": "all" 57 | }, 58 | "development": { 59 | "buildOptimizer": false, 60 | "optimization": false, 61 | "vendorChunk": true, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "apm:build:production" 74 | }, 75 | "development": { 76 | "browserTarget": "apm:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "apm:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "polyfills": [ 91 | "zone.js", 92 | "zone.js/testing" 93 | ], 94 | "tsConfig": "tsconfig.spec.json", 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ], 99 | "styles": [ 100 | "src/styles.css" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "cli": { 109 | "analytics": "d6c991fd-ff72-4369-8646-0b114d0a793d" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /apm-final/angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "apm": { 7 | "projectType": "application", 8 | "schematics": { 9 | "@schematics/angular:component": { 10 | "standalone": true 11 | }, 12 | "@schematics/angular:directive": { 13 | "standalone": true 14 | }, 15 | "@schematics/angular:pipe": { 16 | "standalone": true 17 | } 18 | }, 19 | "root": "", 20 | "sourceRoot": "src", 21 | "prefix": "pm", 22 | "architect": { 23 | "build": { 24 | "builder": "@angular-devkit/build-angular:browser", 25 | "options": { 26 | "outputPath": "dist/apm", 27 | "index": "src/index.html", 28 | "main": "src/main.ts", 29 | "polyfills": [ 30 | "zone.js" 31 | ], 32 | "tsConfig": "tsconfig.app.json", 33 | "assets": [ 34 | "src/favicon.ico", 35 | "src/assets" 36 | ], 37 | "styles": [ 38 | "src/styles.css" 39 | ], 40 | "scripts": [] 41 | }, 42 | "configurations": { 43 | "production": { 44 | "budgets": [ 45 | { 46 | "type": "initial", 47 | "maximumWarning": "500kb", 48 | "maximumError": "1mb" 49 | }, 50 | { 51 | "type": "anyComponentStyle", 52 | "maximumWarning": "2kb", 53 | "maximumError": "4kb" 54 | } 55 | ], 56 | "outputHashing": "all" 57 | }, 58 | "development": { 59 | "buildOptimizer": false, 60 | "optimization": false, 61 | "vendorChunk": true, 62 | "extractLicenses": false, 63 | "sourceMap": true, 64 | "namedChunks": true 65 | } 66 | }, 67 | "defaultConfiguration": "production" 68 | }, 69 | "serve": { 70 | "builder": "@angular-devkit/build-angular:dev-server", 71 | "configurations": { 72 | "production": { 73 | "browserTarget": "apm:build:production" 74 | }, 75 | "development": { 76 | "browserTarget": "apm:build:development" 77 | } 78 | }, 79 | "defaultConfiguration": "development" 80 | }, 81 | "extract-i18n": { 82 | "builder": "@angular-devkit/build-angular:extract-i18n", 83 | "options": { 84 | "browserTarget": "apm:build" 85 | } 86 | }, 87 | "test": { 88 | "builder": "@angular-devkit/build-angular:karma", 89 | "options": { 90 | "polyfills": [ 91 | "zone.js", 92 | "zone.js/testing" 93 | ], 94 | "tsConfig": "tsconfig.spec.json", 95 | "assets": [ 96 | "src/favicon.ico", 97 | "src/assets" 98 | ], 99 | "styles": [ 100 | "src/styles.css" 101 | ], 102 | "scripts": [] 103 | } 104 | } 105 | } 106 | } 107 | }, 108 | "cli": { 109 | "analytics": "d6c991fd-ff72-4369-8646-0b114d0a793d" 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /apm-final/src/app/products/product.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '@angular/common/http'; 2 | import { Injectable, computed, inject, signal } from '@angular/core'; 3 | import { Observable, catchError, filter, map, of, shareReplay, switchMap, tap, throwError } from 'rxjs'; 4 | import { Product, Result } from './product'; 5 | import { HttpErrorService } from '../utilities/http-error.service'; 6 | import { ReviewService } from '../reviews/review.service'; 7 | import { Review } from '../reviews/review'; 8 | import { toObservable, toSignal } from '@angular/core/rxjs-interop'; 9 | 10 | @Injectable({ 11 | providedIn: 'root' 12 | }) 13 | export class ProductService { 14 | private productsUrl = 'api/products'; 15 | 16 | private http = inject(HttpClient); 17 | private errorService = inject(HttpErrorService); 18 | private reviewService = inject(ReviewService); 19 | 20 | selectedProductId = signal(undefined); 21 | 22 | private productsResult$ = this.http.get(this.productsUrl) 23 | .pipe( 24 | map(p => ({ data: p } as Result)), 25 | tap(p => console.log(JSON.stringify(p))), 26 | shareReplay(1), 27 | catchError(err => of({ 28 | data: [], 29 | error: this.errorService.formatError(err) 30 | } as Result)) 31 | ); 32 | private productsResult = toSignal(this.productsResult$, 33 | { initialValue: ({ data: [] } as Result) }); 34 | products = computed(() => this.productsResult().data); 35 | productsError = computed(() => this.productsResult().error); 36 | 37 | // products = computed(() => { 38 | // try { 39 | // return toSignal(this.products$, { initialValue: [] as Product[] })(); 40 | // } catch (error) { 41 | // return [] as Product[]; 42 | // } 43 | // }); 44 | 45 | private productResult1$ = toObservable(this.selectedProductId) 46 | .pipe( 47 | filter(Boolean), 48 | switchMap(id => { 49 | const productUrl = this.productsUrl + '/' + id; 50 | return this.http.get(productUrl) 51 | .pipe( 52 | switchMap(product => this.getProductWithReviews(product)), 53 | catchError(err => of({ 54 | data: undefined, 55 | error: this.errorService.formatError(err) 56 | } as Result)) 57 | ); 58 | }), 59 | map(p => ({ data: p } as Result)) 60 | ); 61 | 62 | // Find the product in the existing array of products 63 | private foundProduct = computed(() => { 64 | // Dependent signals 65 | const p = this.products(); 66 | const id = this.selectedProductId(); 67 | if (p && id) { 68 | return p.find(product => product.id === id); 69 | } 70 | return undefined; 71 | }) 72 | 73 | // Get the related set of reviews 74 | private productResult$ = toObservable(this.foundProduct) 75 | .pipe( 76 | filter(Boolean), 77 | switchMap(product => this.getProductWithReviews(product)), 78 | map(p => ({ data: p } as Result)), 79 | catchError(err => of({ 80 | data: undefined, 81 | error: this.errorService.formatError(err) 82 | } as Result)) 83 | ); 84 | private productResult = toSignal(this.productResult$); 85 | product = computed(() => this.productResult()?.data); 86 | productError = computed(() => this.productResult()?.error); 87 | 88 | productSelected(selectedProductId: number): void { 89 | this.selectedProductId.set(selectedProductId); 90 | } 91 | 92 | private getProductWithReviews(product: Product): Observable { 93 | if (product.hasReviews) { 94 | return this.http.get(this.reviewService.getReviewUrl(product.id)) 95 | .pipe( 96 | map(reviews => ({ ...product, reviews } as Product)) 97 | ) 98 | } else { 99 | return of(product); 100 | } 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /links.md: -------------------------------------------------------------------------------- 1 | ## Reactive Programming 2 | 3 | [RxJS Documentation](https://rxjs.dev/) 4 | 5 | [Angular Signals Documentation](https://angular.io/guide/signals) 6 | 7 | [Rx.NET](https://github.com/dotnet/reactive) 8 | 9 | [Proposal for ECMAScript Observable](https://tc39.es/proposal-observable/) 10 | 11 | ## RxJS Terms and Syntax 12 | 13 | [StackBlitz](https://stackblitz.com) 14 | 15 | [RxJS Documentation](https://rxjs.dev/) 16 | 17 | ["RxJS in Angular: Terms, Tips and Patterns" - YouTube video](https://youtu.be/vtCDRiG__D4) 18 | 19 | [Demo Code (StackBlitz)](https://stackblitz.com/edit/rxjs-signals-m3-deborahk) 20 | 21 | ## RxJS Operators 22 | 23 | [RxJS Documentation](https://rxjs.dev/) 24 | 25 | ["RxJS in Angular: Terms, Tips and Patterns" - YouTube video](https://youtu.be/vtCDRiG__D4) 26 | 27 | [Demo Code (StackBlitz)](https://stackblitz.com/edit/rxjs-signals-m4-deborahk) 28 | 29 | ## Retrieving Data with HTTP and Observables 30 | 31 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 32 | 33 | [Sample Project Code (StackBlitz)](https://stackblitz.com/github/DeborahK/angular-rxjs-signals-fundamentals/tree/main/apm-begin) 34 | 35 | ["Gentle Introduction to Git/GitHub" - YouTube course](https://youtu.be/pICJdbC7j0Q) 36 | 37 | ["Understanding communicating with backend services using HTTP" - Angular documentation](https://angular.io/guide/understanding-communicating-with-http) 38 | 39 | ["RxJS Mapping: Mapping Retrieved Data" - YouTube video](https://youtu.be/c7z-rsKcvZw) 40 | 41 | ["Simplify with Angular Standalone Components" - YouTube video](https://youtu.be/c8YGsPx0zVk) 42 | 43 | ## Handling HTTP Errors with Observables 44 | 45 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 46 | 47 | ["HTTP client - Handle request errors" - Angular documentation](https://angular.io/guide/http-handle-request-errors) 48 | 49 | ["Error Handling with Observables" - YouTube video](https://youtu.be/L9kFTps_7Tk) 50 | 51 | ## Getting Related Data: switchMap, concatMap and mergeMap 52 | [Demo code (StackBlitz)](https://stackblitz.com/edit/rxjs-signals-m7-deborahk) 53 | 54 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 55 | 56 | ["Higher-order Observables" - RxJS documentation](https://rxjs.dev/guide/higher-order-observables) 57 | 58 | ["Why You Shouldn’t Nest Subscribes" - Medium article](https://medium.com/ngconf/why-you-shouldnt-nest-subscribes-eafbc3b00af2) 59 | 60 | ["switchMap vs concatMap vs mergeMap … Oh My!" - YouTube video](https://youtu.be/RSf7DlJXoGQ) 61 | 62 | ["RxJS in Angular: Terms, Tips, and Patterns" - YouTube video (This time code links to using an array of ids to retrieve data)](https://youtu.be/vtCDRiG__D4?t=2190) 63 | 64 | ## Using a Declarative Approach 65 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 66 | 67 | ["Observables in Angular: Async pipe" - Angular documentation](https://angular.io/guide/observables-in-angular#async-pipe) 68 | 69 | ["shareReplay" - RxJS documentation](https://rxjs.dev/api/index/function/shareReplay) 70 | 71 | ["Declarative Pattern for Getting Data from an Observable" - YouTube video](https://youtu.be/0XPxUa8u-LY) 72 | 73 | ## Reacting to Actions: Subject and BehaviorSubject 74 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 75 | 76 | ["Subject" - RxJS Documentation](https://rxjs.dev/api/index/class/Subject) 77 | 78 | ["BehaviorSubject" - RxJS Documentation](https://rxjs.dev/api/index/class/BehaviorSubject) 79 | 80 | ["combineLatest" - RxJS Documentation](https://rxjs.dev/api/index/function/combineLatest) 81 | 82 | ["4 Wicked Pipelines for RxJS in Angular" - YouTube video](https://youtu.be/wQ8jXlWMoCo) 83 | 84 | ## Introduction to Angular Signals 85 | [Demo code (StackBlitz)](https://stackblitz.com/edit/rxjs-signals-m10-deborahk) 86 | 87 | ["Signals in Angular – How to Write More Reactive Code" - freeCodeCamp article](https://www.freecodecamp.org/news/angular-signals) 88 | 89 | ["Angular Signals: What? Why? and How?" - YouTube video](https://youtu.be/oqYQG7QMdzw) 90 | 91 | ## Using Signals to Build a Shopping Cart Feature 92 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 93 | 94 | ["Manage State with Angular Signals" - YouTube video](https://youtu.be/04avEeicarQ) 95 | 96 | ## RxJS and Angular Signals: Better Together 97 | [Sample Project Code (GitHub)](https://github.com/DeborahK/angular-rxjs-signals-fundamentals) 98 | 99 | [Code from the Slides (StackBlitz)](https://stackblitz.com/edit/rxjs-signals-m12-deborahk) 100 | 101 | ["Unlocking the Power of Angular Signals + RxJS" - YouTube video](https://youtu.be/nXJFhZdbWzw) 102 | --------------------------------------------------------------------------------