diff --git a/angular.json b/angular.json index 45579721f5..786e816296 100644 --- a/angular.json +++ b/angular.json @@ -16,6 +16,20 @@ }, "@schematics/angular:directive": { "prefix": "bc" + }, + "@nrwl/angular:application": { + "style": "scss", + "linter": "eslint", + "unitTestRunner": "jest", + "e2eTestRunner": "cypress" + }, + "@nrwl/angular:library": { + "style": "scss", + "linter": "eslint", + "unitTestRunner": "jest" + }, + "@nrwl/angular:component": { + "style": "scss" } }, "projects": { @@ -711,97 +725,93 @@ }, "schematics": {} }, - "data-example-app": { + "tour-of-heroes-data": { "projectType": "application", - "schematics": { - "@nrwl/angular:component": { - "style": "scss" - } - }, - "root": "projects/data-example-app", - "sourceRoot": "projects/data-example-app/src", + "root": "projects/tour-of-heroes-data", + "sourceRoot": "projects/tour-of-heroes-data/src", "prefix": "ngrx", "architect": { "build": { "builder": "@angular-devkit/build-angular:browser", - "options": { - "outputPath": "dist/projects/data-example-app", - "index": "projects/data-example-app/src/index.html", - "main": "projects/data-example-app/src/main.ts", - "polyfills": "projects/data-example-app/src/polyfills.ts", - "tsConfig": "projects/data-example-app/tsconfig.app.json", - "aot": true, + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/projects/tour-of-heroes-data", + "index": "projects/tour-of-heroes-data/src/index.html", + "main": "projects/tour-of-heroes-data/src/main.ts", + "polyfills": "projects/tour-of-heroes-data/src/polyfills.ts", + "tsConfig": "projects/tour-of-heroes-data/tsconfig.app.json", + "inlineStyleLanguage": "scss", "assets": [ - "projects/data-example-app/src/favicon.ico", - "projects/data-example-app/src/assets" + "projects/tour-of-heroes-data/src/favicon.ico", + "projects/tour-of-heroes-data/src/assets" ], - "styles": ["projects/data-example-app/src/styles.scss"], - "scripts": [] + "styles": [ + "./node_modules/@angular/material/prebuilt-themes/indigo-pink.css", + "projects/tour-of-heroes-data/src/styles.scss", + "projects/tour-of-heroes-data/src/assets/styles/mixin.scss", + "projects/tour-of-heroes-data/src/assets/styles/theme.scss", + "projects/tour-of-heroes-data/src/assets/styles/styles.scss" + ], + "scripts": [], + "stylePreprocessorOptions": { + "includePaths": ["projects/tour-of-heroes-data/src/assets/styles"] + } }, "configurations": { "production": { + "budgets": [], "fileReplacements": [ { - "replace": "projects/data-example-app/src/environments/environment.ts", - "with": "projects/data-example-app/src/environments/environment.prod.ts" + "replace": "projects/tour-of-heroes-data/src/environments/environment.ts", + "with": "projects/tour-of-heroes-data/src/environments/environment.prod.ts" } ], - "optimization": true, - "outputHashing": "all", - "sourceMap": false, - "namedChunks": false, - "extractLicenses": true, - "vendorChunk": false, - "buildOptimizer": true, - "budgets": [ - { - "type": "initial", - "maximumWarning": "2mb", - "maximumError": "5mb" - }, - { - "type": "anyComponentStyle", - "maximumWarning": "6kb", - "maximumError": "10kb" - } - ] + "outputHashing": "all" + }, + "development": { + "buildOptimizer": false, + "optimization": false, + "vendorChunk": true, + "extractLicenses": false, + "sourceMap": true, + "namedChunks": true } }, - "outputs": ["{options.outputPath}"] + "defaultConfiguration": "production" }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", - "options": { - "browserTarget": "data-example-app:build" - }, "configurations": { "production": { - "browserTarget": "data-example-app:build:production" + "browserTarget": "tour-of-heroes-data:build:production" + }, + "development": { + "browserTarget": "tour-of-heroes-data:build:development" } - } + }, + "defaultConfiguration": "development" }, "extract-i18n": { "builder": "@angular-devkit/build-angular:extract-i18n", "options": { - "browserTarget": "data-example-app:build" + "browserTarget": "tour-of-heroes-data:build" } }, "lint": { "builder": "@nrwl/linter:eslint", "options": { "lintFilePatterns": [ - "projects/data-example-app/*/**/*.ts", - "projects/data-example-app/*/**/*.html" + "projects/tour-of-heroes-data/src/**/*.ts", + "projects/tour-of-heroes-data/src/**/*.html" ] } }, "test": { "builder": "@nrwl/jest:jest", + "outputs": ["coverage/projects/tour-of-heroes-data"], "options": { - "jestConfig": "projects/data-example-app/jest.config.js", - "tsConfig": "projects/data-example-app/tsconfig.spec.json", - "passWithNoTests": true, - "setupFile": "projects/data-example-app/src/test-setup.ts" + "jestConfig": "projects/tour-of-heroes-data/jest.config.js", + "passWithNoTests": true } } } diff --git a/jest.config.js b/jest.config.js index c1d55a3603..abab66748e 100644 --- a/jest.config.js +++ b/jest.config.js @@ -11,5 +11,6 @@ module.exports = { '/modules/component', '/modules/component-store', '/modules/schematics-core', + '/projects/tour-of-heroes-data', ], }; diff --git a/nx.json b/nx.json index 212267997b..bb52127d3f 100644 --- a/nx.json +++ b/nx.json @@ -8,7 +8,9 @@ ".circleci/config.yml": "*", ".eslintrc.json": "*" }, - "affected": { "defaultBase": "master" }, + "affected": { + "defaultBase": "master" + }, "npmScope": "ngrx", "tasksRunnerOptions": { "default": { @@ -27,19 +29,45 @@ } } }, - "workspaceLayout": { "appsDir": "projects", "libsDir": "modules" }, + "workspaceLayout": { + "appsDir": "projects", + "libsDir": "modules" + }, "projects": { - "example-app": { "tags": [] }, - "store": { "tags": [] }, - "effects": { "tags": [] }, - "data": { "tags": [] }, - "entity": { "tags": [] }, - "store-devtools": { "tags": [] }, - "router-store": { "tags": [] }, - "schematics": { "tags": [] }, - "component": { "tags": [] }, - "component-store": { "tags": [] }, - "example-app-e2e": { "tags": [], "implicitDependencies": ["example-app"] }, + "example-app": { + "tags": [] + }, + "store": { + "tags": [] + }, + "effects": { + "tags": [] + }, + "data": { + "tags": [] + }, + "entity": { + "tags": [] + }, + "store-devtools": { + "tags": [] + }, + "router-store": { + "tags": [] + }, + "schematics": { + "tags": [] + }, + "component": { + "tags": [] + }, + "component-store": { + "tags": [] + }, + "example-app-e2e": { + "tags": [], + "implicitDependencies": ["example-app"] + }, "docs-app": { "tags": [], "implicitDependencies": [ @@ -54,7 +82,11 @@ "component-store" ] }, - "schematics-core": { "tags": [] }, - "data-example-app": { "tags": [] } + "schematics-core": { + "tags": [] + }, + "tour-of-heroes-data": { + "tags": [] + } } } diff --git a/package.json b/package.json index 284d6365d7..9605a188a5 100644 --- a/package.json +++ b/package.json @@ -122,6 +122,7 @@ "@angular/bazel": "^12.0.4", "@angular/cli": "12.0.4", "@angular/compiler-cli": "12.0.4", + "@angular/language-service": "^12.0.0", "@babel/core": "7.9.0", "@bazel/bazelisk": "1.4.0", "@bazel/buildifier": "^2.2.1", @@ -155,6 +156,7 @@ "@types/shelljs": "^0.8.5", "@typescript-eslint/eslint-plugin": "4.19.0", "@typescript-eslint/parser": "4.19.0", + "angular-in-memory-web-api": "^0.11.0", "chokidar": "^1.7.0", "chokidar-cli": "^1.2.0", "conventional-changelog": "^1.1.4", @@ -229,4 +231,4 @@ "pre-commit": "lint-staged" } } -} +} \ No newline at end of file diff --git a/projects/data-example-app/README.md b/projects/data-example-app/README.md deleted file mode 100644 index a4e65a5733..0000000000 --- a/projects/data-example-app/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Example NgRx Data App - -To serve the NgRx Data example app run `ng serve data-example-app` - -The example app will be available at `http://localhost:4200/` - -No backend is needed as the example app uses a fake backend implemented with an HTTP Interceptor (`fake-backened-interceptor.service.ts`) to -intercept requests and make the CRUD operations. diff --git a/projects/data-example-app/src/app/app-routing.module.ts b/projects/data-example-app/src/app/app-routing.module.ts deleted file mode 100644 index a0421515c7..0000000000 --- a/projects/data-example-app/src/app/app-routing.module.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; - -@NgModule({ - imports: [ - RouterModule.forRoot( - [ - { - path: '', - loadChildren: () => - import('../board/board.module').then((m) => m.BoardModule), - }, - ], - { initialNavigation: 'enabled' } - ), - ], - exports: [RouterModule], -}) -export class AppRoutingModule {} diff --git a/projects/data-example-app/src/app/app.component.html b/projects/data-example-app/src/app/app.component.html deleted file mode 100644 index 0680b43f9c..0000000000 --- a/projects/data-example-app/src/app/app.component.html +++ /dev/null @@ -1 +0,0 @@ - diff --git a/projects/data-example-app/src/app/app.component.scss b/projects/data-example-app/src/app/app.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/projects/data-example-app/src/app/app.module.ts b/projects/data-example-app/src/app/app.module.ts deleted file mode 100644 index 96d345c5ac..0000000000 --- a/projects/data-example-app/src/app/app.module.ts +++ /dev/null @@ -1,50 +0,0 @@ -import { BrowserModule } from '@angular/platform-browser'; -import { NgModule } from '@angular/core'; - -import { AppComponent } from './app.component'; -import { AppRoutingModule } from './app-routing.module'; -import { StoreModule } from '@ngrx/store'; -import { EffectsModule } from '@ngrx/effects'; -import { EntityDataModule } from '@ngrx/data'; -import { storyEntityMetadata } from '../state/story.metadata'; -import { DragDropModule } from '@angular/cdk/drag-drop'; -import { - HTTP_INTERCEPTORS, - HttpClientJsonpModule, - HttpClientModule, -} from '@angular/common/http'; -import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; -import { FakeBackendInterceptor } from '../fake-backend-interceptor.service'; -import { StoreDevtoolsModule } from '@ngrx/store-devtools'; - -@NgModule({ - declarations: [AppComponent], - imports: [ - BrowserModule, - AppRoutingModule, - StoreModule.forRoot({}), - EffectsModule.forRoot([]), - StoreDevtoolsModule.instrument(), - EntityDataModule.forRoot({ - entityMetadata: { - Story: storyEntityMetadata, - }, - pluralNames: { - Story: 'stories', - }, - }), - BrowserAnimationsModule, - DragDropModule, - HttpClientModule, - HttpClientJsonpModule, - ], - providers: [ - { - provide: HTTP_INTERCEPTORS, - multi: true, - useClass: FakeBackendInterceptor, - }, - ], - bootstrap: [AppComponent], -}) -export class AppModule {} diff --git a/projects/data-example-app/src/board/board-routing.module.ts b/projects/data-example-app/src/board/board-routing.module.ts deleted file mode 100644 index 228370d08a..0000000000 --- a/projects/data-example-app/src/board/board-routing.module.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { NgModule } from '@angular/core'; -import { RouterModule } from '@angular/router'; -import { BoardComponent } from './board.component'; - -@NgModule({ - imports: [ - RouterModule.forChild([ - { - path: '', - component: BoardComponent, - }, - ]), - ], -}) -export class BoardRoutingModule {} diff --git a/projects/data-example-app/src/board/board.component.html b/projects/data-example-app/src/board/board.component.html deleted file mode 100644 index f0c3f23d52..0000000000 --- a/projects/data-example-app/src/board/board.component.html +++ /dev/null @@ -1,7 +0,0 @@ - - diff --git a/projects/data-example-app/src/board/board.component.scss b/projects/data-example-app/src/board/board.component.scss deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/projects/data-example-app/src/board/board.component.ts b/projects/data-example-app/src/board/board.component.ts deleted file mode 100644 index 898faf482f..0000000000 --- a/projects/data-example-app/src/board/board.component.ts +++ /dev/null @@ -1,40 +0,0 @@ -import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { StoryDataService } from '../state/story-data.service'; -import { - CreateStoryDto, - DeleteStoryDto, - Stories, - UpdateStoryDto, -} from '../state/story'; - -@Component({ - selector: 'ngrx-board-component', - templateUrl: './board.component.html', - styleUrls: ['./board.component.scss'], -}) -export class BoardComponent implements OnInit { - stories$: Observable = this.storyDataService.groupedStories$; - - constructor(private storyDataService: StoryDataService) {} - - ngOnInit(): void { - this.storyDataService.getAll(); - } - - add(story: CreateStoryDto): void { - this.storyDataService.add(story, { isOptimistic: false }); - } - - update(story: UpdateStoryDto): void { - this.storyDataService.update(story, { isOptimistic: true }); - } - - loadAll(): void { - this.storyDataService.getAll(); - } - - delete(id: DeleteStoryDto): void { - this.storyDataService.delete(id); - } -} diff --git a/projects/data-example-app/src/board/board.module.ts b/projects/data-example-app/src/board/board.module.ts deleted file mode 100644 index e665c1a8dc..0000000000 --- a/projects/data-example-app/src/board/board.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BoardComponent } from './board.component'; -import { BoardUiModule } from './ui/board-ui.module'; -import { BoardRoutingModule } from './board-routing.module'; - -@NgModule({ - imports: [CommonModule, BoardUiModule, BoardRoutingModule], - declarations: [BoardComponent], - exports: [BoardComponent], -}) -export class BoardModule {} diff --git a/projects/data-example-app/src/board/ui/board-ui.component.html b/projects/data-example-app/src/board/ui/board-ui.component.html deleted file mode 100644 index 04d7dc2a9a..0000000000 --- a/projects/data-example-app/src/board/ui/board-ui.component.html +++ /dev/null @@ -1,33 +0,0 @@ -
-
-

Column {{ i }}

- - - -
-
- - - - -
-
-
-
diff --git a/projects/data-example-app/src/board/ui/board-ui.component.scss b/projects/data-example-app/src/board/ui/board-ui.component.scss deleted file mode 100644 index 2de4cc03ab..0000000000 --- a/projects/data-example-app/src/board/ui/board-ui.component.scss +++ /dev/null @@ -1,55 +0,0 @@ -.list-container { - width: 400px; - max-width: 100%; - margin: 0 25px 25px 0; - display: inline-block; - vertical-align: top; - border: black; -} - -.drag-list { - padding: 20px; - border: solid 1px #ccc; - min-height: 60px; - background: white; - border-radius: 4px; - overflow: hidden; - display: block; -} - -.drag-box { - padding: 20px 10px; - border: solid 1px #ccc; - color: rgba(0, 0, 0, 0.87); - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - box-sizing: border-box; - cursor: move; - background: white; - font-size: 14px; -} - -.cdk-drag-preview { - box-sizing: border-box; - border-radius: 4px; - box-shadow: 0 5px 5px -3px rgba(0, 0, 0, 0.2), - 0 8px 10px 1px rgba(0, 0, 0, 0.14), 0 3px 14px 2px rgba(0, 0, 0, 0.12); -} - -.cdk-drag-placeholder { - opacity: 0; -} - -.cdk-drag-animating { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} - -.drag-box:last-child { - border: none; -} - -.drag.cdk-drop-list-dragging .drag-box:not(.cdk-drag-placeholder) { - transition: transform 250ms cubic-bezier(0, 0, 0.2, 1); -} diff --git a/projects/data-example-app/src/board/ui/board-ui.component.ts b/projects/data-example-app/src/board/ui/board-ui.component.ts deleted file mode 100644 index e076a070b6..0000000000 --- a/projects/data-example-app/src/board/ui/board-ui.component.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { CdkDragDrop } from '@angular/cdk/drag-drop'; -import { Component, EventEmitter, Input, Output } from '@angular/core'; -import { - CreateStoryDto, - DeleteStoryDto, - Stories, - Story, - UpdateStoryDto, -} from '../../state/story'; - -@Component({ - selector: 'ngrx-board-ui', - templateUrl: './board-ui.component.html', - styleUrls: ['./board-ui.component.scss'], -}) -export class BoardUiComponent { - @Input() stories: Stories[] = []; - - @Output() add = new EventEmitter(); - - @Output() delete = new EventEmitter(); - - @Output() update = new EventEmitter(); - - addNew(column: number, stories: Stories): void { - this.add.emit({ - order: stories.length, - column, - title: `Order ${stories.length} Column ${column}`, - description: '', - }); - } - - dropStory(event: CdkDragDrop, column: number): void { - this.update.emit({ - column, - order: event.currentIndex, - storyId: event.item.data.storyId, - }); - } - - updateStory(story: Story, title: string): void { - this.update.emit({ - storyId: story.storyId, - title, - }); - } -} diff --git a/projects/data-example-app/src/board/ui/board-ui.module.ts b/projects/data-example-app/src/board/ui/board-ui.module.ts deleted file mode 100644 index 71bc809d7b..0000000000 --- a/projects/data-example-app/src/board/ui/board-ui.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { NgModule } from '@angular/core'; -import { CommonModule } from '@angular/common'; -import { BoardUiComponent } from './board-ui.component'; -import { DragDropModule } from '@angular/cdk/drag-drop'; -import { FormsModule } from '@angular/forms'; - -@NgModule({ - imports: [CommonModule, DragDropModule, FormsModule], - declarations: [BoardUiComponent], - exports: [BoardUiComponent], -}) -export class BoardUiModule {} diff --git a/projects/data-example-app/src/fake-backend-interceptor.service.ts b/projects/data-example-app/src/fake-backend-interceptor.service.ts deleted file mode 100644 index a8a58753c1..0000000000 --- a/projects/data-example-app/src/fake-backend-interceptor.service.ts +++ /dev/null @@ -1,91 +0,0 @@ -import { Injectable } from '@angular/core'; -import { - HttpErrorResponse, - HttpEvent, - HttpHandler, - HttpInterceptor, - HttpRequest, - HttpResponse, -} from '@angular/common/http'; -import { Observable, of, throwError } from 'rxjs'; -import { Story } from './state/story'; - -const data: Record = {}; - -@Injectable({ - providedIn: 'root', -}) -export class FakeBackendInterceptor implements HttpInterceptor { - intercept( - req: HttpRequest, - next: HttpHandler - ): Observable> { - const { url, body } = req; - - let storyId: string | undefined; - - if (req.method === 'GET' && req.url.includes('stories')) { - return of(new HttpResponse({ status: 200, body: Object.values(data) })); - } - - switch (req.method) { - case 'GET': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(new HttpErrorResponse({ status: 400 })); - } - - const obj = data[storyId]; - if (obj) { - return of(new HttpResponse({ status: 200, body: obj })); - } - - return throwError(new HttpErrorResponse({ status: 404 })); - - case 'POST': - storyId = Date.now().toString(); - data[storyId] = { - ...body, - storyId, - createdAt: new Date(), - updatedAt: new Date(), - }; - - return of(new HttpResponse({ status: 201, body: data[storyId] })); - - case 'PUT': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(new HttpErrorResponse({ status: 400 })); - } - - if (!data[storyId]) { - return throwError(new HttpErrorResponse({ status: 404 })); - } - - data[storyId] = { - ...data[storyId], - ...body, - updatedAt: new Date(), - }; - - return of(new HttpResponse({ status: 200, body: data[storyId] })); - - case 'DELETE': - storyId = url.split('/').pop(); - - if (!storyId) { - return throwError(new HttpErrorResponse({ status: 400 })); - } - - delete data[storyId]; - - return of(new HttpResponse({ status: 200, body: storyId })); - - default: - return throwError(new HttpErrorResponse({ status: 501 })); - } - } -} diff --git a/projects/data-example-app/src/index.html b/projects/data-example-app/src/index.html deleted file mode 100644 index 5b0f1d0903..0000000000 --- a/projects/data-example-app/src/index.html +++ /dev/null @@ -1,13 +0,0 @@ - - - - - DataExampleApp - - - - - - - - diff --git a/projects/data-example-app/src/state/story-data.service.ts b/projects/data-example-app/src/state/story-data.service.ts deleted file mode 100644 index b73a1f0a01..0000000000 --- a/projects/data-example-app/src/state/story-data.service.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { - EntityCollectionServiceBase, - EntityCollectionServiceElementsFactory, -} from '@ngrx/data'; -import { select } from '@ngrx/store'; -import { Injectable } from '@angular/core'; -import { selectStories } from './story.selectors'; -import { Story } from './story'; - -@Injectable({ - providedIn: 'root', -}) -export class StoryDataService extends EntityCollectionServiceBase { - groupedStories$ = this.entities$.pipe(select(selectStories)); - - constructor(serviceElementsFactory: EntityCollectionServiceElementsFactory) { - super('Story', serviceElementsFactory); - } -} diff --git a/projects/data-example-app/src/state/story.metadata.ts b/projects/data-example-app/src/state/story.metadata.ts deleted file mode 100644 index 8969f79214..0000000000 --- a/projects/data-example-app/src/state/story.metadata.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { EntityMetadata } from '@ngrx/data'; -import { Story } from './story'; - -export const storyEntityMetadata: EntityMetadata = { - entityName: 'Story', - selectId: (entity: Story): string => entity.storyId, - sortComparer: (a, b) => a.createdAt.getTime() - b.createdAt.getTime(), - filterFn: (entities, pattern) => - entities.filter( - (entity) => - entity.title?.includes(pattern) || entity.title?.includes(pattern) - ), -}; diff --git a/projects/data-example-app/src/state/story.selectors.ts b/projects/data-example-app/src/state/story.selectors.ts deleted file mode 100644 index e889fc6fc4..0000000000 --- a/projects/data-example-app/src/state/story.selectors.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { createSelector } from '@ngrx/store'; -import { Stories, Story } from './story'; - -export const selectStories = createSelector( - (stories) => stories, - (stories: Stories) => - stories.reduce( - (prev, cur) => { - prev[cur.column].push(cur); - - prev[cur.column].sort((a, b) => a.order - b.order); - - return prev; - }, - [[], [], [], []] - ) -); diff --git a/projects/data-example-app/src/state/story.ts b/projects/data-example-app/src/state/story.ts deleted file mode 100644 index 502e51e584..0000000000 --- a/projects/data-example-app/src/state/story.ts +++ /dev/null @@ -1,18 +0,0 @@ -export interface Story { - storyId: string; - order: number; - column: number; - title: string; - description: string; - createdAt: Date; - updatedAt: Date; -} - -export type Stories = Story[]; - -export type CreateStoryDto = Partial; - -export type UpdateStoryDto = Required> & - Partial>; - -export type DeleteStoryDto = string; diff --git a/projects/data-example-app/src/styles.scss b/projects/data-example-app/src/styles.scss deleted file mode 100644 index 90d4ee0072..0000000000 --- a/projects/data-example-app/src/styles.scss +++ /dev/null @@ -1 +0,0 @@ -/* You can add global styles to this file, and also import other style files */ diff --git a/projects/data-example-app/src/test-setup.ts b/projects/data-example-app/src/test-setup.ts deleted file mode 100644 index 485edda61e..0000000000 --- a/projects/data-example-app/src/test-setup.ts +++ /dev/null @@ -1,15 +0,0 @@ -import 'jest-preset-angular'; -(global as any)['CSS'] = null; - -/** - * ISSUE: https://github.com/angular/material2/issues/7101 - * Workaround for JSDOM missing transform property - */ -Object.defineProperty(document.body.style, 'transform', { - value: () => { - return { - enumerable: true, - configurable: true, - }; - }, -}); diff --git a/projects/data-example-app/.browserslistrc b/projects/tour-of-heroes-data/.browserslistrc similarity index 100% rename from projects/data-example-app/.browserslistrc rename to projects/tour-of-heroes-data/.browserslistrc diff --git a/projects/data-example-app/.eslintrc.json b/projects/tour-of-heroes-data/.eslintrc.json similarity index 72% rename from projects/data-example-app/.eslintrc.json rename to projects/tour-of-heroes-data/.eslintrc.json index ba64def100..b7041b9dc2 100644 --- a/projects/data-example-app/.eslintrc.json +++ b/projects/tour-of-heroes-data/.eslintrc.json @@ -1,6 +1,6 @@ { "extends": ["../../.eslintrc.json"], - "ignorePatterns": ["!**/*", "**/environment.prod.ts"], + "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts"], @@ -8,9 +8,6 @@ "plugin:@nrwl/nx/angular", "plugin:@angular-eslint/template/process-inline-templates" ], - "parserOptions": { - "project": ["projects/data-example-app/tsconfig.*?.json"] - }, "rules": { "@angular-eslint/directive-selector": [ "error", @@ -27,10 +24,8 @@ "prefix": "ngrx", "style": "kebab-case" } - ], - "no-case-declarations": "off" - }, - "plugins": ["@angular-eslint/eslint-plugin", "@typescript-eslint"] + ] + } }, { "files": ["*.html"], diff --git a/projects/data-example-app/jest.config.js b/projects/tour-of-heroes-data/jest.config.js similarity index 86% rename from projects/data-example-app/jest.config.js rename to projects/tour-of-heroes-data/jest.config.js index 690e807be6..89e80023fc 100644 --- a/projects/data-example-app/jest.config.js +++ b/projects/tour-of-heroes-data/jest.config.js @@ -1,7 +1,7 @@ module.exports = { - displayName: 'Data Example App', + displayName: 'tour-of-heroes-data', preset: '../../jest.preset.js', - coverageDirectory: '../../coverage/apps/data-example-app', + coverageDirectory: '../../coverage/apps/tour-of-heroes-data', snapshotSerializers: [ 'jest-preset-angular/build/AngularNoNgAttributesSnapshotSerializer.js', 'jest-preset-angular/build/AngularSnapshotSerializer.js', diff --git a/projects/tour-of-heroes-data/src/app/app.component.html b/projects/tour-of-heroes-data/src/app/app.component.html new file mode 100644 index 0000000000..14b4ea8905 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/app.component.html @@ -0,0 +1,6 @@ +
+ +
+ +
+
diff --git a/projects/tour-of-heroes-data/src/app/app.component.scss b/projects/tour-of-heroes-data/src/app/app.component.scss new file mode 100644 index 0000000000..96406ede40 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/app.component.scss @@ -0,0 +1,3 @@ +.title { + margin-right: 1em; +} diff --git a/projects/data-example-app/src/app/app.component.ts b/projects/tour-of-heroes-data/src/app/app.component.ts similarity index 100% rename from projects/data-example-app/src/app/app.component.ts rename to projects/tour-of-heroes-data/src/app/app.component.ts diff --git a/projects/tour-of-heroes-data/src/app/app.module.ts b/projects/tour-of-heroes-data/src/app/app.module.ts new file mode 100644 index 0000000000..5c4a6ee360 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/app.module.ts @@ -0,0 +1,46 @@ +import { HttpClientModule } from '@angular/common/http'; +import { NgModule } from '@angular/core'; +import { BrowserModule } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { RouterModule, Routes } from '@angular/router'; +import { AppComponent } from './app.component'; +import { CoreModule, InMemoryDataService } from './core'; +import { + HttpClientInMemoryWebApiModule, + InMemoryDbService, +} from 'angular-in-memory-web-api'; +import { AppStoreModule } from './store/app-store.module'; + +const routes: Routes = [ + { path: '', pathMatch: 'full', redirectTo: 'heroes' }, + { + path: 'heroes', + loadChildren: () => + import('./heroes/heroes.module').then((m) => m.HeroesModule), + }, + { + path: 'villains', + loadChildren: () => + import('./villains/villains.module').then((m) => m.VillainsModule), + }, +]; + +@NgModule({ + imports: [ + BrowserModule, + BrowserAnimationsModule, + CoreModule, + HttpClientModule, + RouterModule.forRoot(routes), + HttpClientInMemoryWebApiModule.forRoot(InMemoryDataService, { + dataEncapsulation: false, + delay: 300, + passThruUnknownUrl: true, + }), + AppStoreModule, + ], + providers: [{ provide: InMemoryDataService, useExisting: InMemoryDbService }], + declarations: [AppComponent], + bootstrap: [AppComponent], +}) +export class AppModule {} diff --git a/projects/tour-of-heroes-data/src/app/core/core.module.ts b/projects/tour-of-heroes-data/src/app/core/core.module.ts new file mode 100644 index 0000000000..7d3e3fe647 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/core.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { NgModule, Optional, SkipSelf } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { MaterialModule } from '../material/material.module'; +import { SharedModule } from '../shared/shared.module'; +import { ModalComponent } from './modal/modal.component'; +import { throwIfAlreadyLoaded } from './module-import-check'; +import { ToolbarComponent } from './toolbar/toolbar.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + MaterialModule, + RouterModule, // because we use and routerLink + ], + declarations: [ModalComponent, ToolbarComponent], + exports: [ToolbarComponent], + entryComponents: [ModalComponent], +}) +export class CoreModule { + constructor( + @Optional() + @SkipSelf() + parentModule: CoreModule + ) { + throwIfAlreadyLoaded(parentModule, 'CoreModule'); + } +} diff --git a/projects/tour-of-heroes-data/src/app/core/e2e-check.ts b/projects/tour-of-heroes-data/src/app/core/e2e-check.ts new file mode 100644 index 0000000000..3f451678f8 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/e2e-check.ts @@ -0,0 +1,2 @@ +/** True if running under e2e testing (e.g., launch URL ends `?e2e`) */ +export const isE2E = window.location.search.includes('e2e'); diff --git a/projects/tour-of-heroes-data/src/app/core/in-memory-data.service.ts b/projects/tour-of-heroes-data/src/app/core/in-memory-data.service.ts new file mode 100644 index 0000000000..151ace0307 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/in-memory-data.service.ts @@ -0,0 +1,152 @@ +/** + * Hero-oriented InMemoryDbService with method overrides. + */ +import { Injectable } from '@angular/core'; + +import { + ParsedRequestUrl, + RequestInfo, + RequestInfoUtilities, +} from 'angular-in-memory-web-api'; + +import { Hero, Villain } from './model'; + +/** In-memory database data */ +interface Db { + [collectionName: string]: any[]; +} + +@Injectable() +export class InMemoryDataService { + /** True if in-mem service is intercepting; all requests pass thru when false. */ + active = true; + maxId = 0; + + /** In-memory database data */ + db: Db = {}; + + /** Create the in-memory database on start or by command */ + createDb(reqInfo?: RequestInfo) { + this.db = getDbData(); + + if (reqInfo) { + const body = reqInfo.utils.getJsonBody(reqInfo.req) || {}; + if (body.clear === true) { + // tslint:disable-next-line:forin + for (const coll in this.db) { + this.db[coll].length = 0; + } + } + + this.active = !!body.active; + } + return this.db; + } + + /** + * Simulate generating new Id on the server + * All collections in this db have numeric ids. + * Seed grows by highest id seen in any of the collections. + */ + genId(collection: { id: number }[], collectionName: string) { + this.maxId = + 1 + + collection.reduce((prev, cur) => Math.max(prev, cur.id || 0), this.maxId); + return this.maxId; + } + + /** + * Override `parseRequestUrl` + * Manipulates the request URL or the parsed result. + * If in-mem is inactive, clear collectionName so that service passes request thru. + * If in-mem is active, after parsing with the default parser, + * @param url from request URL + * @param utils for manipulating parsed URL + */ + parseRequestUrl(url: string, utils: RequestInfoUtilities): ParsedRequestUrl { + const parsed = utils.parseRequestUrl(url); + const isDefaultRoot = parsed.apiBase === 'api/'; + parsed.collectionName = + this.active && isDefaultRoot + ? mapCollectionName(parsed.collectionName) + : undefined; + return parsed; + } +} + +/** + * Remap a known singular collection name ("hero") + * to the plural collection name ("heroes"); else return the name + * @param name - collection name from the parsed URL + */ +function mapCollectionName(name: string): string { + return ( + ({ + hero: 'heroes', + villain: 'villains', + } as any)[name] || name + ); +} + +/** + * Development data + */ +function getDbData() { + const heroes: Hero[] = [ + { + id: 11, + name: 'Maxwell Smart', + saying: 'Missed it by that much.', + }, + { + id: 12, + name: 'Bullwinkle J. Moose', + saying: 'Watch me pull a rabbit out of a hat.', + }, + { + id: 13, + name: 'Muhammad Ali', + saying: 'Float like a butterfly, sting like a bee.', + }, + { + id: 14, + name: 'Eleanor Roosevelt', + saying: 'No one can make you feel inferior without your consent.', + }, + ]; + + const villains: Villain[] = [ + { + id: 21, + name: 'Dr. Evil', + saying: 'One million dollars!', + }, + { + id: 22, + name: 'Agent Smith', + saying: 'Human beings are a disease.', + }, + { + id: 23, + name: 'Natasha Fatale', + saying: 'You can say that again, dahling.', + }, + { + id: 24, + name: 'Goldfinger', + saying: 'No, I expect you to die!', + }, + { + id: 25, + name: 'West Witch', + saying: "I'll get you, my pretty, and your little dog too!", + }, + { + id: 26, + name: 'Tony Montana', + saying: 'Say hello to my little friend.', + }, + ]; + + return { heroes, villains } as Db; +} diff --git a/projects/tour-of-heroes-data/src/app/core/index.ts b/projects/tour-of-heroes-data/src/app/core/index.ts new file mode 100644 index 0000000000..8ef600d83f --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/index.ts @@ -0,0 +1,6 @@ +export * from './core.module'; +export * from './e2e-check'; +export * from './in-memory-data.service'; +export * from './modal/modal.component'; +export * from './model'; +export * from './toast.service'; diff --git a/projects/tour-of-heroes-data/src/app/core/modal/modal.component.html b/projects/tour-of-heroes-data/src/app/core/modal/modal.component.html new file mode 100644 index 0000000000..c86a990470 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/modal/modal.component.html @@ -0,0 +1,10 @@ +

{{ title }}

+ + +

{{ message }}

+
+ + + + + diff --git a/projects/tour-of-heroes-data/src/app/core/modal/modal.component.ts b/projects/tour-of-heroes-data/src/app/core/modal/modal.component.ts new file mode 100644 index 0000000000..a109038775 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/modal/modal.component.ts @@ -0,0 +1,27 @@ +import { Component, Inject } from '@angular/core'; +import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; + +@Component({ + selector: 'ngrx-modal', + templateUrl: './modal.component.html', +}) +export class ModalComponent { + message: string; + title: string; + + constructor( + private dialogRef: MatDialogRef, + @Inject(MAT_DIALOG_DATA) data + ) { + this.title = data.title; + this.message = data.message; + } + + ok() { + this.dialogRef.close(true); + } + + cancel() { + this.dialogRef.close(false); + } +} diff --git a/projects/tour-of-heroes-data/src/app/core/model/hero.ts b/projects/tour-of-heroes-data/src/app/core/model/hero.ts new file mode 100644 index 0000000000..cf3498962b --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/model/hero.ts @@ -0,0 +1,5 @@ +export class Hero { + id: number; + name: string; + saying: string; +} diff --git a/projects/tour-of-heroes-data/src/app/core/model/index.ts b/projects/tour-of-heroes-data/src/app/core/model/index.ts new file mode 100644 index 0000000000..91b952f986 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/model/index.ts @@ -0,0 +1,2 @@ +export * from './hero'; +export * from './villain'; diff --git a/projects/tour-of-heroes-data/src/app/core/model/villain.ts b/projects/tour-of-heroes-data/src/app/core/model/villain.ts new file mode 100644 index 0000000000..549e2eca16 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/model/villain.ts @@ -0,0 +1,5 @@ +export class Villain { + id: number; + name: string; + saying: string; +} diff --git a/projects/tour-of-heroes-data/src/app/core/module-import-check.ts b/projects/tour-of-heroes-data/src/app/core/module-import-check.ts new file mode 100644 index 0000000000..0a470aba5d --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/module-import-check.ts @@ -0,0 +1,6 @@ +export function throwIfAlreadyLoaded(parentModule: any, moduleName: string) { + if (parentModule) { + const msg = `${moduleName} has already been loaded. Import Core modules in the AppModule only.`; + throw new Error(msg); + } +} diff --git a/projects/tour-of-heroes-data/src/app/core/toast.service.ts b/projects/tour-of-heroes-data/src/app/core/toast.service.ts new file mode 100644 index 0000000000..d25d9c8625 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/toast.service.ts @@ -0,0 +1,18 @@ +import { Injectable } from '@angular/core'; +import { MatSnackBar } from '@angular/material/snack-bar'; +import { isE2E } from './e2e-check'; + +@Injectable({ providedIn: 'root' }) +export class ToastService { + constructor(public snackBar: MatSnackBar) {} + + openSnackBar(message: string, action: string) { + if (isE2E) { + console.log(`${message} - ${action}`); + } else { + this.snackBar.open(message, action, { + duration: 2000, + }); + } + } +} diff --git a/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.html b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.html new file mode 100644 index 0000000000..33f54d9e42 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.html @@ -0,0 +1,46 @@ + + +
+ + + + {{ labTitle }} + + + Heroes + + + Villains + +
+
+ + + +
diff --git a/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.scss b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.scss new file mode 100644 index 0000000000..cfb3c4fc43 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.scss @@ -0,0 +1,100 @@ +@import 'mixin'; + +.pull-right { + position: fixed !important; + right: 8px; +} + +.ng-title-icon > i { + background-image: url('../../../assets/ng.png'); + background-repeat: no-repeat; + background-position: center center; + padding: 1.2em; +} + +.title { + margin-right: 1em; +} + +.item { + margin-left: 16px; +} + +.item, +a { + vertical-align: middle; +} + +.router-link-active { + @include primary-background-contrast-color; +} + +.footer-container { + display: flex; + flex-flow: row wrap; + width: 100%; + + > * { + // padding: 10px; + // flex: 1 100%; + display: flex; + } +} + +@media only screen and (max-device-width: 480px) { + .footer-container { + > * { + font-size: 12px; + } + } +} + +.commands { + flex: 18 0px; + order: 1; + align-self: center; + padding-left: 8px; +} + +.avatar { + flex: 1 auto; +} + +.spacer { + order: 0; + flex: 18 0px; +} + +.avatar-1 { + order: 1; +} + +.avatar-2 { + order: 2; +} + +.avatar-1 > i, +.avatar-2 > i { + background-repeat: no-repeat; + background-position: center center; + padding: 16px; + + border: 0; + border-radius: 50%; + opacity: 1; +} + +.avatar-1 > i { + background-image: url('../../../assets/wb.jpg'); +} + +.avatar-2 > i { + background-image: url('../../../assets/jp.jpg'); +} + +.row-commands { + height: 40px; + border-top: 1px solid white; + // @include primary-background-contrast-color; // end + @include accent-background-color; // begin +} diff --git a/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.ts b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.ts new file mode 100644 index 0000000000..6ea872fd1d --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/core/toolbar/toolbar.component.ts @@ -0,0 +1,11 @@ +import { Component } from '@angular/core'; + +@Component({ + selector: 'ngrx-toolbar', + templateUrl: './toolbar.component.html', + styleUrls: ['./toolbar.component.scss'], +}) +export class ToolbarComponent { + labTitle = 'ngrx/data-lab'; + labState = 'now using ngrx/data'; +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.html b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.html new file mode 100644 index 0000000000..fe93c4790f --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.html @@ -0,0 +1,64 @@ + + + Hero Details + + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.scss b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.scss new file mode 100644 index 0000000000..efa9930807 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.scss @@ -0,0 +1,6 @@ +@import 'mixin'; + +.mat-card { + @include mat-card-layout; + @include editarea-margins; +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.ts b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.ts new file mode 100644 index 0000000000..547cb6b1ec --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-detail/hero-detail.component.ts @@ -0,0 +1,81 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Hero } from '../../core'; + +@Component({ + selector: 'ngrx-hero-detail', + templateUrl: './hero-detail.component.html', + styleUrls: ['./hero-detail.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HeroDetailComponent implements OnChanges { + @Input() hero: Hero; + @Output() unselect = new EventEmitter(); + @Output() add = new EventEmitter(); + @Output() update = new EventEmitter(); + + @ViewChild('name', { static: true }) nameElement: ElementRef; + + addMode = false; + + form = this.fb.group({ + id: [], + name: ['', Validators.required], + saying: [''], + }); + + constructor(private fb: FormBuilder) {} + + ngOnChanges(changes: SimpleChanges) { + this.setFocus(); + if (this.hero && this.hero.id) { + this.form.patchValue(this.hero); + this.addMode = false; + } else { + this.form.reset(); + this.addMode = true; + } + } + + addHero(form: FormGroup) { + const { value, valid, touched } = form; + if (touched && valid) { + this.add.emit({ ...this.hero, ...value }); + } + this.close(); + } + + close() { + this.unselect.emit(); + } + + saveHero(form: FormGroup) { + if (this.addMode) { + this.addHero(form); + } else { + this.updateHero(form); + } + } + + setFocus() { + this.nameElement.nativeElement.focus(); + } + + updateHero(form: FormGroup) { + const { value, valid, touched } = form; + if (touched && valid) { + this.update.emit({ ...this.hero, ...value }); + } + this.close(); + } +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.html b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.html new file mode 100644 index 0000000000..c3d0f4d596 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.html @@ -0,0 +1,34 @@ + + + Heroes + + +
    +
  • + +
    +
    {{ hero.id }}
    +
    +
    {{ hero.name }}
    +
    {{ hero.saying }}
    +
    +
    +
  • +
+
+
diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.scss b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.scss new file mode 100644 index 0000000000..f4fdbd2ff0 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.scss @@ -0,0 +1,9 @@ +@import 'mixin'; + +.heroes { + @include item-list; +} + +.mat-card { + @include mat-card-layout; +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.ts b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.ts new file mode 100644 index 0000000000..82f1b8beb3 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero-list/hero-list.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; +import { Hero, ModalComponent } from '../../core'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; + +@Component({ + selector: 'ngrx-hero-list', + templateUrl: './hero-list.component.html', + styleUrls: ['./hero-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class HeroListComponent { + @Input() heroes: Hero[]; + @Input() selectedHero: Hero; + @Output() deleted = new EventEmitter(); + @Output() selected = new EventEmitter(); + + constructor(public dialog: MatDialog) {} + + byId(index: number, hero: Hero) { + return hero.id; + } + + select(hero: Hero) { + this.selected.emit(hero); + } + + deleteHero(hero: Hero) { + const dialogConfig = new MatDialogConfig(); + dialogConfig.disableClose = true; + dialogConfig.autoFocus = true; + dialogConfig.width = '250px'; + dialogConfig.data = { + title: 'Delete Hero', + message: `Do you want to delete ${hero.name}`, + }; + + const dialogRef = this.dialog.open(ModalComponent, dialogConfig); + + dialogRef.afterClosed().subscribe((deleteIt) => { + console.log('The dialog was closed'); + if (deleteIt) { + this.deleted.emit(hero); + } + }); + } +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/hero.service.ts b/projects/tour-of-heroes-data/src/app/heroes/hero.service.ts new file mode 100644 index 0000000000..b15aab1374 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/hero.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { + EntityCollectionServiceBase, + EntityCollectionServiceElementsFactory, +} from '@ngrx/data'; +import { Hero } from '../core'; + +@Injectable({ providedIn: 'root' }) +export class HeroService extends EntityCollectionServiceBase { + constructor(factory: EntityCollectionServiceElementsFactory) { + super('Hero', factory); + } +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/heroes.module.ts b/projects/tour-of-heroes-data/src/app/heroes/heroes.module.ts new file mode 100644 index 0000000000..04cd496391 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/heroes.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MaterialModule } from '../material/material.module'; +import { SharedModule } from '../shared/shared.module'; +import { HeroDetailComponent } from './hero-detail/hero-detail.component'; +import { HeroListComponent } from './hero-list/hero-list.component'; +import { HeroesComponent } from './heroes/heroes.component'; + +const routes: Routes = [ + { path: '', pathMatch: 'full', component: HeroesComponent }, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + MaterialModule, + RouterModule.forChild(routes), + ], + exports: [HeroesComponent, HeroDetailComponent], + declarations: [HeroesComponent, HeroDetailComponent, HeroListComponent], +}) +export class HeroesModule {} diff --git a/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.html b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.html new file mode 100644 index 0000000000..69f59bd17c --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.html @@ -0,0 +1,52 @@ +
+
+ + +
+
+
+
+
+ + + + +
+
+
+ + +
+
diff --git a/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.scss b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.scss new file mode 100644 index 0000000000..930678d9ea --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.scss @@ -0,0 +1,13 @@ +@import 'mixin'; + +.control-panel { + @include control-panel-layout; +} + +.content-container { + @include content-container-layout; +} + +.button-panel > button { + margin-right: 12px; +} diff --git a/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.ts b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.ts new file mode 100644 index 0000000000..9f7996d213 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/heroes/heroes/heroes.component.ts @@ -0,0 +1,54 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Hero } from '../../core'; +import { HeroService } from '../hero.service'; + +@Component({ + selector: 'ngrx-heroes', + templateUrl: './heroes.component.html', + styleUrls: ['./heroes.component.scss'], +}) +export class HeroesComponent implements OnInit { + loading$: Observable; + selected: Hero; + heroes$: Observable; + + constructor(private heroService: HeroService) { + this.heroes$ = heroService.entities$; + this.loading$ = heroService.loading$; + } + + ngOnInit() { + this.getHeroes(); + } + + add(hero: Hero) { + this.heroService.add(hero); + } + + close() { + this.selected = null; + } + + delete(hero: Hero) { + this.heroService.delete(hero.id); + this.close(); + } + + enableAddMode() { + this.selected = {}; + } + + getHeroes() { + this.heroService.getAll(); + this.close(); + } + + select(hero: Hero) { + this.selected = hero; + } + + update(hero: Hero) { + this.heroService.update(hero); + } +} diff --git a/projects/tour-of-heroes-data/src/app/material/material.module.ts b/projects/tour-of-heroes-data/src/app/material/material.module.ts new file mode 100644 index 0000000000..e37effc2b0 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/material/material.module.ts @@ -0,0 +1,40 @@ +import { NgModule } from '@angular/core'; +import { MatToolbarModule } from '@angular/material/toolbar'; +import { MatSlideToggleModule } from '@angular/material/slide-toggle'; +import { MatIconModule } from '@angular/material/icon'; +import { MatDialogModule } from '@angular/material/dialog'; +import { MatCardModule } from '@angular/material/card'; +import { MatInputModule } from '@angular/material/input'; +import { MatTooltipModule } from '@angular/material/tooltip'; +import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; +import { MatButtonModule } from '@angular/material/button'; +import { MatSnackBarModule } from '@angular/material/snack-bar'; + +@NgModule({ + imports: [ + MatButtonModule, + MatCardModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + MatSlideToggleModule, + MatSnackBarModule, + MatToolbarModule, + MatTooltipModule, + ], + exports: [ + MatButtonModule, + MatCardModule, + MatDialogModule, + MatIconModule, + MatInputModule, + MatProgressSpinnerModule, + MatSlideToggleModule, + MatSnackBarModule, + MatToolbarModule, + MatTooltipModule, + ], + declarations: [], +}) +export class MaterialModule {} diff --git a/projects/tour-of-heroes-data/src/app/shared/shared.module.ts b/projects/tour-of-heroes-data/src/app/shared/shared.module.ts new file mode 100644 index 0000000000..39210f7587 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/shared/shared.module.ts @@ -0,0 +1,10 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; + +@NgModule({ + imports: [CommonModule, ReactiveFormsModule], + exports: [ReactiveFormsModule], + declarations: [], +}) +export class SharedModule {} diff --git a/projects/tour-of-heroes-data/src/app/store/app-store.module.ts b/projects/tour-of-heroes-data/src/app/store/app-store.module.ts new file mode 100644 index 0000000000..548771fdc3 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/store/app-store.module.ts @@ -0,0 +1,17 @@ +import { NgModule } from '@angular/core'; +import { EffectsModule } from '@ngrx/effects'; +import { StoreModule } from '@ngrx/store'; +import { StoreDevtoolsModule } from '@ngrx/store-devtools'; +import { environment } from '../../environments/environment'; +import { entityConfig } from './entity-metadata'; +import { EntityDataModule } from '@ngrx/data'; + +@NgModule({ + imports: [ + StoreModule.forRoot({}), + EffectsModule.forRoot([]), + environment.production ? [] : StoreDevtoolsModule.instrument(), + EntityDataModule.forRoot(entityConfig), + ], +}) +export class AppStoreModule {} diff --git a/projects/tour-of-heroes-data/src/app/store/entity-metadata.ts b/projects/tour-of-heroes-data/src/app/store/entity-metadata.ts new file mode 100644 index 0000000000..46d415d592 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/store/entity-metadata.ts @@ -0,0 +1,14 @@ +import { EntityMetadataMap } from '@ngrx/data'; + +const entityMetadata: EntityMetadataMap = { + Hero: {}, + Villain: {}, +}; + +// because the plural of "hero" is not "heros" +const pluralNames = { Hero: 'Heroes' }; + +export const entityConfig = { + entityMetadata, + pluralNames, +}; diff --git a/projects/tour-of-heroes-data/src/app/store/ngrx-data-toast.service.ts b/projects/tour-of-heroes-data/src/app/store/ngrx-data-toast.service.ts new file mode 100644 index 0000000000..ad0216cb5e --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/store/ngrx-data-toast.service.ts @@ -0,0 +1,48 @@ +import { Injectable } from '@angular/core'; +import { Actions, ofType } from '@ngrx/effects'; +import { + EntityAction, + EntityCacheAction, + ofEntityOp, + OP_ERROR, + OP_SUCCESS, +} from '@ngrx/data'; +import { filter } from 'rxjs/operators'; +import { ToastService } from '../core/toast.service'; + +/** Report ngrx-data success/error actions as toast messages * */ +@Injectable({ providedIn: 'root' }) +export class NgrxDataToastService { + constructor(actions$: Actions, toast: ToastService) { + actions$ + .pipe( + ofEntityOp(), + filter( + (ea: EntityAction) => + ea.payload.entityOp.endsWith(OP_SUCCESS) || + ea.payload.entityOp.endsWith(OP_ERROR) + ) + ) + // this service never dies so no need to unsubscribe + .subscribe((action) => + toast.openSnackBar( + `${action.payload.entityName} action`, + action.payload.entityOp + ) + ); + + actions$ + .pipe( + ofType( + EntityCacheAction.SAVE_ENTITIES_SUCCESS, + EntityCacheAction.SAVE_ENTITIES_ERROR + ) + ) + .subscribe((action: any) => + toast.openSnackBar( + `${action.type} - url: ${action.payload.url}`, + 'SaveEntities' + ) + ); + } +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.html b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.html new file mode 100644 index 0000000000..934fa23a7d --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.html @@ -0,0 +1,64 @@ + + + Villain Details + + +
+
+
+ + + +
+
+ + + +
+
+ + + +
+
+ + +
+
+
diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.scss b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.scss new file mode 100644 index 0000000000..efa9930807 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.scss @@ -0,0 +1,6 @@ +@import 'mixin'; + +.mat-card { + @include mat-card-layout; + @include editarea-margins; +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.ts b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.ts new file mode 100644 index 0000000000..199d3e4d48 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-detail/villain-detail.component.ts @@ -0,0 +1,81 @@ +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + Output, + SimpleChanges, + ViewChild, +} from '@angular/core'; +import { FormBuilder, FormGroup, Validators } from '@angular/forms'; +import { Villain } from '../../core'; + +@Component({ + selector: 'ngrx-villain-detail', + templateUrl: './villain-detail.component.html', + styleUrls: ['./villain-detail.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VillainDetailComponent implements OnChanges { + @Input() villain: Villain; + @Output() unselect = new EventEmitter(); + @Output() add = new EventEmitter(); + @Output() update = new EventEmitter(); + + @ViewChild('name', { static: true }) nameElement: ElementRef; + + addMode = false; + + form = this.fb.group({ + id: [], + name: ['', Validators.required], + saying: [''], + }); + + constructor(private fb: FormBuilder) {} + + ngOnChanges(changes: SimpleChanges) { + this.setFocus(); + if (this.villain && this.villain.id) { + this.form.patchValue(this.villain); + this.addMode = false; + } else { + this.form.reset(); + this.addMode = true; + } + } + + addVillain(form: FormGroup) { + const { value, valid, touched } = form; + if (touched && valid) { + this.add.emit({ ...this.villain, ...value }); + } + this.close(); + } + + close() { + this.unselect.emit(); + } + + saveVillain(form: FormGroup) { + if (this.addMode) { + this.addVillain(form); + } else { + this.updateVillain(form); + } + } + + setFocus() { + this.nameElement.nativeElement.focus(); + } + + updateVillain(form: FormGroup) { + const { value, valid, touched } = form; + if (touched && valid) { + this.update.emit({ ...this.villain, ...value }); + } + this.close(); + } +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.html b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.html new file mode 100644 index 0000000000..6f7921dfe3 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.html @@ -0,0 +1,37 @@ + + + Villains + + +
    +
  • + +
    +
    {{ villain.id }}
    +
    +
    {{ villain.name }}
    +
    {{ villain.saying }}
    +
    +
    +
  • +
+
+
diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.scss b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.scss new file mode 100644 index 0000000000..3b045e158b --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.scss @@ -0,0 +1,9 @@ +@import 'mixin'; + +.villains { + @include item-list; +} + +.mat-card { + @include mat-card-layout; +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.ts b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.ts new file mode 100644 index 0000000000..f1c4dbc58b --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain-list/villain-list.component.ts @@ -0,0 +1,52 @@ +import { + ChangeDetectionStrategy, + Component, + EventEmitter, + Input, + Output, +} from '@angular/core'; +import { MatDialog, MatDialogConfig } from '@angular/material/dialog'; +import { ModalComponent, Villain } from '../../core'; + +@Component({ + selector: 'ngrx-villain-list', + templateUrl: './villain-list.component.html', + styleUrls: ['./villain-list.component.scss'], + changeDetection: ChangeDetectionStrategy.OnPush, +}) +export class VillainListComponent { + @Input() villains: Villain[]; + @Input() selectedVillain: Villain; + @Output() deleted = new EventEmitter(); + @Output() selected = new EventEmitter(); + + constructor(public dialog: MatDialog) {} + + byId(index: number, villain: Villain) { + return villain.id; + } + + select(villain: Villain) { + this.selected.emit(villain); + } + + deleteVillain(villain: Villain) { + const dialogConfig = new MatDialogConfig(); + dialogConfig.disableClose = true; + dialogConfig.autoFocus = true; + dialogConfig.width = '250px'; + dialogConfig.data = { + title: 'Delete Villain', + message: `Do you want to delete ${villain.name}`, + }; + + const dialogRef = this.dialog.open(ModalComponent, dialogConfig); + + dialogRef.afterClosed().subscribe((deleteIt) => { + console.log('The dialog was closed'); + if (deleteIt) { + this.deleted.emit(villain); + } + }); + } +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villain.service.ts b/projects/tour-of-heroes-data/src/app/villains/villain.service.ts new file mode 100644 index 0000000000..57cbcfe44b --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villain.service.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@angular/core'; +import { + EntityCollectionServiceBase, + EntityCollectionServiceElementsFactory, +} from '@ngrx/data'; +import { Villain } from '../core'; + +@Injectable({ providedIn: 'root' }) +export class VillainService extends EntityCollectionServiceBase { + constructor(factory: EntityCollectionServiceElementsFactory) { + super('Villain', factory); + } +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villains.module.ts b/projects/tour-of-heroes-data/src/app/villains/villains.module.ts new file mode 100644 index 0000000000..325fe50469 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villains.module.ts @@ -0,0 +1,28 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; +import { RouterModule, Routes } from '@angular/router'; +import { MaterialModule } from '../material/material.module'; +import { SharedModule } from '../shared/shared.module'; +import { VillainDetailComponent } from './villain-detail/villain-detail.component'; +import { VillainListComponent } from './villain-list/villain-list.component'; +import { VillainsComponent } from './villains/villains.component'; + +const routes: Routes = [ + { path: '', pathMatch: 'full', component: VillainsComponent }, +]; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + MaterialModule, + RouterModule.forChild(routes), + ], + exports: [VillainsComponent, VillainDetailComponent], + declarations: [ + VillainsComponent, + VillainDetailComponent, + VillainListComponent, + ], +}) +export class VillainsModule {} diff --git a/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.html b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.html new file mode 100644 index 0000000000..f0c6dd0a66 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.html @@ -0,0 +1,52 @@ +
+
+ + +
+
+
+
+
+ + + + +
+
+
+ + +
+
diff --git a/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.scss b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.scss new file mode 100644 index 0000000000..930678d9ea --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.scss @@ -0,0 +1,13 @@ +@import 'mixin'; + +.control-panel { + @include control-panel-layout; +} + +.content-container { + @include content-container-layout; +} + +.button-panel > button { + margin-right: 12px; +} diff --git a/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.ts b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.ts new file mode 100644 index 0000000000..aa0ff914f7 --- /dev/null +++ b/projects/tour-of-heroes-data/src/app/villains/villains/villains.component.ts @@ -0,0 +1,54 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Villain } from '../../core'; +import { VillainService } from '../villain.service'; + +@Component({ + selector: 'ngrx-villains', + templateUrl: './villains.component.html', + styleUrls: ['./villains.component.scss'], +}) +export class VillainsComponent implements OnInit { + loading$: Observable; + selected: Villain; + villains$: Observable; + + constructor(private villainService: VillainService) { + this.villains$ = villainService.entities$; + this.loading$ = villainService.loading$; + } + + ngOnInit() { + this.getVillains(); + } + + add(villain: Villain) { + this.villainService.add(villain); + } + + close() { + this.selected = null; + } + + delete(villain: Villain) { + this.villainService.delete(villain.id); + this.close(); + } + + enableAddMode() { + this.selected = {}; + } + + getVillains() { + this.villainService.getAll(); + this.close(); + } + + select(villain: Villain) { + this.selected = villain; + } + + update(villain: Villain) { + this.villainService.update(villain); + } +} diff --git a/projects/data-example-app/src/assets/.gitkeep b/projects/tour-of-heroes-data/src/assets/.gitkeep similarity index 100% rename from projects/data-example-app/src/assets/.gitkeep rename to projects/tour-of-heroes-data/src/assets/.gitkeep diff --git a/projects/tour-of-heroes-data/src/assets/jp-48.jpg b/projects/tour-of-heroes-data/src/assets/jp-48.jpg new file mode 100644 index 0000000000..6fd15cd915 Binary files /dev/null and b/projects/tour-of-heroes-data/src/assets/jp-48.jpg differ diff --git a/projects/tour-of-heroes-data/src/assets/jp.jpg b/projects/tour-of-heroes-data/src/assets/jp.jpg new file mode 100644 index 0000000000..c90f73e2d7 Binary files /dev/null and b/projects/tour-of-heroes-data/src/assets/jp.jpg differ diff --git a/projects/tour-of-heroes-data/src/assets/ng.png b/projects/tour-of-heroes-data/src/assets/ng.png new file mode 100644 index 0000000000..8081c7ceaf Binary files /dev/null and b/projects/tour-of-heroes-data/src/assets/ng.png differ diff --git a/projects/tour-of-heroes-data/src/assets/styles/mixin.scss b/projects/tour-of-heroes-data/src/assets/styles/mixin.scss new file mode 100644 index 0000000000..55546a3680 --- /dev/null +++ b/projects/tour-of-heroes-data/src/assets/styles/mixin.scss @@ -0,0 +1,186 @@ +@import 'theme'; + +@mixin primary-color { + $primary-color: map-get($primary, 800); + color: $primary-color; +} + +@mixin primary-background-contrast-color { + $primary-color: map-get($primary, 400); + $font-color: map-get(map-get($primary, contrast), 400); + background-color: $primary-color; + color: $font-color !important; +} + +@mixin accent-color { + $accent-font-color: map-get($accent, 800); + color: $accent-font-color; +} + +@mixin accent-background-color { + $accent-color: map-get($accent, 700); + $accent-font-color: map-get(map-get($accent, contrast), 700); + background-color: $accent-color; + color: $accent-font-color !important; +} + +@mixin selected-style { + $background-color: map-get($accent, 800); + $font-color: map-get(map-get($accent, contrast), 800); + background: $background-color !important; + color: $font-color !important; +} + +@mixin hover-style { + $background-color: map-get($accent, 400); + $font-color: map-get(map-get($accent, contrast), 900); + background: $background-color !important; + color: $font-color !important; +} + +@mixin selected-hover-style { + $background-color: map-get($accent, 600); + $font-color: map-get(map-get($accent, contrast), 600); + background: $background-color !important; + color: $font-color !important; +} + +@mixin item-list { + margin: 0 0 2em 0; + list-style-type: none; + padding: 0; + width: 25em; + .selected, + .selected > .item-text * { + background-color: rgb(0, 120, 215) !important; + color: white; + } + li { + cursor: pointer; + position: relative; + margin: 0.5em; + height: 4em; + } + .saying { + overflow: hidden; + text-overflow: ellipsis; + width: 15em; + white-space: nowrap; + margin: 5px 0px; + } + .name { + @include primary-color; + } + .item-container { + display: flex; + flex-flow: row wrap; + } + > * { + flex: 1 100%; + } + .selectable-item { + display: flex; + flex-flow: row wrap; + flex: 18 auto; + order: 0; + padding: 0; + margin: 0; + + &:hover { + color: #607d8b; + color: rgb(0, 120, 215); + background-color: #ddd; + left: 1px; + } + + &.selected:hover { + color: white; + } + } + .item-text { + flex: 1 auto; + order: 2; + padding: 10px; + } + .badge { + flex: 1 auto; + order: 1; + font-size: small; + color: #ffffff; + padding: 1.5em 1em 0em 1em; + background-color: rgb(134, 183, 221); + margin: 0em 0em 0em 0em; + max-width: 1.5em; + text-align: center; + } + button.delete-button { + margin-right: 12px; + margin-top: 6px; + order: 1; + } +} + +@mixin mat-card-layout { + width: 366px; + margin: 1em; + padding: 0px; + .mat-card-header { + background-color: #85b7de; + padding: 18px 12px 6px 24px; + color: #ffffff; + } + .mat-card-content { + padding: 24px; + } +} + +@mixin editarea-margins { + button { + margin: 0.5em 1em; + } + .editfields { + margin: 1em; + + .mat-form-field { + width: 100%; + } + } +} + +@mixin control-panel-layout { + margin: 8px; + margin-left: 1em; + display: flex; + flex-flow: column wrap; + > * { + flex: 1 100%; + } + .button-panel { + flex: 1 auto; + margin: 8px 0px; + order: 1; + } + .filter-panel { + flex: 1 auto; + margin: 8px 0px; + order: 2; + } +} + +@mixin content-container-layout { + display: flex; + flex-flow: row wrap; + > * { + flex: 1 100%; + } + .list-container { + flex: 1 auto; + order: 1; + max-width: 30em; + } + .detail-container { + flex: 1 auto; + order: 2; + max-width: 22em; + } +} diff --git a/projects/tour-of-heroes-data/src/assets/styles/styles.scss b/projects/tour-of-heroes-data/src/assets/styles/styles.scss new file mode 100644 index 0000000000..d718e581d3 --- /dev/null +++ b/projects/tour-of-heroes-data/src/assets/styles/styles.scss @@ -0,0 +1,16 @@ +@import 'mixin'; + +html, +body { + font-family: 'Roboto', sans-serif; + margin: 0px; + background-color: #ffffff; +} + +.router-link-active { + @include primary-background-contrast-color; +} + +.page-content { + margin: 1em; +} diff --git a/projects/tour-of-heroes-data/src/assets/styles/theme.scss b/projects/tour-of-heroes-data/src/assets/styles/theme.scss new file mode 100644 index 0000000000..7d75db254a --- /dev/null +++ b/projects/tour-of-heroes-data/src/assets/styles/theme.scss @@ -0,0 +1,10 @@ +// @import '~@angular/material/prebuilt-themes/indigo-pink.css'; +@import '~@angular/material/theming'; + +@include mat-core(); + +$primary: mat-palette($mat-blue, 800); +$accent: mat-palette($mat-pink, A200, A100, A400); +$warn: mat-palette($mat-deep-orange); +$light-theme: mat-light-theme($primary, $accent, $warn); +@include angular-material-theme($light-theme); diff --git a/projects/tour-of-heroes-data/src/assets/wb-48.jpg b/projects/tour-of-heroes-data/src/assets/wb-48.jpg new file mode 100644 index 0000000000..17821e101d Binary files /dev/null and b/projects/tour-of-heroes-data/src/assets/wb-48.jpg differ diff --git a/projects/tour-of-heroes-data/src/assets/wb.jpg b/projects/tour-of-heroes-data/src/assets/wb.jpg new file mode 100644 index 0000000000..674d41b769 Binary files /dev/null and b/projects/tour-of-heroes-data/src/assets/wb.jpg differ diff --git a/projects/data-example-app/src/environments/environment.prod.ts b/projects/tour-of-heroes-data/src/environments/environment.prod.ts similarity index 100% rename from projects/data-example-app/src/environments/environment.prod.ts rename to projects/tour-of-heroes-data/src/environments/environment.prod.ts diff --git a/projects/data-example-app/src/environments/environment.ts b/projects/tour-of-heroes-data/src/environments/environment.ts similarity index 88% rename from projects/data-example-app/src/environments/environment.ts rename to projects/tour-of-heroes-data/src/environments/environment.ts index 31cb7855f1..66998ae9a7 100644 --- a/projects/data-example-app/src/environments/environment.ts +++ b/projects/tour-of-heroes-data/src/environments/environment.ts @@ -1,5 +1,5 @@ // This file can be replaced during build by using the `fileReplacements` array. -// `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. +// `ng build` replaces `environment.ts` with `environment.prod.ts`. // The list of file replacements can be found in `angular.json`. export const environment = { diff --git a/projects/data-example-app/src/favicon.ico b/projects/tour-of-heroes-data/src/favicon.ico similarity index 100% rename from projects/data-example-app/src/favicon.ico rename to projects/tour-of-heroes-data/src/favicon.ico diff --git a/projects/tour-of-heroes-data/src/index.html b/projects/tour-of-heroes-data/src/index.html new file mode 100644 index 0000000000..5849adec30 --- /dev/null +++ b/projects/tour-of-heroes-data/src/index.html @@ -0,0 +1,22 @@ + + + + + TourOfHeroesData + + + + + + + + + + + diff --git a/projects/data-example-app/src/main.ts b/projects/tour-of-heroes-data/src/main.ts similarity index 100% rename from projects/data-example-app/src/main.ts rename to projects/tour-of-heroes-data/src/main.ts diff --git a/projects/data-example-app/src/polyfills.ts b/projects/tour-of-heroes-data/src/polyfills.ts similarity index 97% rename from projects/data-example-app/src/polyfills.ts rename to projects/tour-of-heroes-data/src/polyfills.ts index 4e2d31486f..8a120c374d 100644 --- a/projects/data-example-app/src/polyfills.ts +++ b/projects/tour-of-heroes-data/src/polyfills.ts @@ -18,7 +18,9 @@ * BROWSER POLYFILLS */ -/** IE11 requires the following for NgClass support on SVG elements */ +/** + * IE11 requires the following for NgClass support on SVG elements + */ // import 'classlist.js'; // Run `npm install --save classlist.js`. /** diff --git a/projects/tour-of-heroes-data/src/server/index.js b/projects/tour-of-heroes-data/src/server/index.js new file mode 100644 index 0000000000..40555d9be7 --- /dev/null +++ b/projects/tour-of-heroes-data/src/server/index.js @@ -0,0 +1,19 @@ +const express = require('express'); +const bodyParser = require('body-parser'); +const routes = require('./routes'); + +const port = process.env.PORT || 3001; +const publicweb = process.env.PUBLICWEB || './dist'; + +const app = express(); +app.use(bodyParser.json()); +app.use(bodyParser.urlencoded({ extended: false })); + +app.use(express.static(publicweb)); +app.use('/some-api', routes); +console.log(`serving ${publicweb}`); + +app.get('*', (req, res) => { + res.sendFile(`index.html`, { root: publicweb }); +}); +app.listen(port, () => console.log(`listening on http://localhost:${port}`)); diff --git a/projects/tour-of-heroes-data/src/server/routes.js b/projects/tour-of-heroes-data/src/server/routes.js new file mode 100644 index 0000000000..4f8052eee3 --- /dev/null +++ b/projects/tour-of-heroes-data/src/server/routes.js @@ -0,0 +1,24 @@ +const express = require('express'); +const router = express.Router(); + +router.get('/heroes', (req, res) => { + res.send([ + { + id: 221, + name: 'Big Hero 6', + saying: 'fa la la la la (from node server)', + }, + ]); +}); + +router.get('/badguys', (req, res) => { + res.send([ + { + id: 41, + name: 'Bad Guy', + saying: 'bwahahahaha (from node server)', + }, + ]); +}); + +module.exports = router; diff --git a/projects/tour-of-heroes-data/src/styles.scss b/projects/tour-of-heroes-data/src/styles.scss new file mode 100644 index 0000000000..198654ac2c --- /dev/null +++ b/projects/tour-of-heroes-data/src/styles.scss @@ -0,0 +1,11 @@ +/* You can add global styles to this file, and also import other style files */ + +html, +body { + height: 100%; +} + +body { + margin: 0; + font-family: Roboto, 'Helvetica Neue', sans-serif; +} diff --git a/projects/tour-of-heroes-data/src/test-setup.ts b/projects/tour-of-heroes-data/src/test-setup.ts new file mode 100644 index 0000000000..1100b3e8a6 --- /dev/null +++ b/projects/tour-of-heroes-data/src/test-setup.ts @@ -0,0 +1 @@ +import 'jest-preset-angular/setup-jest'; diff --git a/projects/data-example-app/tsconfig.app.json b/projects/tour-of-heroes-data/tsconfig.app.json similarity index 80% rename from projects/data-example-app/tsconfig.app.json rename to projects/tour-of-heroes-data/tsconfig.app.json index 1250aa8867..08b76dffc1 100644 --- a/projects/data-example-app/tsconfig.app.json +++ b/projects/tour-of-heroes-data/tsconfig.app.json @@ -3,7 +3,8 @@ "compilerOptions": { "outDir": "../../dist/out-tsc", "module": "commonjs", - "types": ["jest", "node"] + "types": ["jest", "node"], + "strict": false }, "files": ["src/main.ts", "src/polyfills.ts"], "include": ["src/**/*.d.ts"] diff --git a/projects/data-example-app/tsconfig.spec.json b/projects/tour-of-heroes-data/tsconfig.spec.json similarity index 100% rename from projects/data-example-app/tsconfig.spec.json rename to projects/tour-of-heroes-data/tsconfig.spec.json diff --git a/yarn.lock b/yarn.lock index 32b70aeb29..17e672970b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -244,6 +244,11 @@ dependencies: tslib "^2.1.0" +"@angular/language-service@^12.0.0": + version "12.2.3" + resolved "https://registry.yarnpkg.com/@angular/language-service/-/language-service-12.2.3.tgz#920b12be860f48764d0dcb968243ec5fc1f5f7c0" + integrity sha512-aQcbz8ISL1L6Fludzchj9qZpYkt3RlvdHNz/Dvv6cGukxA+6vMYOK4jVuGsPVDe3gqRxyj9WucoF3WTgmc2Nqw== + "@angular/material@^12.0.4": version "12.0.4" resolved "https://registry.yarnpkg.com/@angular/material/-/material-12.0.4.tgz#0cd88328f846d7ba6485abc7f1888aef174d7e4c" @@ -3661,6 +3666,11 @@ alphanum-sort@^1.0.2: resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" integrity sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM= +angular-in-memory-web-api@^0.11.0: + version "0.11.0" + resolved "https://registry.yarnpkg.com/angular-in-memory-web-api/-/angular-in-memory-web-api-0.11.0.tgz#46e4ad896b36d669f36801fc8cafa7db8278d078" + integrity sha512-QV1qYHm+Zd+wrvlcPLnAcqqGpOmCN1EUj4rRuYHpek8+QqFFdxBNuPZOJCKvU7I97z5QSKHsdc6PNKlpUQr3UA== + ansi-colors@4.1.1, ansi-colors@^4.1.1: version "4.1.1" resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-4.1.1.tgz#cbb9ae256bf750af1eab344f229aa27fe94ba348"