diff --git a/README.md b/README.md index ec31b30..c5c3b4e 100644 --- a/README.md +++ b/README.md @@ -9,11 +9,12 @@ Materials for my Pluralsight course: ["Angular Reactive Forms"](https://app.plur Angular v12 turns strict typing on by default. Angular forms is not very strongly typed, so some changes are required to support strict typing. Use these files if you are using Angular v12 or newer or if you turn on strict typing. -`demo-start-v12`: The starter files updated to Angular v12. - -`demo-final-v12`: The completed files updated to Angular v12. - -`apm-v12`: Angular reactive form in the context of a more full-featured application updated to Angular v12. Includes examples of CRUD (Create, Read, Update, and Delete) operations. +- `demo-start-v12`: The starter files updated to Angular v12. +- `demo-final-v12`: The completed files updated to Angular v12. +- `apm-v12`: Angular reactive form in the context of a more full-featured application updated to Angular v12. Includes examples of CRUD (Create, Read, Update, and Delete) operations. +- `demo-start-v14`: The starter files updated to Angular v14. +- `demo-final-v14`: The completed files updated to Angular v14. +- `apm-v14`: Angular reactive form in the context of a more full-featured application updated to Angular v14. Includes examples of CRUD (Create, Read, Update, and Delete) operations. See the `README.md` file under each folder for details on installing and running the application. diff --git a/apm-v14/.browserslistrc b/apm-v14/.browserslistrc new file mode 100644 index 0000000..4f9ac26 --- /dev/null +++ b/apm-v14/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/apm-v14/.gitignore b/apm-v14/.gitignore new file mode 100644 index 0000000..16c02db --- /dev/null +++ b/apm-v14/.gitignore @@ -0,0 +1,43 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log +yarn.lock + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/apm-v14/.vscode/extensions.json b/apm-v14/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/apm-v14/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/apm-v14/.vscode/launch.json b/apm-v14/.vscode/launch.json new file mode 100644 index 0000000..82e5f7a --- /dev/null +++ b/apm-v14/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "pwa-chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + } + ] +} diff --git a/apm-v14/.vscode/tasks.json b/apm-v14/.vscode/tasks.json new file mode 100644 index 0000000..b02874a --- /dev/null +++ b/apm-v14/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/apm-v14/README.md b/apm-v14/README.md new file mode 100644 index 0000000..643665d --- /dev/null +++ b/apm-v14/README.md @@ -0,0 +1,27 @@ +# DemoStartV14 + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.1.2. + +## Development server + +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. + +## Code scaffolding + +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`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +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. + +## Further help + +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. diff --git a/apm-v14/angular.json b/apm-v14/angular.json new file mode 100644 index 0000000..16966b8 --- /dev/null +++ b/apm-v14/angular.json @@ -0,0 +1,111 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "demo-start-v14": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/demo-start-v14", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "demo-start-v14:build:production" + }, + "development": { + "browserTarget": "demo-start-v14:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "demo-start-v14:build" + } + } + } + } + } +} diff --git a/apm-v14/package.json b/apm-v14/package.json new file mode 100644 index 0000000..be7c34d --- /dev/null +++ b/apm-v14/package.json @@ -0,0 +1,33 @@ +{ + "name": "apm-v14", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve -o", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^14.2.1", + "@angular/compiler": "^14.2.1", + "@angular/core": "^14.2.1", + "@angular/forms": "^14.2.1", + "@angular/platform-browser": "^14.2.1", + "@angular/platform-browser-dynamic": "^14.2.1", + "@angular/router": "^14.2.1", + "@popperjs/core": "2.11.6", + "angular-in-memory-web-api": "^0.14.0", + "bootstrap": "^5.2.1", + "font-awesome": "^4.7.0", + "rxjs": "^7.5.6", + "tslib": "^2.4.0", + "zone.js": "^0.11.8" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^14.1.2", + "@angular/cli": "~14.1.2", + "@angular/compiler-cli": "^14.1.0", + "typescript": "~4.7.2" + } +} diff --git a/apm-v14/src/app/app.component.css b/apm-v14/src/app/app.component.css new file mode 100644 index 0000000..a58ae86 --- /dev/null +++ b/apm-v14/src/app/app.component.css @@ -0,0 +1,3 @@ +.nav-link { + font-size: large; +} diff --git a/apm-v14/src/app/app.component.html b/apm-v14/src/app/app.component.html new file mode 100644 index 0000000..bb498a3 --- /dev/null +++ b/apm-v14/src/app/app.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/apm-v14/src/app/app.component.ts b/apm-v14/src/app/app.component.ts new file mode 100644 index 0000000..6eb56e9 --- /dev/null +++ b/apm-v14/src/app/app.component.ts @@ -0,0 +1,28 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'pm-root', + template: ` + +
+ +
+ `, + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + pageTitle = 'Acme Product Management'; +} diff --git a/apm-v14/src/app/app.module.ts b/apm-v14/src/app/app.module.ts new file mode 100644 index 0000000..ce5f844 --- /dev/null +++ b/apm-v14/src/app/app.module.ts @@ -0,0 +1,27 @@ +import { BrowserModule } from '@angular/platform-browser'; +import { NgModule } from '@angular/core'; +import { HttpClientModule } from '@angular/common/http'; +import { RouterModule } from '@angular/router'; + +import { AppComponent } from './app.component'; +import { WelcomeComponent } from './home/welcome.component'; +import { ProductModule } from './products/product.module'; + +@NgModule({ + declarations: [ + AppComponent, + WelcomeComponent + ], + imports: [ + BrowserModule, + HttpClientModule, + RouterModule.forRoot([ + { path: 'welcome', component: WelcomeComponent }, + { path: '', redirectTo: 'welcome', pathMatch: 'full' }, + { path: '**', redirectTo: 'welcome', pathMatch: 'full' } + ]), + ProductModule + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/apm-v14/src/app/customers/customer.component.css b/apm-v14/src/app/customers/customer.component.css new file mode 100644 index 0000000..e69de29 diff --git a/apm-v14/src/app/customers/customer.component.html b/apm-v14/src/app/customers/customer.component.html new file mode 100644 index 0000000..965d099 --- /dev/null +++ b/apm-v14/src/app/customers/customer.component.html @@ -0,0 +1,221 @@ +
+
+ Sign Up! +
+ +
+
+ +
+ +
+ +
+ + Please enter your first name. + + + The first name must be longer than 3 characters. + +
+
+
+ +
+ +
+ +
+ + Please enter your last name. + + + The last name must be less than 50 characters. + + +
+
+ +
+ +
+ +
+ + Please enter your email address. + + + Please enter a valid email address. + + +
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+
Dirty: {{ signupForm.dirty }} +
Touched: {{ signupForm.touched }} +
Valid: {{ signupForm.valid }} +
Value: {{ signupForm.value | json }} \ No newline at end of file diff --git a/apm-v14/src/app/customers/customer.component.ts b/apm-v14/src/app/customers/customer.component.ts new file mode 100644 index 0000000..d2ef3f2 --- /dev/null +++ b/apm-v14/src/app/customers/customer.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { Customer } from './customer'; + +@Component({ + selector: 'app-customer', + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.css'] +}) +export class CustomerComponent implements OnInit { + customer = new Customer(); + + constructor() { } + + ngOnInit(): void { + } + + save(customerForm: NgForm): void { + console.log(customerForm.form); + console.log('Saved: ' + JSON.stringify(customerForm.value)); + } +} diff --git a/apm-v14/src/app/customers/customer.ts b/apm-v14/src/app/customers/customer.ts new file mode 100644 index 0000000..87c6c59 --- /dev/null +++ b/apm-v14/src/app/customers/customer.ts @@ -0,0 +1,14 @@ +export class Customer { + + constructor( + public firstName = '', + public lastName = '', + public email = '', + public sendCatalog = false, + public addressType = 'home', + public street1?: string, + public street2?: string, + public city?: string, + public state = '', + public zip?: string) { } +} diff --git a/apm-v14/src/app/home/welcome.component.html b/apm-v14/src/app/home/welcome.component.html new file mode 100644 index 0000000..01a2588 --- /dev/null +++ b/apm-v14/src/app/home/welcome.component.html @@ -0,0 +1,24 @@ +
+
+ {{pageTitle}} +
+
+
+
+ +
+ +
Developed by:
+
+

Deborah Kurata

+
+ +
@deborahkurata
+ +
+
+
\ No newline at end of file diff --git a/apm-v14/src/app/home/welcome.component.ts b/apm-v14/src/app/home/welcome.component.ts new file mode 100644 index 0000000..7abec89 --- /dev/null +++ b/apm-v14/src/app/home/welcome.component.ts @@ -0,0 +1,8 @@ +import { Component } from '@angular/core'; + +@Component({ + templateUrl: './welcome.component.html' +}) +export class WelcomeComponent { + public pageTitle = 'Welcome'; +} diff --git a/apm-v14/src/app/products/product-data.ts b/apm-v14/src/app/products/product-data.ts new file mode 100644 index 0000000..360ab71 --- /dev/null +++ b/apm-v14/src/app/products/product-data.ts @@ -0,0 +1,64 @@ +import { InMemoryDbService } from 'angular-in-memory-web-api'; + +import { Product } from './product'; + +export class ProductData implements InMemoryDbService { + + createDb(): { products: Product[]} { + const products: Product[] = [ + { + id: 1, + productName: 'Leaf Rake', + productCode: 'GDN-0011', + releaseDate: 'March 19, 2018', + description: 'Leaf rake with 48-inch wooden handle', + price: 19.95, + starRating: 3.2, + imageUrl: 'assets/images/leaf_rake.png', + tags: ['rake', 'leaf', 'yard', 'home'] + }, + { + id: 2, + productName: 'Garden Cart', + productCode: 'GDN-0023', + releaseDate: 'March 18, 2018', + description: '15 gallon capacity rolling garden cart', + price: 32.99, + starRating: 4.2, + imageUrl: 'assets/images/garden_cart.png' + }, + { + id: 5, + productName: 'Hammer', + productCode: 'TBX-0048', + releaseDate: 'May 21, 2018', + description: 'Curved claw steel hammer', + price: 8.9, + starRating: 4.8, + imageUrl: 'assets/images/hammer.png', + tags: ['tools', 'hammer', 'construction'] + }, + { + id: 8, + productName: 'Saw', + productCode: 'TBX-0022', + releaseDate: 'May 15, 2018', + description: '15-inch steel blade hand saw', + price: 11.55, + starRating: 3.7, + imageUrl: 'assets/images/saw.png' + }, + { + id: 10, + productName: 'Video Game Controller', + productCode: 'GMG-0042', + releaseDate: 'October 15, 2018', + description: 'Standard two-button video game controller', + price: 35.95, + starRating: 4.6, + imageUrl: 'assets/images/xbox-controller.png' + } + ]; + return { products }; + } +} diff --git a/apm-v14/src/app/products/product-detail.component.css b/apm-v14/src/app/products/product-detail.component.css new file mode 100644 index 0000000..e69de29 diff --git a/apm-v14/src/app/products/product-detail.component.html b/apm-v14/src/app/products/product-detail.component.html new file mode 100644 index 0000000..5716406 --- /dev/null +++ b/apm-v14/src/app/products/product-detail.component.html @@ -0,0 +1,76 @@ +
+
+ {{pageTitle + ": " + product.productName}} +
+ +
+ +
+ +
+
+
Name:
+
{{product.productName}}
+
+
+
Code:
+
{{product.productCode}}
+
+
+
Description:
+
{{product.description}}
+
+
+
Availability:
+
{{product.releaseDate}}
+
+
+
Price:
+
{{product.price|currency:"USD":"symbol"}}
+
+
+
5 Star Rating:
+
+ + +
+
+
+
Tags:
+
{{product.tags}}
+
+
+ +
+ +
+
+ +
+
+ + +
+
+ +
+ +
{{errorMessage}} +
+
\ No newline at end of file diff --git a/apm-v14/src/app/products/product-detail.component.ts b/apm-v14/src/app/products/product-detail.component.ts new file mode 100644 index 0000000..5492b85 --- /dev/null +++ b/apm-v14/src/app/products/product-detail.component.ts @@ -0,0 +1,40 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Product } from './product'; +import { ProductService } from './product.service'; + +@Component({ + templateUrl: './product-detail.component.html', + styleUrls: ['./product-detail.component.css'] +}) +export class ProductDetailComponent implements OnInit { + pageTitle = 'Product Detail'; + errorMessage = ''; + product: Product | undefined; + + constructor(private route: ActivatedRoute, + private router: Router, + private productService: ProductService) { + } + + ngOnInit(): void { + const param = this.route.snapshot.paramMap.get('id'); + if (param) { + const id = +param; + this.getProduct(id); + } + } + + getProduct(id: number): void { + this.productService.getProduct(id).subscribe({ + next: product => this.product = product, + error: err => this.errorMessage = err + }); + } + + onBack(): void { + this.router.navigate(['/products']); + } + +} diff --git a/apm-v14/src/app/products/product-edit.component.html b/apm-v14/src/app/products/product-edit.component.html new file mode 100644 index 0000000..519362e --- /dev/null +++ b/apm-v14/src/app/products/product-edit.component.html @@ -0,0 +1,139 @@ +
+
+ {{pageTitle}} +
+ +
+
+ +
+ +
+ + + {{displayMessage['productName'] }} + +
+
+ +
+ +
+ + + {{displayMessage['productCode']}} + +
+
+ +
+ + +
+ + + {{displayMessage['starRating']}} + +
+
+ +
+
+ + +
+ +
+ +
+
+ +
+
+ +
+
+ +
+ + +
+ + + {{ displayMessage['description']}} + +
+
+ +
+
+ + + +
+
+
+
+ +
{{errorMessage}} +
+
\ No newline at end of file diff --git a/apm-v14/src/app/products/product-edit.component.ts b/apm-v14/src/app/products/product-edit.component.ts new file mode 100644 index 0000000..241b8e0 --- /dev/null +++ b/apm-v14/src/app/products/product-edit.component.ts @@ -0,0 +1,189 @@ +import { Component, OnInit, AfterViewInit, OnDestroy, ViewChildren, ElementRef } from '@angular/core'; +import { FormBuilder, FormGroup, FormControl, FormArray, Validators, FormControlName } from '@angular/forms'; +import { ActivatedRoute, Router } from '@angular/router'; + +import { Observable, Subscription, fromEvent, merge } from 'rxjs'; +import { debounceTime } from 'rxjs/operators'; + +import { Product } from './product'; +import { ProductService } from './product.service'; + +import { NumberValidators } from '../shared/number.validator'; +import { GenericValidator } from '../shared/generic-validator'; + +@Component({ + templateUrl: './product-edit.component.html' +}) +export class ProductEditComponent implements OnInit, AfterViewInit, OnDestroy { + @ViewChildren(FormControlName, { read: ElementRef }) formInputElements!: ElementRef[]; + + pageTitle = 'Product Edit'; + errorMessage = ''; + productForm!: FormGroup; + + product!: Product; + private sub!: Subscription; + + // Use with the generic validation message class + displayMessage: { [key: string]: string } = {}; + private validationMessages: { [key: string]: { [key: string]: string } }; + private genericValidator: GenericValidator; + + get tags(): FormArray { + return this.productForm.get('tags') as FormArray; + } + + constructor(private fb: FormBuilder, + private route: ActivatedRoute, + private router: Router, + private productService: ProductService) { + + // Defines all of the validation messages for the form. + // These could instead be retrieved from a file or database. + this.validationMessages = { + productName: { + required: 'Product name is required.', + minlength: 'Product name must be at least three characters.', + maxlength: 'Product name cannot exceed 50 characters.' + }, + productCode: { + required: 'Product code is required.' + }, + starRating: { + range: 'Rate the product between 1 (lowest) and 5 (highest).' + } + }; + + // Define an instance of the validator for use with this form, + // passing in this form's set of validation messages. + this.genericValidator = new GenericValidator(this.validationMessages); + } + + ngOnInit(): void { + this.productForm = this.fb.group({ + productName: ['', [Validators.required, + Validators.minLength(3), + Validators.maxLength(50)]], + productCode: ['', Validators.required], + starRating: ['', NumberValidators.range(1, 5)], + tags: this.fb.array([]), + description: '' + }); + + // Read the product Id from the route parameter + this.sub = this.route.paramMap.subscribe( + params => { + const id = Number(this.route.snapshot.paramMap.get('id')); + this.getProduct(id); + } + ); + } + + ngOnDestroy(): void { + this.sub.unsubscribe(); + } + + ngAfterViewInit(): void { + // Watch for the blur event from any input element on the form. + // This is required because the valueChanges does not provide notification on blur + const controlBlurs: Observable[] = this.formInputElements + .map((formControl: ElementRef) => fromEvent(formControl.nativeElement, 'blur')); + + // Merge the blur event observable with the valueChanges observable + // so we only need to subscribe once. + merge(this.productForm.valueChanges, ...controlBlurs).pipe( + debounceTime(800) + ).subscribe(value => { + this.displayMessage = this.genericValidator.processMessages(this.productForm); + }); + } + + addTag(): void { + this.tags.push(new FormControl()); + } + + deleteTag(index: number): void { + this.tags.removeAt(index); + this.tags.markAsDirty(); + } + + getProduct(id: number): void { + this.productService.getProduct(id) + .subscribe({ + next: (product: Product) => this.displayProduct(product), + error: err => this.errorMessage = err + }); + } + + displayProduct(product: Product): void { + if (this.productForm) { + this.productForm.reset(); + } + this.product = product; + + if (this.product.id === 0) { + this.pageTitle = 'Add Product'; + } else { + this.pageTitle = `Edit Product: ${this.product.productName}`; + } + + // Update the data on the form + this.productForm.patchValue({ + productName: this.product.productName, + productCode: this.product.productCode, + starRating: this.product.starRating, + description: this.product.description + }); + this.productForm.setControl('tags', this.fb.array(this.product.tags || [])); + } + + deleteProduct(): void { + if (this.product.id === 0) { + // Don't delete, it was never saved. + this.onSaveComplete(); + } else if (this.product.id) { + if (confirm(`Really delete the product: ${this.product.productName}?`)) { + this.productService.deleteProduct(this.product.id) + .subscribe({ + next: () => this.onSaveComplete(), + error: err => this.errorMessage = err + }); + } + } + } + + saveProduct(): void { + if (this.productForm.valid) { + if (this.productForm.dirty) { + const p = { ...this.product, ...this.productForm.value }; + + if (p.id === 0) { + this.productService.createProduct(p) + .subscribe({ + next: x => { + console.log(x); + return this.onSaveComplete(); + }, + error: err => this.errorMessage = err + }); + } else { + this.productService.updateProduct(p) + .subscribe({ + next: () => this.onSaveComplete(), + error: err => this.errorMessage = err + }); + } + } else { + this.onSaveComplete(); + } + } else { + this.errorMessage = 'Please correct the validation errors.'; + } + } + + onSaveComplete(): void { + // Reset the form to clear the flags + this.productForm.reset(); + this.router.navigate(['/products']); + } +} diff --git a/apm-v14/src/app/products/product-edit.guard.ts b/apm-v14/src/app/products/product-edit.guard.ts new file mode 100644 index 0000000..34a74ec --- /dev/null +++ b/apm-v14/src/app/products/product-edit.guard.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { CanDeactivate } from '@angular/router'; +import { Observable } from 'rxjs'; + +import { ProductEditComponent } from './product-edit.component'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductEditGuard implements CanDeactivate { + canDeactivate(component: ProductEditComponent): Observable | Promise | boolean { + if (component.productForm.dirty) { + const productName = component.productForm.get('productName')?.value || 'New Product'; + return confirm(`Navigate away and lose all changes to ${productName}?`); + } + return true; + } +} diff --git a/apm-v14/src/app/products/product-list.component.css b/apm-v14/src/app/products/product-list.component.css new file mode 100644 index 0000000..862d768 --- /dev/null +++ b/apm-v14/src/app/products/product-list.component.css @@ -0,0 +1,3 @@ +thead { + color: #337AB7; +} \ No newline at end of file diff --git a/apm-v14/src/app/products/product-list.component.html b/apm-v14/src/app/products/product-list.component.html new file mode 100644 index 0000000..ecc868d --- /dev/null +++ b/apm-v14/src/app/products/product-list.component.html @@ -0,0 +1,78 @@ +
+
+ {{pageTitle}} +
+ +
+
+
Filter by:
+
+ +
+
+
+
+

Filtered by: {{listFilter}}

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + +
+ + ProductCodeAvailablePrice5 Star Rating
+ + + + {{ product.productName }} + + {{ product.productCode }}{{ product.releaseDate }}{{ product.price | currency:"USD":"symbol":"1.2-2" }} + + + + +
+
+ +
+
+ +
+ Error: {{ errorMessage }} +
\ No newline at end of file diff --git a/apm-v14/src/app/products/product-list.component.ts b/apm-v14/src/app/products/product-list.component.ts new file mode 100644 index 0000000..c20c5af --- /dev/null +++ b/apm-v14/src/app/products/product-list.component.ts @@ -0,0 +1,58 @@ +import { Component, OnInit } from '@angular/core'; + +import { Product } from './product'; +import { ProductService } from './product.service'; + +@Component({ + templateUrl: './product-list.component.html', + styleUrls: ['./product-list.component.css'] +}) +export class ProductListComponent implements OnInit { + pageTitle = 'Product List'; + imageWidth = 50; + imageMargin = 2; + showImage = false; + errorMessage = ''; + + _listFilter = ''; + get listFilter(): string { + return this._listFilter; + } + set listFilter(value: string) { + this._listFilter = value; + this.filteredProducts = this.listFilter ? this.performFilter(this.listFilter) : this.products; + } + + filteredProducts: Product[] = []; + products: Product[] = []; + + constructor(private productService: ProductService) { } + + performFilter(filterBy: string): Product[] { + filterBy = filterBy.toLocaleLowerCase(); + return this.products.filter((product: Product) => + product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1); + } + + // Checks both the product name and tags + performFilter2(filterBy: string): Product[] { + filterBy = filterBy.toLocaleLowerCase(); + return this.products.filter((product: Product) => + product.productName.toLocaleLowerCase().indexOf(filterBy) !== -1 || + (product.tags && product.tags.some(tag => tag.toLocaleLowerCase().indexOf(filterBy) !== -1))); + } + + toggleImage(): void { + this.showImage = !this.showImage; + } + + ngOnInit(): void { + this.productService.getProducts().subscribe({ + next: products => { + this.products = products; + this.filteredProducts = this.products; + }, + error: err => this.errorMessage = err + }); + } +} diff --git a/apm-v14/src/app/products/product.module.ts b/apm-v14/src/app/products/product.module.ts new file mode 100644 index 0000000..692d0c3 --- /dev/null +++ b/apm-v14/src/app/products/product.module.ts @@ -0,0 +1,37 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { ReactiveFormsModule } from '@angular/forms'; + +import { SharedModule } from '../shared/shared.module'; + +// Imports for loading & configuring the in-memory web api +import { InMemoryWebApiModule } from 'angular-in-memory-web-api'; +import { ProductData } from './product-data'; + +import { ProductListComponent } from './product-list.component'; +import { ProductDetailComponent } from './product-detail.component'; +import { ProductEditComponent } from './product-edit.component'; +import { ProductEditGuard } from './product-edit.guard'; + +@NgModule({ + imports: [ + SharedModule, + ReactiveFormsModule, + InMemoryWebApiModule.forRoot(ProductData), + RouterModule.forChild([ + { path: 'products', component: ProductListComponent }, + { path: 'products/:id', component: ProductDetailComponent }, + { + path: 'products/:id/edit', + canDeactivate: [ProductEditGuard], + component: ProductEditComponent + } + ]) + ], + declarations: [ + ProductListComponent, + ProductDetailComponent, + ProductEditComponent + ] +}) +export class ProductModule { } diff --git a/apm-v14/src/app/products/product.service.ts b/apm-v14/src/app/products/product.service.ts new file mode 100644 index 0000000..5200d0b --- /dev/null +++ b/apm-v14/src/app/products/product.service.ts @@ -0,0 +1,99 @@ +import { Injectable } from '@angular/core'; +import { HttpClient, HttpErrorResponse, HttpHeaders } from '@angular/common/http'; + +import { Observable, of, throwError } from 'rxjs'; +import { catchError, tap, map } from 'rxjs/operators'; + +import { Product } from './product'; + +@Injectable({ + providedIn: 'root' +}) +export class ProductService { + private productsUrl = 'api/products'; + + constructor(private http: HttpClient) { } + + getProducts(): Observable { + return this.http.get(this.productsUrl) + .pipe( + tap(data => console.log(JSON.stringify(data))), + catchError(this.handleError) + ); + } + + getProduct(id: number): Observable { + if (id === 0) { + return of(this.initializeProduct()); + } + const url = `${this.productsUrl}/${id}`; + return this.http.get(url) + .pipe( + tap(data => console.log('getProduct: ' + JSON.stringify(data))), + catchError(this.handleError) + ); + } + + createProduct(product: Product): Observable { + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + product.id = null; + return this.http.post(this.productsUrl, product, { headers }) + .pipe( + tap(data => console.log('createProduct: ' + JSON.stringify(data))), + catchError(this.handleError) + ); + } + + deleteProduct(id: number): Observable<{}> { + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + const url = `${this.productsUrl}/${id}`; + return this.http.delete(url, { headers }) + .pipe( + tap(data => console.log('deleteProduct: ' + id)), + catchError(this.handleError) + ); + } + + updateProduct(product: Product): Observable { + const headers = new HttpHeaders({ 'Content-Type': 'application/json' }); + const url = `${this.productsUrl}/${product.id}`; + return this.http.put(url, product, { headers }) + .pipe( + tap(() => console.log('updateProduct: ' + product.id)), + // Return the product on an update + map(() => product), + catchError(this.handleError) + ); + } + + private handleError(err: HttpErrorResponse): Observable { + // in a real world app, we may send the server to some remote logging infrastructure + // instead of just logging it to the console + let errorMessage = ''; + if (err.error instanceof ErrorEvent) { + // A client-side or network error occurred. Handle it accordingly. + errorMessage = `An error occurred: ${err.error.message}`; + } else { + // The backend returned an unsuccessful response code. + // The response body may contain clues as to what went wrong, + errorMessage = `Server returned code: ${err.status}, error message is: ${err.message}`; + } + console.error(errorMessage); + return throwError(() => errorMessage); + } + + private initializeProduct(): Product { + // Return an initialized object + return { + id: 0, + productName: '', + productCode: '', + tags: [''], + releaseDate: '', + price: 0, + description: '', + starRating: 0, + imageUrl: '' + }; + } +} diff --git a/apm-v14/src/app/products/product.ts b/apm-v14/src/app/products/product.ts new file mode 100644 index 0000000..bbbb17c --- /dev/null +++ b/apm-v14/src/app/products/product.ts @@ -0,0 +1,13 @@ +/* Defines the product entity */ +export interface Product { + id: number | null; + productName: string; + productCode: string; + tags?: string[]; + releaseDate: string; + price: number; + description: string; + starRating: number; + imageUrl: string; +} + diff --git a/apm-v14/src/app/shared/generic-validator.ts b/apm-v14/src/app/shared/generic-validator.ts new file mode 100644 index 0000000..d639b56 --- /dev/null +++ b/apm-v14/src/app/shared/generic-validator.ts @@ -0,0 +1,54 @@ +import { FormGroup } from '@angular/forms'; + +// Generic validator for Reactive forms +// Implemented as a class, not a service, so it can retain state for multiple forms. +// NOTE: This validator does NOT support validation of controls or groups within a FormArray. +export class GenericValidator { + + // Provide the set of valid validation messages + // Stucture: + // controlName1: { + // validationRuleName1: 'Validation Message.', + // validationRuleName2: 'Validation Message.' + // }, + // controlName2: { + // validationRuleName1: 'Validation Message.', + // validationRuleName2: 'Validation Message.' + // } + constructor(private validationMessages: { [key: string]: { [key: string]: string } }) { + + } + + // Processes each control within a FormGroup + // And returns a set of validation messages to display + // Structure + // controlName1: 'Validation Message.', + // controlName2: 'Validation Message.' + processMessages(container: FormGroup): { [key: string]: string } { + const messages: any = {}; + for (const controlKey in container.controls) { + if (container.controls.hasOwnProperty(controlKey)) { + const c = container.controls[controlKey]; + // If it is a FormGroup, process its child controls. + if (c instanceof FormGroup) { + const childMessages = this.processMessages(c); + Object.assign(messages, childMessages); + } else { + // Only validate if there are validation messages for the control + if (this.validationMessages[controlKey]) { + messages[controlKey] = ''; + if ((c.dirty || c.touched) && c.errors) { + Object.keys(c.errors).map(messageKey => { + if (this.validationMessages[controlKey][messageKey]) { + messages[controlKey] += this.validationMessages[controlKey][messageKey] + ' '; + } + }); + } + } + } + } + } + return messages; + } + +} diff --git a/apm-v14/src/app/shared/number.validator.ts b/apm-v14/src/app/shared/number.validator.ts new file mode 100644 index 0000000..8a73c34 --- /dev/null +++ b/apm-v14/src/app/shared/number.validator.ts @@ -0,0 +1,13 @@ +import { AbstractControl, ValidatorFn } from '@angular/forms'; + +export class NumberValidators { + + static range(min: number, max: number): ValidatorFn { + return (c: AbstractControl): { [key: string]: boolean } | null => { + if (c.value && (isNaN(c.value) || c.value < min || c.value > max)) { + return { range: true }; + } + return null; + }; + } +} diff --git a/apm-v14/src/app/shared/shared.module.ts b/apm-v14/src/app/shared/shared.module.ts new file mode 100644 index 0000000..3cb4989 --- /dev/null +++ b/apm-v14/src/app/shared/shared.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { FormsModule } from '@angular/forms'; + +import { StarComponent } from './star.component'; + +@NgModule({ + imports: [ + CommonModule + ], + declarations: [ + StarComponent + ], + exports: [ + StarComponent, + CommonModule, + FormsModule + ] +}) +export class SharedModule { } diff --git a/apm-v14/src/app/shared/star.component.css b/apm-v14/src/app/shared/star.component.css new file mode 100644 index 0000000..f5828c6 --- /dev/null +++ b/apm-v14/src/app/shared/star.component.css @@ -0,0 +1,6 @@ +.crop { + overflow: hidden; +} +div { + cursor: pointer; +} \ No newline at end of file diff --git a/apm-v14/src/app/shared/star.component.html b/apm-v14/src/app/shared/star.component.html new file mode 100644 index 0000000..5aa28eb --- /dev/null +++ b/apm-v14/src/app/shared/star.component.html @@ -0,0 +1,12 @@ +
+
+ + + + + +
+
\ No newline at end of file diff --git a/apm-v14/src/app/shared/star.component.ts b/apm-v14/src/app/shared/star.component.ts new file mode 100644 index 0000000..4def194 --- /dev/null +++ b/apm-v14/src/app/shared/star.component.ts @@ -0,0 +1,21 @@ +import { Component, OnChanges, Input, EventEmitter, Output } from '@angular/core'; + +@Component({ + selector: 'pm-star', + templateUrl: './star.component.html', + styleUrls: ['./star.component.css'] +}) +export class StarComponent implements OnChanges { + @Input() rating = 0; + starWidth = 0; + @Output() ratingClicked: EventEmitter = + new EventEmitter(); + + ngOnChanges(): void { + this.starWidth = this.rating * 75 / 5; + } + + onClick(): void { + this.ratingClicked.emit(`The rating ${this.rating} was clicked!`); + } +} diff --git a/apm-v14/src/assets/.gitkeep b/apm-v14/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/apm-v14/src/assets/images/garden_cart.png b/apm-v14/src/assets/images/garden_cart.png new file mode 100644 index 0000000..8a529bb Binary files /dev/null and b/apm-v14/src/assets/images/garden_cart.png differ diff --git a/apm-v14/src/assets/images/hammer.png b/apm-v14/src/assets/images/hammer.png new file mode 100644 index 0000000..522fbfd Binary files /dev/null and b/apm-v14/src/assets/images/hammer.png differ diff --git a/apm-v14/src/assets/images/leaf_rake.png b/apm-v14/src/assets/images/leaf_rake.png new file mode 100644 index 0000000..a52d461 Binary files /dev/null and b/apm-v14/src/assets/images/leaf_rake.png differ diff --git a/apm-v14/src/assets/images/logo.jpg b/apm-v14/src/assets/images/logo.jpg new file mode 100644 index 0000000..f8676af Binary files /dev/null and b/apm-v14/src/assets/images/logo.jpg differ diff --git a/apm-v14/src/assets/images/saw.png b/apm-v14/src/assets/images/saw.png new file mode 100644 index 0000000..6107332 Binary files /dev/null and b/apm-v14/src/assets/images/saw.png differ diff --git a/apm-v14/src/assets/images/xbox-controller.png b/apm-v14/src/assets/images/xbox-controller.png new file mode 100644 index 0000000..aa5183a Binary files /dev/null and b/apm-v14/src/assets/images/xbox-controller.png differ diff --git a/apm-v14/src/environments/environment.prod.ts b/apm-v14/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/apm-v14/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/apm-v14/src/environments/environment.ts b/apm-v14/src/environments/environment.ts new file mode 100644 index 0000000..f56ff47 --- /dev/null +++ b/apm-v14/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/apm-v14/src/favicon.ico b/apm-v14/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/apm-v14/src/favicon.ico differ diff --git a/apm-v14/src/index.html b/apm-v14/src/index.html new file mode 100644 index 0000000..a470fa7 --- /dev/null +++ b/apm-v14/src/index.html @@ -0,0 +1,13 @@ + + + + + Apm + + + + + + + + diff --git a/apm-v14/src/main.ts b/apm-v14/src/main.ts new file mode 100644 index 0000000..c7b673c --- /dev/null +++ b/apm-v14/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/apm-v14/src/polyfills.ts b/apm-v14/src/polyfills.ts new file mode 100644 index 0000000..373f538 --- /dev/null +++ b/apm-v14/src/polyfills.ts @@ -0,0 +1,65 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/apm-v14/src/styles.css b/apm-v14/src/styles.css new file mode 100644 index 0000000..9b410f0 --- /dev/null +++ b/apm-v14/src/styles.css @@ -0,0 +1,15 @@ +/* You can add global styles to this file, and also import other style files */ +@import "~bootstrap/dist/css/bootstrap.min.css"; +@import "~font-awesome/css/font-awesome.min.css"; + +div.card-header { + font-size: large; +} + +div.card { + margin-top: 10px +} + +.table { + margin-top: 10px +} \ No newline at end of file diff --git a/apm-v14/tsconfig.app.json b/apm-v14/tsconfig.app.json new file mode 100644 index 0000000..82d91dc --- /dev/null +++ b/apm-v14/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/apm-v14/tsconfig.json b/apm-v14/tsconfig.json new file mode 100644 index 0000000..ff06eae --- /dev/null +++ b/apm-v14/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2020", + "module": "es2020", + "lib": [ + "es2020", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/demo-final-v14/.browserslistrc b/demo-final-v14/.browserslistrc new file mode 100644 index 0000000..4f9ac26 --- /dev/null +++ b/demo-final-v14/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/demo-final-v14/.gitignore b/demo-final-v14/.gitignore new file mode 100644 index 0000000..16c02db --- /dev/null +++ b/demo-final-v14/.gitignore @@ -0,0 +1,43 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log +yarn.lock + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/demo-final-v14/.vscode/extensions.json b/demo-final-v14/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/demo-final-v14/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/demo-final-v14/.vscode/launch.json b/demo-final-v14/.vscode/launch.json new file mode 100644 index 0000000..82e5f7a --- /dev/null +++ b/demo-final-v14/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "pwa-chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + } + ] +} diff --git a/demo-final-v14/.vscode/tasks.json b/demo-final-v14/.vscode/tasks.json new file mode 100644 index 0000000..b02874a --- /dev/null +++ b/demo-final-v14/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/demo-final-v14/README.md b/demo-final-v14/README.md new file mode 100644 index 0000000..643665d --- /dev/null +++ b/demo-final-v14/README.md @@ -0,0 +1,27 @@ +# DemoStartV14 + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.1.2. + +## Development server + +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. + +## Code scaffolding + +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`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +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. + +## Further help + +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. diff --git a/demo-final-v14/angular.json b/demo-final-v14/angular.json new file mode 100644 index 0000000..16966b8 --- /dev/null +++ b/demo-final-v14/angular.json @@ -0,0 +1,111 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "demo-start-v14": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/demo-start-v14", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "demo-start-v14:build:production" + }, + "development": { + "browserTarget": "demo-start-v14:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "demo-start-v14:build" + } + } + } + } + } +} diff --git a/demo-final-v14/package.json b/demo-final-v14/package.json new file mode 100644 index 0000000..a921cbc --- /dev/null +++ b/demo-final-v14/package.json @@ -0,0 +1,33 @@ +{ + "name": "demo-final-v14", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve -o", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^14.2.1", + "@angular/compiler": "^14.2.1", + "@angular/core": "^14.2.1", + "@angular/forms": "^14.2.1", + "@angular/platform-browser": "^14.2.1", + "@angular/platform-browser-dynamic": "^14.2.1", + "@angular/router": "^14.2.1", + "@popperjs/core": "2.11.6", + "angular-in-memory-web-api": "^0.14.0", + "bootstrap": "^5.2.1", + "font-awesome": "^4.7.0", + "rxjs": "^7.5.6", + "tslib": "^2.4.0", + "zone.js": "^0.11.8" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^14.1.2", + "@angular/cli": "~14.1.2", + "@angular/compiler-cli": "^14.1.0", + "typescript": "~4.7.2" + } +} diff --git a/demo-final-v14/src/app/app.component.css b/demo-final-v14/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/demo-final-v14/src/app/app.component.html b/demo-final-v14/src/app/app.component.html new file mode 100644 index 0000000..bb498a3 --- /dev/null +++ b/demo-final-v14/src/app/app.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/demo-final-v14/src/app/app.component.ts b/demo-final-v14/src/app/app.component.ts new file mode 100644 index 0000000..d0587b9 --- /dev/null +++ b/demo-final-v14/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'demo'; +} diff --git a/demo-final-v14/src/app/app.module.ts b/demo-final-v14/src/app/app.module.ts new file mode 100644 index 0000000..de0ba12 --- /dev/null +++ b/demo-final-v14/src/app/app.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { CustomerComponent } from './customers/customer.component'; + +@NgModule({ + declarations: [ + AppComponent, + CustomerComponent + ], + imports: [ + BrowserModule, + ReactiveFormsModule + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/demo-final-v14/src/app/customers/customer.component.css b/demo-final-v14/src/app/customers/customer.component.css new file mode 100644 index 0000000..e69de29 diff --git a/demo-final-v14/src/app/customers/customer.component.html b/demo-final-v14/src/app/customers/customer.component.html new file mode 100644 index 0000000..f025d73 --- /dev/null +++ b/demo-final-v14/src/app/customers/customer.component.html @@ -0,0 +1,318 @@ +
+
+ Sign Up! +
+ +
+
+ +
+ +
+ +
+ + Please enter your first name. + + + The first name must be longer than 3 characters. + + +
+
+ +
+ +
+ +
+ + Please enter your last name. + + + The last name must be less than 50 characters. + +
+
+
+ +
+
+ +
+ + + {{ emailMessage }} + +
+
+ +
+ +
+ +
+ + Please confirm your email address. + + + The confirmation does not match the email address. + +
+
+
+
+ +
+ +
+ +
+ + Please enter your phone number. + +
+
+
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + Please rate your experience from 1 to 5. + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+ + Please enter your street address. + +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+
+ +
+
+
+ +
+
+ + +
+
+ +
+
+
Dirty: {{ customerForm.dirty }} +
Touched: {{ customerForm.touched }} +
Valid: {{ customerForm.valid }} +
Value: {{ customerForm.value | json }} +
Street: {{ addresses.get('0.street1')?.value }} +
  \ No newline at end of file diff --git a/demo-final-v14/src/app/customers/customer.component.ts b/demo-final-v14/src/app/customers/customer.component.ts new file mode 100644 index 0000000..d5607dc --- /dev/null +++ b/demo-final-v14/src/app/customers/customer.component.ts @@ -0,0 +1,134 @@ +import { Component, OnInit } from '@angular/core'; +import { FormGroup, FormBuilder, Validators, AbstractControl, ValidatorFn, FormArray } from '@angular/forms'; + +import { debounceTime } from 'rxjs/operators'; + +import { Customer } from './customer'; + +function emailMatcher(c: AbstractControl): { [key: string]: boolean } | null { + const emailControl = c.get('email'); + const confirmControl = c.get('confirmEmail'); + + if (emailControl?.pristine || confirmControl?.pristine) { + return null; + } + + if (emailControl?.value === confirmControl?.value) { + return null; + } + return { match: true }; +} + +function ratingRange(min: number, max: number): ValidatorFn { + return (c: AbstractControl): { [key: string]: boolean } | null => { + if (c.value !== null && (isNaN(c.value) || c.value < min || c.value > max)) { + return { range: true }; + } + return null; + }; +} + +@Component({ + selector: 'app-customer', + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.css'] +}) +export class CustomerComponent implements OnInit { + customerForm!: FormGroup; + customer = new Customer(); + emailMessage = ''; + + get addresses(): FormArray { + return this.customerForm.get('addresses') as FormArray; + } + + private validationMessages: any = { + required: 'Please enter your email address.', + email: 'Please enter a valid email address.' + }; + + constructor(private fb: FormBuilder) { } + + ngOnInit(): void { + this.customerForm = this.fb.group({ + firstName: ['', [Validators.required, Validators.minLength(3)]], + lastName: ['', [Validators.required, Validators.maxLength(50)]], + emailGroup: this.fb.group({ + email: ['', [Validators.required, Validators.email]], + confirmEmail: ['', Validators.required], + }, { validators: emailMatcher }), + phone: '', + notification: 'email', + rating: [null, ratingRange(1, 5)], + sendCatalog: true, + addresses: this.fb.array([this.buildAddress()]) + }); + + this.customerForm.get('notification')?.valueChanges.subscribe( + value => this.setNotification(value) + ); + + const emailControl = this.customerForm.get('emailGroup.email'); + emailControl?.valueChanges.pipe( + debounceTime(1000) + ).subscribe( + value => this.setMessage(emailControl) + ); + } + + addAddress(): void { + this.addresses.push(this.buildAddress()); + } + + buildAddress(): FormGroup { + return this.fb.group({ + addressType: 'home', + street1: ['', Validators.required], + street2: '', + city: '', + state: '', + zip: '' + }); + } + + populateTestData(): void { + this.customerForm.patchValue({ + firstName: 'Jack', + lastName: 'Harkness', + emailGroup: { email: 'jack@torchwood.com', confirmEmail: 'jack@torchwood.com' } + }); + const addressGroup = this.fb.group({ + addressType: 'work', + street1: 'Mermaid Quay', + street2: '', + city: 'Cardiff Bay', + state: 'CA', + zip: '' + }); + this.customerForm.setControl('addresses', this.fb.array([addressGroup])); + } + + save(): void { + console.log(this.customerForm); + console.log('Saved: ' + JSON.stringify(this.customerForm.value)); + } + + setMessage(c: AbstractControl): void { + this.emailMessage = ''; + if ((c.touched || c.dirty) && c.errors) { + this.emailMessage = Object.keys(c.errors).map( + key => this.validationMessages[key]).join(' '); + } + } + + setNotification(notifyVia: string): void { + const phoneControl = this.customerForm.get('phone'); + if (notifyVia === 'text') { + phoneControl?.setValidators(Validators.required); + } else { + phoneControl?.clearValidators(); + } + phoneControl?.updateValueAndValidity(); + } + +} diff --git a/demo-final-v14/src/app/customers/customer.ts b/demo-final-v14/src/app/customers/customer.ts new file mode 100644 index 0000000..87c6c59 --- /dev/null +++ b/demo-final-v14/src/app/customers/customer.ts @@ -0,0 +1,14 @@ +export class Customer { + + constructor( + public firstName = '', + public lastName = '', + public email = '', + public sendCatalog = false, + public addressType = 'home', + public street1?: string, + public street2?: string, + public city?: string, + public state = '', + public zip?: string) { } +} diff --git a/demo-final-v14/src/assets/.gitkeep b/demo-final-v14/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/demo-final-v14/src/environments/environment.prod.ts b/demo-final-v14/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/demo-final-v14/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/demo-final-v14/src/environments/environment.ts b/demo-final-v14/src/environments/environment.ts new file mode 100644 index 0000000..f56ff47 --- /dev/null +++ b/demo-final-v14/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/demo-final-v14/src/favicon.ico b/demo-final-v14/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/demo-final-v14/src/favicon.ico differ diff --git a/demo-final-v14/src/index.html b/demo-final-v14/src/index.html new file mode 100644 index 0000000..f9f06e7 --- /dev/null +++ b/demo-final-v14/src/index.html @@ -0,0 +1,13 @@ + + + + + Demo + + + + + + + + diff --git a/demo-final-v14/src/main.ts b/demo-final-v14/src/main.ts new file mode 100644 index 0000000..c7b673c --- /dev/null +++ b/demo-final-v14/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/demo-final-v14/src/polyfills.ts b/demo-final-v14/src/polyfills.ts new file mode 100644 index 0000000..373f538 --- /dev/null +++ b/demo-final-v14/src/polyfills.ts @@ -0,0 +1,65 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/demo-final-v14/src/styles.css b/demo-final-v14/src/styles.css new file mode 100644 index 0000000..ee31842 --- /dev/null +++ b/demo-final-v14/src/styles.css @@ -0,0 +1,10 @@ +/* You can add global styles to this file, and also import other style files */ +@import "~bootstrap/dist/css/bootstrap.min.css"; + +div.card-header { + font-size: large; +} + +div.card { + margin-top: 10px +} \ No newline at end of file diff --git a/demo-final-v14/src/test.ts b/demo-final-v14/src/test.ts new file mode 100644 index 0000000..b4dd603 --- /dev/null +++ b/demo-final-v14/src/test.ts @@ -0,0 +1,27 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { teardown: { destroyAfterEach: true }}, +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/demo-final-v14/tsconfig.app.json b/demo-final-v14/tsconfig.app.json new file mode 100644 index 0000000..82d91dc --- /dev/null +++ b/demo-final-v14/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/demo-final-v14/tsconfig.json b/demo-final-v14/tsconfig.json new file mode 100644 index 0000000..ff06eae --- /dev/null +++ b/demo-final-v14/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2020", + "module": "es2020", + "lib": [ + "es2020", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +} diff --git a/demo-start-v14/.browserslistrc b/demo-start-v14/.browserslistrc new file mode 100644 index 0000000..4f9ac26 --- /dev/null +++ b/demo-start-v14/.browserslistrc @@ -0,0 +1,16 @@ +# This file is used by the build system to adjust CSS and JS output to support the specified browsers below. +# For additional information regarding the format and rule options, please see: +# https://github.com/browserslist/browserslist#queries + +# For the full list of supported browsers by the Angular framework, please see: +# https://angular.io/guide/browser-support + +# You can see what browsers were selected by your queries by running: +# npx browserslist + +last 1 Chrome version +last 1 Firefox version +last 2 Edge major versions +last 2 Safari major versions +last 2 iOS major versions +Firefox ESR diff --git a/demo-start-v14/.gitignore b/demo-start-v14/.gitignore new file mode 100644 index 0000000..16c02db --- /dev/null +++ b/demo-start-v14/.gitignore @@ -0,0 +1,43 @@ +# See http://help.github.com/ignore-files/ for more about ignoring files. + +# Compiled output +/dist +/tmp +/out-tsc +/bazel-out + +# Node +/node_modules +npm-debug.log +yarn-error.log +yarn.lock + +# IDEs and editors +.idea/ +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# Visual Studio Code +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +.history/* + +# Miscellaneous +/.angular/cache +.sass-cache/ +/connect.lock +/coverage +/libpeerconnection.log +testem.log +/typings + +# System files +.DS_Store +Thumbs.db diff --git a/demo-start-v14/.vscode/extensions.json b/demo-start-v14/.vscode/extensions.json new file mode 100644 index 0000000..77b3745 --- /dev/null +++ b/demo-start-v14/.vscode/extensions.json @@ -0,0 +1,4 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=827846 + "recommendations": ["angular.ng-template"] +} diff --git a/demo-start-v14/.vscode/launch.json b/demo-start-v14/.vscode/launch.json new file mode 100644 index 0000000..82e5f7a --- /dev/null +++ b/demo-start-v14/.vscode/launch.json @@ -0,0 +1,13 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "ng serve", + "type": "pwa-chrome", + "request": "launch", + "preLaunchTask": "npm: start", + "url": "http://localhost:4200/" + } + ] +} diff --git a/demo-start-v14/.vscode/tasks.json b/demo-start-v14/.vscode/tasks.json new file mode 100644 index 0000000..b02874a --- /dev/null +++ b/demo-start-v14/.vscode/tasks.json @@ -0,0 +1,24 @@ +{ + // For more information, visit: https://go.microsoft.com/fwlink/?LinkId=733558 + "version": "2.0.0", + "tasks": [ + { + "type": "npm", + "script": "start", + "isBackground": true, + "problemMatcher": { + "owner": "typescript", + "pattern": "$tsc", + "background": { + "activeOnStart": true, + "beginsPattern": { + "regexp": "(.*?)" + }, + "endsPattern": { + "regexp": "bundle generation complete" + } + } + } + } + ] +} diff --git a/demo-start-v14/README.md b/demo-start-v14/README.md new file mode 100644 index 0000000..643665d --- /dev/null +++ b/demo-start-v14/README.md @@ -0,0 +1,27 @@ +# DemoStartV14 + +This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 14.1.2. + +## Development server + +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. + +## Code scaffolding + +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`. + +## Build + +Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. + +## Running unit tests + +Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io). + +## Running end-to-end tests + +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. + +## Further help + +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. diff --git a/demo-start-v14/angular.json b/demo-start-v14/angular.json new file mode 100644 index 0000000..16966b8 --- /dev/null +++ b/demo-start-v14/angular.json @@ -0,0 +1,111 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "newProjectRoot": "projects", + "projects": { + "demo-start-v14": { + "projectType": "application", + "schematics": { + "@schematics/angular:component": { + "inlineTemplate": true, + "inlineStyle": true, + "skipTests": true + }, + "@schematics/angular:class": { + "skipTests": true + }, + "@schematics/angular:directive": { + "skipTests": true + }, + "@schematics/angular:guard": { + "skipTests": true + }, + "@schematics/angular:interceptor": { + "skipTests": true + }, + "@schematics/angular:pipe": { + "skipTests": true + }, + "@schematics/angular:resolver": { + "skipTests": true + }, + "@schematics/angular:service": { + "skipTests": true + } + }, + "root": "", + "sourceRoot": "src", + "prefix": "app", + "architect": { + "build": { + "builder": "@angular-devkit/build-angular:browser", + "options": { + "outputPath": "dist/demo-start-v14", + "index": "src/index.html", + "main": "src/main.ts", + "polyfills": "src/polyfills.ts", + "tsConfig": "tsconfig.app.json", + "assets": [ + "src/favicon.ico", + "src/assets" + ], + "styles": [ + "src/styles.css" + ], + "scripts": [] + }, + "configurations": { + "production": { + "budgets": [ + { + "type": "initial", + "maximumWarning": "500kb", + "maximumError": "1mb" + }, + { + "type": "anyComponentStyle", + "maximumWarning": "2kb", + "maximumError": "4kb" + } + ], + "fileReplacements": [ + { + "replace": "src/environments/environment.ts", + "with": "src/environments/environment.prod.ts" + } + ], + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true + } + }, + "defaultConfiguration": "production" + }, + "serve": { + "builder": "@angular-devkit/build-angular:dev-server", + "configurations": { + "production": { + "browserTarget": "demo-start-v14:build:production" + }, + "development": { + "browserTarget": "demo-start-v14:build:development" + } + }, + "defaultConfiguration": "development" + }, + "extract-i18n": { + "builder": "@angular-devkit/build-angular:extract-i18n", + "options": { + "browserTarget": "demo-start-v14:build" + } + } + } + } + } +} diff --git a/demo-start-v14/package.json b/demo-start-v14/package.json new file mode 100644 index 0000000..dc2ae49 --- /dev/null +++ b/demo-start-v14/package.json @@ -0,0 +1,33 @@ +{ + "name": "demo-start-v14", + "version": "0.0.0", + "scripts": { + "ng": "ng", + "start": "ng serve -o", + "build": "ng build", + "watch": "ng build --watch --configuration development" + }, + "private": true, + "dependencies": { + "@angular/common": "^14.2.1", + "@angular/compiler": "^14.2.1", + "@angular/core": "^14.2.1", + "@angular/forms": "^14.2.1", + "@angular/platform-browser": "^14.2.1", + "@angular/platform-browser-dynamic": "^14.2.1", + "@angular/router": "^14.2.1", + "@popperjs/core": "2.11.6", + "angular-in-memory-web-api": "^0.14.0", + "bootstrap": "^5.2.1", + "font-awesome": "^4.7.0", + "rxjs": "^7.5.6", + "tslib": "^2.4.0", + "zone.js": "^0.11.8" + }, + "devDependencies": { + "@angular-devkit/build-angular": "^14.1.2", + "@angular/cli": "~14.1.2", + "@angular/compiler-cli": "^14.1.0", + "typescript": "~4.7.2" + } +} diff --git a/demo-start-v14/src/app/app.component.css b/demo-start-v14/src/app/app.component.css new file mode 100644 index 0000000..e69de29 diff --git a/demo-start-v14/src/app/app.component.html b/demo-start-v14/src/app/app.component.html new file mode 100644 index 0000000..bb498a3 --- /dev/null +++ b/demo-start-v14/src/app/app.component.html @@ -0,0 +1,3 @@ +
+ +
diff --git a/demo-start-v14/src/app/app.component.ts b/demo-start-v14/src/app/app.component.ts new file mode 100644 index 0000000..d0587b9 --- /dev/null +++ b/demo-start-v14/src/app/app.component.ts @@ -0,0 +1,10 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'app-root', + templateUrl: './app.component.html', + styleUrls: ['./app.component.css'] +}) +export class AppComponent { + title = 'demo'; +} diff --git a/demo-start-v14/src/app/app.module.ts b/demo-start-v14/src/app/app.module.ts new file mode 100644 index 0000000..88f3665 --- /dev/null +++ b/demo-start-v14/src/app/app.module.ts @@ -0,0 +1,19 @@ +import { NgModule } from '@angular/core'; +import { FormsModule } from '@angular/forms'; +import { BrowserModule } from '@angular/platform-browser'; + +import { AppComponent } from './app.component'; +import { CustomerComponent } from './customers/customer.component'; + +@NgModule({ + declarations: [ + AppComponent, + CustomerComponent + ], + imports: [ + BrowserModule, + FormsModule + ], + bootstrap: [AppComponent] +}) +export class AppModule { } diff --git a/demo-start-v14/src/app/customers/customer.component.css b/demo-start-v14/src/app/customers/customer.component.css new file mode 100644 index 0000000..e69de29 diff --git a/demo-start-v14/src/app/customers/customer.component.html b/demo-start-v14/src/app/customers/customer.component.html new file mode 100644 index 0000000..a86bdee --- /dev/null +++ b/demo-start-v14/src/app/customers/customer.component.html @@ -0,0 +1,221 @@ +
+
+ Sign Up! +
+ +
+
+ +
+ +
+ +
+ + Please enter your first name. + + + The first name must be longer than 3 characters. + +
+
+
+ +
+ +
+ +
+ + Please enter your last name. + + + The last name must be less than 50 characters. + +
+
+
+ +
+ +
+ +
+ + Please enter your email address. + + + Please enter a valid email address. + +
+
+
+ +
+
+
+ +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+ +
+ +
+
+ +
+
+ +
+
+
+ +
+
+ +
+
+
+
+
+
Dirty: {{ signupForm.dirty }} +
Touched: {{ signupForm.touched }} +
Valid: {{ signupForm.valid }} +
Value: {{ signupForm.value | json }} \ No newline at end of file diff --git a/demo-start-v14/src/app/customers/customer.component.ts b/demo-start-v14/src/app/customers/customer.component.ts new file mode 100644 index 0000000..d2ef3f2 --- /dev/null +++ b/demo-start-v14/src/app/customers/customer.component.ts @@ -0,0 +1,23 @@ +import { Component, OnInit } from '@angular/core'; +import { NgForm } from '@angular/forms'; + +import { Customer } from './customer'; + +@Component({ + selector: 'app-customer', + templateUrl: './customer.component.html', + styleUrls: ['./customer.component.css'] +}) +export class CustomerComponent implements OnInit { + customer = new Customer(); + + constructor() { } + + ngOnInit(): void { + } + + save(customerForm: NgForm): void { + console.log(customerForm.form); + console.log('Saved: ' + JSON.stringify(customerForm.value)); + } +} diff --git a/demo-start-v14/src/app/customers/customer.ts b/demo-start-v14/src/app/customers/customer.ts new file mode 100644 index 0000000..87c6c59 --- /dev/null +++ b/demo-start-v14/src/app/customers/customer.ts @@ -0,0 +1,14 @@ +export class Customer { + + constructor( + public firstName = '', + public lastName = '', + public email = '', + public sendCatalog = false, + public addressType = 'home', + public street1?: string, + public street2?: string, + public city?: string, + public state = '', + public zip?: string) { } +} diff --git a/demo-start-v14/src/assets/.gitkeep b/demo-start-v14/src/assets/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/demo-start-v14/src/environments/environment.prod.ts b/demo-start-v14/src/environments/environment.prod.ts new file mode 100644 index 0000000..3612073 --- /dev/null +++ b/demo-start-v14/src/environments/environment.prod.ts @@ -0,0 +1,3 @@ +export const environment = { + production: true +}; diff --git a/demo-start-v14/src/environments/environment.ts b/demo-start-v14/src/environments/environment.ts new file mode 100644 index 0000000..f56ff47 --- /dev/null +++ b/demo-start-v14/src/environments/environment.ts @@ -0,0 +1,16 @@ +// This file can be replaced during build by using the `fileReplacements` array. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. +// The list of file replacements can be found in `angular.json`. + +export const environment = { + production: false +}; + +/* + * For easier debugging in development mode, you can import the following file + * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. + * + * This import should be commented out in production mode because it will have a negative impact + * on performance if an error is thrown. + */ +// import 'zone.js/plugins/zone-error'; // Included with Angular CLI. diff --git a/demo-start-v14/src/favicon.ico b/demo-start-v14/src/favicon.ico new file mode 100644 index 0000000..997406a Binary files /dev/null and b/demo-start-v14/src/favicon.ico differ diff --git a/demo-start-v14/src/index.html b/demo-start-v14/src/index.html new file mode 100644 index 0000000..f9f06e7 --- /dev/null +++ b/demo-start-v14/src/index.html @@ -0,0 +1,13 @@ + + + + + Demo + + + + + + + + diff --git a/demo-start-v14/src/main.ts b/demo-start-v14/src/main.ts new file mode 100644 index 0000000..c7b673c --- /dev/null +++ b/demo-start-v14/src/main.ts @@ -0,0 +1,12 @@ +import { enableProdMode } from '@angular/core'; +import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; + +import { AppModule } from './app/app.module'; +import { environment } from './environments/environment'; + +if (environment.production) { + enableProdMode(); +} + +platformBrowserDynamic().bootstrapModule(AppModule) + .catch(err => console.error(err)); diff --git a/demo-start-v14/src/polyfills.ts b/demo-start-v14/src/polyfills.ts new file mode 100644 index 0000000..373f538 --- /dev/null +++ b/demo-start-v14/src/polyfills.ts @@ -0,0 +1,65 @@ +/** + * This file includes polyfills needed by Angular and is loaded before the app. + * You can add your own extra polyfills to this file. + * + * This file is divided into 2 sections: + * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. + * 2. Application imports. Files imported after ZoneJS that should be loaded before your main + * file. + * + * The current setup is for so-called "evergreen" browsers; the last versions of browsers that + * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), + * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. + * + * Learn more in https://angular.io/guide/browser-support + */ + +/*************************************************************************************************** + * BROWSER POLYFILLS + */ + +/** + * IE11 requires the following for NgClass support on SVG elements + */ +// import 'classlist.js'; // Run `npm install --save classlist.js`. + +/** + * Web Animations `@angular/platform-browser/animations` + * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. + * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). + */ +// import 'web-animations-js'; // Run `npm install --save web-animations-js`. + +/** + * By default, zone.js will patch all possible macroTask and DomEvents + * user can disable parts of macroTask/DomEvents patch by setting following flags + * because those flags need to be set before `zone.js` being loaded, and webpack + * will put import in the top of bundle, so user need to create a separate file + * in this directory (for example: zone-flags.ts), and put the following flags + * into that file, and then add the following code before importing zone.js. + * import './zone-flags'; + * + * The flags allowed in zone-flags.ts are listed here. + * + * The following flags will work for all browsers. + * + * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame + * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick + * (window as any).__zone_symbol__UNPATCHED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames + * + * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js + * with the following flag, it will bypass `zone.js` patch for IE/Edge + * + * (window as any).__Zone_enable_cross_context_check = true; + * + */ + +/*************************************************************************************************** + * Zone JS is required by default for Angular itself. + */ +import 'zone.js'; // Included with Angular CLI. + + +/*************************************************************************************************** + * APPLICATION IMPORTS + */ diff --git a/demo-start-v14/src/styles.css b/demo-start-v14/src/styles.css new file mode 100644 index 0000000..ee31842 --- /dev/null +++ b/demo-start-v14/src/styles.css @@ -0,0 +1,10 @@ +/* You can add global styles to this file, and also import other style files */ +@import "~bootstrap/dist/css/bootstrap.min.css"; + +div.card-header { + font-size: large; +} + +div.card { + margin-top: 10px +} \ No newline at end of file diff --git a/demo-start-v14/src/test.ts b/demo-start-v14/src/test.ts new file mode 100644 index 0000000..b4dd603 --- /dev/null +++ b/demo-start-v14/src/test.ts @@ -0,0 +1,27 @@ +// This file is required by karma.conf.js and loads recursively all the .spec and framework files + +import 'zone.js/testing'; +import { getTestBed } from '@angular/core/testing'; +import { + BrowserDynamicTestingModule, + platformBrowserDynamicTesting +} from '@angular/platform-browser-dynamic/testing'; + +declare const require: { + context(path: string, deep?: boolean, filter?: RegExp): { + keys(): string[]; + (id: string): T; + }; +}; + +// First, initialize the Angular testing environment. +getTestBed().initTestEnvironment( + BrowserDynamicTestingModule, + platformBrowserDynamicTesting(), + { teardown: { destroyAfterEach: true }}, +); + +// Then we find all the tests. +const context = require.context('./', true, /\.spec\.ts$/); +// And load the modules. +context.keys().map(context); diff --git a/demo-start-v14/tsconfig.app.json b/demo-start-v14/tsconfig.app.json new file mode 100644 index 0000000..82d91dc --- /dev/null +++ b/demo-start-v14/tsconfig.app.json @@ -0,0 +1,15 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "./out-tsc/app", + "types": [] + }, + "files": [ + "src/main.ts", + "src/polyfills.ts" + ], + "include": [ + "src/**/*.d.ts" + ] +} diff --git a/demo-start-v14/tsconfig.json b/demo-start-v14/tsconfig.json new file mode 100644 index 0000000..ff06eae --- /dev/null +++ b/demo-start-v14/tsconfig.json @@ -0,0 +1,32 @@ +/* To learn more about this file see: https://angular.io/config/tsconfig. */ +{ + "compileOnSave": false, + "compilerOptions": { + "baseUrl": "./", + "outDir": "./dist/out-tsc", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "sourceMap": true, + "declaration": false, + "downlevelIteration": true, + "experimentalDecorators": true, + "moduleResolution": "node", + "importHelpers": true, + "target": "es2020", + "module": "es2020", + "lib": [ + "es2020", + "dom" + ] + }, + "angularCompilerOptions": { + "enableI18nLegacyMessageIdFormat": false, + "strictInjectionParameters": true, + "strictInputAccessModifiers": true, + "strictTemplates": true + } +}