├── src ├── assets │ ├── .gitkeep │ └── images │ │ └── skiwelt.jpg ├── app │ ├── containers │ │ ├── dashboard │ │ │ ├── dashboard.component.scss │ │ │ ├── dashboard.component.html │ │ │ ├── dashboard.component.ts │ │ │ └── dashboard.component.spec.ts │ │ ├── page-not-found │ │ │ ├── page-not-found.component.scss │ │ │ ├── page-not-found.component.html │ │ │ ├── page-not-found.component.ts │ │ │ └── page-not-found.component.spec.ts │ │ └── search-dialog │ │ │ ├── search-dialog.component.scss │ │ │ ├── search-dialog.component.html │ │ │ ├── search-dialog.component.ts │ │ │ └── search-dialog.component.spec.ts │ ├── app.component.html │ ├── app.component.scss │ ├── shared │ │ ├── components │ │ │ ├── resorts-map │ │ │ │ ├── resorts-map.component.scss │ │ │ │ ├── resorts-map.component.html │ │ │ │ ├── resorts-map.component.spec.ts │ │ │ │ └── resorts-map.component.ts │ │ │ └── resort-autocomplete │ │ │ │ ├── resort-autocomplete.component.scss │ │ │ │ ├── resort-autocomplete.component.html │ │ │ │ ├── resort-autocomplete.component.ts │ │ │ │ └── resort-autocomplete.component.spec.ts │ │ ├── shared.module.spec.ts │ │ └── shared.module.ts │ ├── core │ │ ├── services │ │ │ ├── base.service.ts │ │ │ ├── route.service.ts │ │ │ ├── base.service.spec.ts │ │ │ ├── resort.service.ts │ │ │ ├── resort.service.spec.ts │ │ │ └── route.service.spec.ts │ │ ├── core.module.spec.ts │ │ ├── shell │ │ │ ├── shell.component.scss │ │ │ ├── shell.component.html │ │ │ ├── shell.component.ts │ │ │ └── shell.component.spec.ts │ │ └── core.module.ts │ ├── +favorites │ │ ├── components │ │ │ └── favorites-table │ │ │ │ ├── favorites-table.component.scss │ │ │ │ ├── favorites-table.component.spec.ts │ │ │ │ ├── favorites-table.component.html │ │ │ │ └── favorites-table.component.ts │ │ ├── favorites.module.spec.ts │ │ ├── favorites-routing.module.ts │ │ ├── containers │ │ │ └── favorites │ │ │ │ ├── favorites.component.scss │ │ │ │ ├── favorites.component.html │ │ │ │ ├── favorites.component.ts │ │ │ │ └── favorites.component.spec.ts │ │ └── favorites.module.ts │ ├── state │ │ ├── map │ │ │ ├── map.actions.ts │ │ │ ├── map.reducer.ts │ │ │ └── map.reducer.spec.ts │ │ ├── sidenav │ │ │ ├── sidenav.actions.ts │ │ │ ├── sidenav.reducer.ts │ │ │ └── sidenav.reducer.spec.ts │ │ ├── dialog │ │ │ ├── dialog.reducer.ts │ │ │ ├── dialog.actions.ts │ │ │ ├── dialog.reducer.spec.ts │ │ │ ├── dialog.effects.ts │ │ │ └── dialog.effects.spec.ts │ │ ├── state.module.ts │ │ ├── resort │ │ │ ├── resort.actions.ts │ │ │ ├── resort.effects.ts │ │ │ ├── resort.reducer.ts │ │ │ ├── resort.reducer.spec.ts │ │ │ └── resort.effects.spec.ts │ │ └── index.ts │ ├── models │ │ └── resort.model.ts │ ├── app.component.ts │ ├── app.component.spec.ts │ ├── material.module.ts │ ├── app.routing.module.ts │ └── app.module.ts ├── styles │ ├── _reset.scss │ └── _theming.scss ├── favicon.ico ├── styles.scss ├── tsconfig.app.json ├── tsconfig.spec.json ├── tslint.json ├── browserslist ├── main.ts ├── index.html ├── environments │ ├── environment.prod.ts │ └── environment.ts └── polyfills.ts ├── .prettierrc.yml ├── LICENSE ├── projects ├── simple-store │ ├── src │ │ ├── assets │ │ │ └── .gitkeep │ │ ├── app │ │ │ ├── app.component.scss │ │ │ ├── store │ │ │ │ ├── models.ts │ │ │ │ ├── index.ts │ │ │ │ ├── effects.ts │ │ │ │ ├── actions.ts │ │ │ │ ├── reducers.ts │ │ │ │ └── store.ts │ │ │ ├── services │ │ │ │ └── resort.service.ts │ │ │ ├── app.module.ts │ │ │ ├── app.component.html │ │ │ ├── app.component.spec.ts │ │ │ └── app.component.ts │ │ ├── environments │ │ │ ├── environment.prod.ts │ │ │ └── environment.ts │ │ ├── favicon.ico │ │ ├── styles.scss │ │ ├── index.html │ │ ├── main.ts │ │ ├── test.ts │ │ └── polyfills.ts │ ├── tsconfig.app.json │ ├── tsconfig.spec.json │ ├── tslint.json │ ├── browserslist │ └── karma.conf.js └── simple-store-e2e │ ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts │ ├── tsconfig.e2e.json │ └── protractor.conf.js ├── data └── routes.json ├── setup-jest.ts ├── .firebaserc ├── proxy.conf.json ├── e2e ├── src │ ├── app.po.ts │ └── app.e2e-spec.ts ├── tsconfig.e2e.json └── protractor.conf.js ├── firebase.json ├── .editorconfig ├── ngsw-config.json ├── tsconfig.json ├── .gitignore ├── README.md ├── jest-global-mocks.ts ├── .vscode └── launch.json ├── tslint.json ├── package.json └── angular.json /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.prettierrc.yml: -------------------------------------------------------------------------------- 1 | singleQuote: true -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2019 LiveLoveApp, LLC -------------------------------------------------------------------------------- /projects/simple-store/src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /data/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api/*": "/$1" 3 | } 4 | -------------------------------------------------------------------------------- /src/app/containers/dashboard/dashboard.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/page-not-found/page-not-found.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/search-dialog/search-dialog.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | display: flex; 3 | flex: 1; 4 | } -------------------------------------------------------------------------------- /src/styles/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body { 2 | margin: 0; 3 | padding: 0; 4 | } -------------------------------------------------------------------------------- /setup-jest.ts: -------------------------------------------------------------------------------- 1 | import 'jest-preset-angular'; 2 | import './jest-global-mocks'; 3 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-course/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "ngrx-course-210605" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | .sidenav { 2 | background-color: #f5f5f5; 3 | } -------------------------------------------------------------------------------- /src/app/containers/page-not-found/page-not-found.component.html: -------------------------------------------------------------------------------- 1 |

2 | page-not-found works! 3 |

-------------------------------------------------------------------------------- /src/assets/images/skiwelt.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-course/HEAD/src/assets/images/skiwelt.jpg -------------------------------------------------------------------------------- /proxy.conf.json: -------------------------------------------------------------------------------- 1 | { 2 | "/api": { 3 | "target": "http://localhost:3000", 4 | "secure": false 5 | } 6 | } 7 | -------------------------------------------------------------------------------- /projects/simple-store/src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /projects/simple-store/src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blove/ngrx-course/HEAD/projects/simple-store/src/favicon.ico -------------------------------------------------------------------------------- /src/app/shared/components/resorts-map/resorts-map.component.scss: -------------------------------------------------------------------------------- 1 | ngui-map { 2 | height: calc(100vh - 64px); 3 | width: 100vw; 4 | } -------------------------------------------------------------------------------- /src/app/shared/components/resort-autocomplete/resort-autocomplete.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | mat-form-field { 3 | width: 100%; 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import './styles/theming'; 3 | @import './styles/reset'; -------------------------------------------------------------------------------- /projects/simple-store/src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/models.ts: -------------------------------------------------------------------------------- 1 | export interface Resort { 2 | id: string; 3 | name: string; 4 | lat: string; 5 | lng: string; 6 | status: string; 7 | url: string; 8 | } 9 | -------------------------------------------------------------------------------- /src/app/containers/dashboard/dashboard.component.html: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/containers/search-dialog/search-dialog.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/app/core/services/base.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | 3 | @Injectable() 4 | export class BaseService { 5 | readonly BASE_URL = 'http://localhost:3000/api'; 6 | } 7 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/+favorites/components/favorites-table/favorites-table.component.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | mat-row:hover { 4 | background-color: mat-color($mat-grey, 200); 5 | cursor: pointer; 6 | } 7 | 8 | .mat-column-actions { 9 | flex: 0 0 40px; 10 | } -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist/course", 4 | "ignore": ["firebase.json", "**/.*", "**/node_modules/**"], 5 | "rewrites": [ 6 | { 7 | "source": "**", 8 | "destination": "/index.html" 9 | } 10 | ] 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /projects/simple-store-e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getParagraphText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /projects/simple-store/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "module": "es2015", 6 | "types": [] 7 | }, 8 | "exclude": [ 9 | "src/test.ts", 10 | "**/*.spec.ts" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /projects/simple-store-e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /src/app/state/map/map.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum MapActionTypes { 4 | SetZoom = '[Map] Set Zoom' 5 | } 6 | 7 | export class SetMapZoom implements Action { 8 | readonly type = MapActionTypes.SetZoom; 9 | 10 | constructor(public zoom: number) {} 11 | } 12 | 13 | export type MapAction = SetMapZoom; 14 | -------------------------------------------------------------------------------- /src/app/core/core.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { CoreModule } from '@app/core/core.module'; 2 | 3 | describe('CoreModule', () => { 4 | let coreModule: CoreModule; 5 | 6 | beforeEach(() => { 7 | coreModule = new CoreModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(coreModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { SharedModule } from './shared.module'; 2 | 3 | describe('SharedModule', () => { 4 | let sharedModule: SharedModule; 5 | 6 | beforeEach(() => { 7 | sharedModule = new SharedModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(sharedModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to app!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/index.ts: -------------------------------------------------------------------------------- 1 | import { ResortEffects } from './effects'; 2 | import { resortReducer, sidenavReducer } from './reducers'; 3 | import { ReducerMap, Store } from './store'; 4 | 5 | const reducers: ReducerMap = { 6 | resort: resortReducer, 7 | sidenav: sidenavReducer 8 | }; 9 | 10 | export const store = new Store(reducers, [ResortEffects]); 11 | -------------------------------------------------------------------------------- /src/styles/_theming.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | @include mat-core(); 4 | $app-primary: mat-palette($mat-blue-grey, 800, 700, 800); 5 | $app-accent: mat-palette($mat-blue-grey, 400, 600, 800); 6 | $app-warn: mat-palette($mat-deep-orange); 7 | 8 | $app-theme: mat-light-theme($app-primary, $app-accent, $app-warn); 9 | @include angular-material-theme($app-theme); -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jest", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "test.ts", 13 | "polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /projects/simple-store/src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | SimpleStore 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/app/+favorites/favorites.module.spec.ts: -------------------------------------------------------------------------------- 1 | import { FavoritesModule } from './favorites.module'; 2 | 3 | describe('FavoritesModule', () => { 4 | let favoritesModule: FavoritesModule; 5 | 6 | beforeEach(() => { 7 | favoritesModule = new FavoritesModule(); 8 | }); 9 | 10 | it('should create an instance', () => { 11 | expect(FavoritesModule).toBeTruthy(); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /src/app/containers/page-not-found/page-not-found.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-page-not-found', 5 | templateUrl: './page-not-found.component.html', 6 | styleUrls: ['./page-not-found.component.scss'] 7 | }) 8 | export class PageNotFoundComponent implements OnInit { 9 | constructor() {} 10 | 11 | ngOnInit() {} 12 | } 13 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/core/services/route.service.ts: -------------------------------------------------------------------------------- 1 | import { Route, Routes } from '@angular/router'; 2 | import { ShellComponent } from '@app/core/shell/shell.component'; 3 | 4 | export class RouteService { 5 | static withShell(routes: Routes): Route { 6 | return { 7 | path: '', 8 | component: ShellComponent, 9 | children: routes, 10 | data: { reuse: true } 11 | }; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /projects/simple-store-e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getParagraphText()).toEqual('Welcome to simple-store!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /src/app/models/resort.model.ts: -------------------------------------------------------------------------------- 1 | export interface Resort { 2 | id: string; 3 | name: string; 4 | url: string; 5 | lat: string; 6 | lng: string; 7 | status: string; 8 | } 9 | 10 | export const generateResort = () => ({ 11 | id: '123', 12 | name: 'Big BIG Mountain', 13 | url: 'https://thebigbigmountain.com', 14 | lat: '39.1178138', 15 | lng: '-106.4627402', 16 | status: 'pending' 17 | }); 18 | -------------------------------------------------------------------------------- /projects/simple-store/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../../out-tsc/spec", 5 | "module": "commonjs", 6 | "types": [ 7 | "jasmine", 8 | "node" 9 | ] 10 | }, 11 | "files": [ 12 | "src/test.ts", 13 | "src/polyfills.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /src/app/shared/components/resorts-map/resorts-map.component.html: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | {{title}} 6 | 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | import { AppModule } from '@app/app.module'; 4 | import { environment } from '@env/environment'; 5 | 6 | if (environment.production) { 7 | enableProdMode(); 8 | } 9 | 10 | platformBrowserDynamic() 11 | .bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /projects/simple-store/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /projects/simple-store/browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # For IE 9-11 support, please uncomment the last line of the file and adjust as needed 5 | > 0.5% 6 | last 2 versions 7 | Firefox ESR 8 | not dead 9 | # IE 9-11 -------------------------------------------------------------------------------- /projects/simple-store/src/app/services/resort.service.ts: -------------------------------------------------------------------------------- 1 | import { from, Observable } from 'rxjs'; 2 | import { Resort } from '../store/models'; 3 | 4 | export class ResortService { 5 | static BASE_URL = 'http://localhost:3000/api'; 6 | 7 | static loadAll(): Observable> { 8 | return from( 9 | fetch(`${ResortService.BASE_URL}/resorts`).then(response => 10 | response.json() 11 | ) 12 | ); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /projects/simple-store/src/main.ts: -------------------------------------------------------------------------------- 1 | import { enableProdMode } from '@angular/core'; 2 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 3 | 4 | import { AppModule } from './app/app.module'; 5 | import { environment } from './environments/environment'; 6 | 7 | if (environment.production) { 8 | enableProdMode(); 9 | } 10 | 11 | platformBrowserDynamic().bootstrapModule(AppModule) 12 | .catch(err => console.log(err)); 13 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Title } from '@angular/platform-browser'; 3 | 4 | @Component({ 5 | selector: 'app-root', 6 | templateUrl: './app.component.html', 7 | styleUrls: ['./app.component.scss'] 8 | }) 9 | export class AppComponent implements OnInit { 10 | constructor(private title: Title) {} 11 | 12 | ngOnInit() { 13 | this.title.setTitle('Ski Resorts'); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FlexLayoutModule } from '@angular/flex-layout'; 3 | import { BrowserModule } from '@angular/platform-browser'; 4 | import { AppComponent } from './app.component'; 5 | 6 | @NgModule({ 7 | declarations: [AppComponent], 8 | imports: [BrowserModule, FlexLayoutModule], 9 | providers: [], 10 | bootstrap: [AppComponent] 11 | }) 12 | export class AppModule {} 13 | -------------------------------------------------------------------------------- /src/app/core/services/base.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { BaseService } from './base.service'; 3 | 4 | describe('BaseService', () => { 5 | beforeEach(() => { 6 | TestBed.configureTestingModule({ 7 | providers: [BaseService] 8 | }); 9 | }); 10 | 11 | it('should be created', inject([BaseService], (service: BaseService) => { 12 | expect(service).toBeTruthy(); 13 | })); 14 | }); 15 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Course 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /ngsw-config.json: -------------------------------------------------------------------------------- 1 | { 2 | "index": "/index.html", 3 | "assetGroups": [ 4 | { 5 | "name": "app", 6 | "installMode": "prefetch", 7 | "resources": { 8 | "files": ["/favicon.ico", "/index.html", "/*.css", "/*.js"] 9 | } 10 | }, 11 | { 12 | "name": "assets", 13 | "installMode": "lazy", 14 | "updateMode": "prefetch", 15 | "resources": { 16 | "files": ["/assets/**"] 17 | } 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /src/app/shared/components/resort-autocomplete/resort-autocomplete.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | {{ resort.name }} 7 | 8 | -------------------------------------------------------------------------------- /src/app/+favorites/favorites-routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { RouterModule, Routes } from '@angular/router'; 3 | import { FavoritesComponent } from './containers/favorites/favorites.component'; 4 | 5 | const routes: Routes = [ 6 | { 7 | path: '', 8 | component: FavoritesComponent 9 | } 10 | ]; 11 | 12 | @NgModule({ 13 | imports: [RouterModule.forChild(routes)], 14 | exports: [RouterModule] 15 | }) 16 | export class FavoritesRoutingModule {} 17 | -------------------------------------------------------------------------------- /src/app/state/sidenav/sidenav.actions.ts: -------------------------------------------------------------------------------- 1 | import { Action } from '@ngrx/store'; 2 | 3 | export enum SidenavActionTypes { 4 | HideSidenav = '[Sidenav] Hide Sidenav', 5 | ShowSidenav = '[Sidenav] Show Sidenav' 6 | } 7 | 8 | export class HideSidenav implements Action { 9 | readonly type = SidenavActionTypes.HideSidenav; 10 | } 11 | 12 | export class ShowSidenav implements Action { 13 | readonly type = SidenavActionTypes.ShowSidenav; 14 | } 15 | 16 | export type SidenavAction = HideSidenav | ShowSidenav; 17 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | google: { 3 | maps: { 4 | apiKey: 'API_KEY' 5 | }, 6 | firebase: { 7 | apiKey: 'AIzaSyAkGfqfWiHAqNKOhRTll-Rhw1Qw7SZN1ro', 8 | authDomain: 'ngrx-course-210605.firebaseapp.com', 9 | databaseURL: 'https://ngrx-course-210605.firebaseio.com', 10 | projectId: 'ngrx-course-210605', 11 | storageBucket: 'ngrx-course-210605.appspot.com', 12 | messagingSenderId: '679702729839' 13 | } 14 | }, 15 | production: true 16 | }; 17 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/app.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |

Sidenav

4 |
5 |
6 | 7 | 8 | 9 | 14 |
15 |
-------------------------------------------------------------------------------- /src/app/state/map/map.reducer.ts: -------------------------------------------------------------------------------- 1 | import { MapAction, MapActionTypes } from './map.actions'; 2 | 3 | export interface State { 4 | zoom: number; 5 | } 6 | 7 | export const initialState: State = { 8 | zoom: 10 9 | }; 10 | 11 | export function reducer(state = initialState, action: MapAction): State { 12 | switch (action.type) { 13 | case MapActionTypes.SetZoom: 14 | return { 15 | ...state, 16 | zoom: action.zoom 17 | }; 18 | default: 19 | return state; 20 | } 21 | } 22 | 23 | export const getZoom = (state: State) => state.zoom; 24 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/effects.ts: -------------------------------------------------------------------------------- 1 | import { of } from 'rxjs'; 2 | import { map } from 'rxjs/operators'; 3 | import { ResortService } from '../services/resort.service'; 4 | import { LoadResortsSuccess, ResortActions } from './actions'; 5 | import { Action } from './store'; 6 | 7 | export class ResortEffects { 8 | loadAll = (action: Action) => { 9 | if (action.type !== ResortActions.LoadResorts) { 10 | return of(null); 11 | } 12 | return ResortService.loadAll().pipe( 13 | map(resorts => new LoadResortsSuccess(resorts)) 14 | ); 15 | }; 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "outDir": "./dist/out-tsc", 6 | "sourceMap": true, 7 | "declaration": false, 8 | "moduleResolution": "node", 9 | "emitDecoratorMetadata": true, 10 | "experimentalDecorators": true, 11 | "target": "es5", 12 | "typeRoots": [ 13 | "node_modules/@types" 14 | ], 15 | "lib": [ 16 | "es2017", 17 | "dom" 18 | ], 19 | "paths": { 20 | "@app/*": ["src/app/*"], 21 | "@env/*": ["src/environments/*"] 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/app/containers/search-dialog/search-dialog.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | import { Resort } from '@app/models/resort.model'; 3 | import { State } from '@app/state'; 4 | import { SelectResort } from '@app/state/resort/resort.actions'; 5 | import { Store } from '@ngrx/store'; 6 | 7 | @Component({ 8 | templateUrl: './search-dialog.component.html', 9 | styleUrls: ['./search-dialog.component.scss'] 10 | }) 11 | export class SearchDialogComponent { 12 | constructor(private store: Store) {} 13 | 14 | onResortSelected(resort: Resort) { 15 | this.store.dispatch(new SelectResort(resort)); 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/app/+favorites/containers/favorites/favorites.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | background-color: #fff; 3 | 4 | .banner { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | height: 336px; 10 | margin-top: 64px; 11 | background: url('/assets/images/skiwelt.jpg') no-repeat; 12 | background-position: center center; 13 | background-size: cover; 14 | 15 | h1 { 16 | margin-top: 200px; 17 | color: #fff; 18 | text-shadow: 0px 0px 6px rgba(0, 0, 0, 0.8); 19 | font-size: 4rem; 20 | } 21 | } 22 | 23 | .favorites { 24 | mat-card { 25 | margin-top: 300px; 26 | } 27 | } 28 | } -------------------------------------------------------------------------------- /src/app/state/dialog/dialog.reducer.ts: -------------------------------------------------------------------------------- 1 | import { DialogAction, DialogActionTypes } from './dialog.actions'; 2 | 3 | export interface State { 4 | opened: boolean; 5 | } 6 | 7 | export const initialState: State = { 8 | opened: false 9 | }; 10 | 11 | export function reducer(state = initialState, action: DialogAction): State { 12 | switch (action.type) { 13 | case DialogActionTypes.CloseDialogs: 14 | return { 15 | ...state, 16 | opened: false 17 | }; 18 | case DialogActionTypes.OpenDialog: 19 | return { 20 | ...state, 21 | opened: true 22 | }; 23 | default: 24 | return state; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { FlexModule } from '@angular/flex-layout'; 3 | import { AppComponent } from './app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [AppComponent], 9 | imports: [FlexModule] 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', async(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /src/app/app.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, TestBed } from '@angular/core/testing'; 2 | import { RouterTestingModule } from '@angular/router/testing'; 3 | import { AppComponent } from '@app/app.component'; 4 | 5 | describe('AppComponent', () => { 6 | beforeEach(async(() => { 7 | TestBed.configureTestingModule({ 8 | declarations: [AppComponent], 9 | imports: [RouterTestingModule] 10 | }).compileComponents(); 11 | })); 12 | 13 | it('should create the app', async(() => { 14 | const fixture = TestBed.createComponent(AppComponent); 15 | const app = fixture.debugElement.componentInstance; 16 | expect(app).toBeTruthy(); 17 | })); 18 | }); 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | dist 5 | tmp 6 | out-tsc 7 | 8 | # dependencies 9 | /node_modules 10 | 11 | # IDEs and editors 12 | /.idea 13 | .project 14 | .classpath 15 | .c9/ 16 | *.launch 17 | .settings/ 18 | *.sublime-workspace 19 | 20 | # IDE - VSCode 21 | .vscode/* 22 | !.vscode/settings.json 23 | !.vscode/tasks.json 24 | !.vscode/launch.json 25 | !.vscode/extensions.json 26 | 27 | # misc 28 | /.sass-cache 29 | connect.lock 30 | coverage 31 | libpeerconnection.log 32 | npm-debug.log 33 | yarn-error.log 34 | testem.log 35 | typings 36 | 37 | # System Files 38 | .DS_Store 39 | Thumbs.db 40 | -------------------------------------------------------------------------------- /src/app/core/services/resort.service.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, HttpParams } from '@angular/common/http'; 2 | import { Injectable } from '@angular/core'; 3 | import { Resort } from '@app/models/resort.model'; 4 | import { Observable } from 'rxjs'; 5 | import { BaseService } from './base.service'; 6 | 7 | @Injectable({ 8 | providedIn: 'root' 9 | }) 10 | export class ResortService extends BaseService { 11 | constructor(private httpClient: HttpClient) { 12 | super(); 13 | } 14 | 15 | search(q: string): Observable { 16 | return this.httpClient.get(`${this.BASE_URL}/resorts`, { 17 | params: new HttpParams().set('name_like', `^${q}`) 18 | }); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /projects/simple-store/src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * In development mode, to ignore zone related error stack frames such as 11 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 12 | * import the following file, but please comment it out in production mode 13 | * because it will have performance impact when throw error 14 | */ 15 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 16 | -------------------------------------------------------------------------------- /src/app/state/dialog/dialog.actions.ts: -------------------------------------------------------------------------------- 1 | import { ComponentType } from '@angular/cdk/portal'; 2 | import { MatDialogConfig } from '@angular/material'; 3 | import { Action } from '@ngrx/store'; 4 | 5 | export enum DialogActionTypes { 6 | CloseDialogs = '[Dialog] Close Dialogs', 7 | OpenDialog = '[Dialog] Open Dialog' 8 | } 9 | 10 | export class CloseDialogs implements Action { 11 | readonly type = DialogActionTypes.CloseDialogs; 12 | } 13 | 14 | export class OpenDialog implements Action { 15 | readonly type = DialogActionTypes.OpenDialog; 16 | 17 | constructor( 18 | public componentType: ComponentType, 19 | public config?: MatDialogConfig 20 | ) {} 21 | } 22 | 23 | export type DialogAction = CloseDialogs | OpenDialog; 24 | -------------------------------------------------------------------------------- /src/app/state/sidenav/sidenav.reducer.ts: -------------------------------------------------------------------------------- 1 | import { SidenavAction, SidenavActionTypes } from './sidenav.actions'; 2 | 3 | export interface State { 4 | opened: boolean; 5 | } 6 | 7 | export const initialState: State = { 8 | opened: false 9 | }; 10 | 11 | export function reducer(state = initialState, action: SidenavAction): State { 12 | switch (action.type) { 13 | case SidenavActionTypes.HideSidenav: 14 | return { 15 | ...state, 16 | opened: false 17 | }; 18 | case SidenavActionTypes.ShowSidenav: 19 | return { 20 | ...state, 21 | opened: true 22 | }; 23 | default: 24 | return state; 25 | } 26 | } 27 | 28 | export const getOpened = (state: State) => state.opened; 29 | -------------------------------------------------------------------------------- /projects/simple-store/src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/dist/zone-testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/app/core/shell/shell.component.scss: -------------------------------------------------------------------------------- 1 | @import '~@angular/material/theming'; 2 | 3 | :host { 4 | display: flex; 5 | flex: 1; 6 | background-color: mat-color($mat-grey, 800); 7 | 8 | .container { 9 | position: absolute; 10 | top: 0; 11 | right: 0; 12 | bottom: 0; 13 | left: 0; 14 | } 15 | 16 | mat-sidenav { 17 | min-width: 200px; 18 | background: mat-color($mat-grey, 900); 19 | 20 | .mat-list { 21 | padding-top: 0; 22 | } 23 | 24 | .mat-list-item { 25 | text-decoration: none; 26 | color: mat-color($mat-blue-grey, 200); 27 | 28 | &:hover { 29 | color: mat-color($mat-blue-grey, 50); 30 | background: mat-color($mat-grey, 800); 31 | } 32 | } 33 | } 34 | } -------------------------------------------------------------------------------- /src/app/core/services/resort.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { 2 | HttpClientTestingModule, 3 | HttpTestingController 4 | } from '@angular/common/http/testing'; 5 | import { inject, TestBed } from '@angular/core/testing'; 6 | import { ResortService } from './resort.service'; 7 | 8 | describe('ResortService', () => { 9 | let httpTestingController: HttpTestingController; 10 | 11 | beforeEach(() => { 12 | TestBed.configureTestingModule({ 13 | providers: [ResortService], 14 | imports: [HttpClientTestingModule] 15 | }); 16 | 17 | httpTestingController = TestBed.get(HttpTestingController); 18 | }); 19 | 20 | it('should be created', inject([ResortService], (service: ResortService) => { 21 | expect(service).toBeTruthy(); 22 | })); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/+favorites/containers/favorites/favorites.component.html: -------------------------------------------------------------------------------- 1 | 6 |
7 |
8 | 9 | 10 | 11 | 12 | 13 | 14 |
15 |
-------------------------------------------------------------------------------- /src/app/containers/page-not-found/page-not-found.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { PageNotFoundComponent } from './page-not-found.component'; 3 | 4 | describe('PageNotFoundComponent', () => { 5 | let component: PageNotFoundComponent; 6 | let fixture: ComponentFixture; 7 | 8 | beforeEach(async(() => { 9 | TestBed.configureTestingModule({ 10 | declarations: [PageNotFoundComponent] 11 | }).compileComponents(); 12 | })); 13 | 14 | beforeEach(() => { 15 | fixture = TestBed.createComponent(PageNotFoundComponent); 16 | component = fixture.componentInstance; 17 | fixture.detectChanges(); 18 | }); 19 | 20 | it('should create', () => { 21 | expect(component).toBeTruthy(); 22 | }); 23 | }); 24 | -------------------------------------------------------------------------------- /src/app/state/map/map.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { SetMapZoom } from './map.actions'; 2 | import { initialState, reducer } from './map.reducer'; 3 | 4 | describe('Sidenav Reducer', () => { 5 | describe('unknown action', () => { 6 | it('should return the initial state', () => { 7 | const action = { type: 'NOOP' } as any; 8 | const result = reducer(initialState, action); 9 | 10 | expect(result).toBe(initialState); 11 | }); 12 | }); 13 | 14 | describe('SetMapZoom', () => { 15 | it('should set the opened property to false', () => { 16 | const zoom = 10; 17 | const action = new SetMapZoom(zoom); 18 | const result = reducer(initialState, action); 19 | 20 | expect(result).toEqual({ 21 | ...initialState, 22 | zoom 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { 3 | MatAutocompleteModule, 4 | MatButtonModule, 5 | MatCardModule, 6 | MatDialogModule, 7 | MatFormFieldModule, 8 | MatIconModule, 9 | MatInputModule, 10 | MatListModule, 11 | MatMenuModule, 12 | MatPaginatorModule, 13 | MatSidenavModule, 14 | MatTableModule, 15 | MatToolbarModule 16 | } from '@angular/material'; 17 | 18 | @NgModule({ 19 | exports: [ 20 | MatAutocompleteModule, 21 | MatButtonModule, 22 | MatCardModule, 23 | MatDialogModule, 24 | MatFormFieldModule, 25 | MatIconModule, 26 | MatInputModule, 27 | MatListModule, 28 | MatMenuModule, 29 | MatPaginatorModule, 30 | MatSidenavModule, 31 | MatTableModule, 32 | MatToolbarModule 33 | ] 34 | }) 35 | export class MaterialModule {} 36 | -------------------------------------------------------------------------------- /src/app/+favorites/favorites.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | import { MaterialModule } from '@app/material.module'; 5 | import { SharedModule } from '@app/shared/shared.module'; 6 | import { FavoritesTableComponent } from './components/favorites-table/favorites-table.component'; 7 | import { FavoritesComponent } from './containers/favorites/favorites.component'; 8 | import { FavoritesRoutingModule } from './favorites-routing.module'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | CommonModule, 13 | FavoritesRoutingModule, 14 | FlexLayoutModule, 15 | MaterialModule, 16 | SharedModule 17 | ], 18 | declarations: [FavoritesComponent, FavoritesTableComponent] 19 | }) 20 | export class FavoritesModule {} 21 | -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/app/containers/dashboard/dashboard.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Resort } from '@app/models/resort.model'; 3 | import { mapZoom, resorts, selectedResort, State } from '@app/state'; 4 | import { select, Store } from '@ngrx/store'; 5 | import { Observable } from 'rxjs'; 6 | 7 | @Component({ 8 | templateUrl: './dashboard.component.html', 9 | styleUrls: ['./dashboard.component.scss'] 10 | }) 11 | export class DashboardComponent implements OnInit { 12 | mapZoom: Observable; 13 | resorts: Observable>; 14 | selectedResort: Observable; 15 | 16 | constructor(private store: Store) {} 17 | 18 | ngOnInit() { 19 | this.mapZoom = this.store.pipe(select(mapZoom)); 20 | this.resorts = this.store.pipe(select(resorts)); 21 | this.selectedResort = this.store.pipe(select(selectedResort)); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /projects/simple-store-e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /src/app/core/services/route.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { inject, TestBed } from '@angular/core/testing'; 2 | import { RouteService } from '@app/core/services/route.service'; 3 | import { ShellComponent } from '@app/core/shell/shell.component'; 4 | 5 | describe('Route', () => { 6 | let route: RouteService; 7 | 8 | beforeEach(() => { 9 | TestBed.configureTestingModule({ 10 | providers: [RouteService] 11 | }); 12 | }); 13 | 14 | beforeEach(inject([RouteService], (_route: RouteService) => { 15 | route = _route; 16 | })); 17 | 18 | describe('withShell', () => { 19 | it('should create routes as children of shell', () => { 20 | const testRoutes = [{ path: 'test' }]; 21 | 22 | const result = RouteService.withShell(testRoutes); 23 | 24 | expect(result.path).toBe(''); 25 | expect(result.children).toBe(testRoutes); 26 | expect(result.component).toBe(ShellComponent); 27 | }); 28 | }); 29 | }); 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NgRx Course 2 | 3 | This is a 1-day course on NgRx. 4 | 5 | ## Prequisites 6 | 7 | Skills: 8 | 9 | - Understanding of TypeScript and ES6. 10 | - Intermediate to advanced understanding of Angular. 11 | - Comfortable using command line and 12 | 13 | Software: 14 | 15 | - npm or yarn 16 | - Code editor (VS Code recommended) 17 | 18 | ## Setup 19 | 20 | This course is built for Angular 6. 21 | As such, you will need to install the Angular 6 CLI: 22 | 23 | ``` 24 | npm install -g @angular/cli 25 | yarn add -g @angular/cli 26 | ``` 27 | 28 | We'll be using Angular Workspaces. 29 | Check out [this Gist if you're new to using Angular Workspaces](https://gist.github.com/blove/11ee297ec2e0d8940b0bb04e53ee76ca). 30 | 31 | ## Server 32 | 33 | Start the JSON server: 34 | 35 | ``` 36 | cd server 37 | yarn start 38 | ``` 39 | 40 | Browse the API at: [https://localhost:3000](https://localhost:3000) 41 | 42 | ## Copyright 43 | 44 | Copyright (c) 2019 LiveLoveApp, LLC 45 | -------------------------------------------------------------------------------- /src/app/+favorites/containers/favorites/favorites.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { Resort } from '@app/models/resort.model'; 3 | import { resorts, State } from '@app/state'; 4 | import { DeleteResort, SelectResort } from '@app/state/resort/resort.actions'; 5 | import { select, Store } from '@ngrx/store'; 6 | import { Observable } from 'rxjs'; 7 | 8 | @Component({ 9 | templateUrl: './favorites.component.html', 10 | styleUrls: ['./favorites.component.scss'] 11 | }) 12 | export class FavoritesComponent implements OnInit { 13 | resorts: Observable>; 14 | 15 | constructor(private store: Store) {} 16 | 17 | ngOnInit() { 18 | this.resorts = this.store.pipe(select(resorts)); 19 | } 20 | 21 | onDelete(resort: Resort) { 22 | this.store.dispatch(new DeleteResort({ id: resort.id })); 23 | } 24 | 25 | onResortSelected(resort: Resort) { 26 | this.store.dispatch(new SelectResort(resort)); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/app/shared/components/resorts-map/resorts-map.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { reducers } from '@app/state'; 3 | import { StoreModule } from '@ngrx/store'; 4 | import { NguiMapModule } from '@ngui/map'; 5 | import { ResortsMapComponent } from './resorts-map.component'; 6 | 7 | describe('ResortsMapComponent', () => { 8 | let component: ResortsMapComponent; 9 | let fixture: ComponentFixture; 10 | 11 | beforeEach(async(() => { 12 | TestBed.configureTestingModule({ 13 | declarations: [ResortsMapComponent], 14 | imports: [NguiMapModule, StoreModule.forRoot(reducers)] 15 | }).compileComponents(); 16 | })); 17 | 18 | beforeEach(() => { 19 | fixture = TestBed.createComponent(ResortsMapComponent); 20 | component = fixture.componentInstance; 21 | fixture.detectChanges(); 22 | }); 23 | 24 | it('should create', () => { 25 | expect(component).toBeTruthy(); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/app/app.routing.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { PreloadAllModules, RouterModule, Routes } from '@angular/router'; 3 | import { DashboardComponent } from '@app/containers/dashboard/dashboard.component'; 4 | import { PageNotFoundComponent } from '@app/containers/page-not-found/page-not-found.component'; 5 | import { RouteService } from '@app/core/services/route.service'; 6 | 7 | const routes: Routes = [ 8 | RouteService.withShell([ 9 | { path: 'dashboard', component: DashboardComponent }, 10 | { 11 | path: 'favorites', 12 | loadChildren: './+favorites/favorites.module#FavoritesModule' 13 | }, 14 | { path: '', redirectTo: 'dashboard', pathMatch: 'full' } 15 | ]), 16 | { path: '**', component: PageNotFoundComponent } 17 | ]; 18 | 19 | @NgModule({ 20 | imports: [ 21 | RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules }) 22 | ], 23 | exports: [RouterModule], 24 | providers: [] 25 | }) 26 | export class AppRoutingModule {} 27 | -------------------------------------------------------------------------------- /src/app/core/core.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { 3 | ModuleWithProviders, 4 | NgModule, 5 | Optional, 6 | SkipSelf 7 | } from '@angular/core'; 8 | import { FlexLayoutModule } from '@angular/flex-layout'; 9 | import { RouterModule } from '@angular/router'; 10 | import { ShellComponent } from '@app/core/shell/shell.component'; 11 | import { MaterialModule } from '@app/material.module'; 12 | 13 | @NgModule({ 14 | imports: [CommonModule, FlexLayoutModule, MaterialModule, RouterModule], 15 | declarations: [ShellComponent] 16 | }) 17 | export class CoreModule { 18 | static forRoot(): ModuleWithProviders { 19 | return { 20 | ngModule: CoreModule 21 | }; 22 | } 23 | 24 | constructor( 25 | @Optional() 26 | @SkipSelf() 27 | parentModule?: CoreModule 28 | ) { 29 | if (parentModule) { 30 | throw new Error( 31 | `${parentModule} has already been loaded. Import Core module in the AppModule only.` 32 | ); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/+favorites/components/favorites-table/favorites-table.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 3 | import { MaterialModule } from '@app/material.module'; 4 | import { FavoritesTableComponent } from './favorites-table.component'; 5 | 6 | describe('FavoritesTableComponent', () => { 7 | let component: FavoritesTableComponent; 8 | let fixture: ComponentFixture; 9 | 10 | beforeEach(async(() => { 11 | TestBed.configureTestingModule({ 12 | declarations: [FavoritesTableComponent], 13 | imports: [MaterialModule, NoopAnimationsModule] 14 | }).compileComponents(); 15 | })); 16 | 17 | beforeEach(() => { 18 | fixture = TestBed.createComponent(FavoritesTableComponent); 19 | component = fixture.componentInstance; 20 | fixture.detectChanges(); 21 | }); 22 | 23 | it('should create', () => { 24 | expect(component).toBeTruthy(); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { MaterialModule } from '@app/material.module'; 5 | import { ResortAutocompleteComponent } from '@app/shared/components/resort-autocomplete/resort-autocomplete.component'; 6 | import { ResortsMapComponent } from '@app/shared/components/resorts-map/resorts-map.component'; 7 | import { environment } from '@env/environment'; 8 | import { NguiMapModule } from '@ngui/map'; 9 | 10 | export const components = [ResortAutocompleteComponent, ResortsMapComponent]; 11 | 12 | @NgModule({ 13 | declarations: components, 14 | exports: components, 15 | imports: [ 16 | CommonModule, 17 | MaterialModule, 18 | FormsModule, 19 | NguiMapModule.forRoot({ 20 | apiUrl: `https://maps.google.com/maps/api/js?key=${ 21 | environment.google.maps.apiKey 22 | }` 23 | }), 24 | ReactiveFormsModule 25 | ] 26 | }) 27 | export class SharedModule {} 28 | -------------------------------------------------------------------------------- /src/app/+favorites/components/favorites-table/favorites-table.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | Name 4 | {{resort.name}} 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /projects/simple-store/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../../coverage'), 20 | reports: ['html', 'lcovonly'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/app/+favorites/components/favorites-table/favorites-table.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | EventEmitter, 4 | Input, 5 | OnChanges, 6 | OnInit, 7 | Output, 8 | SimpleChanges, 9 | ViewChild 10 | } from '@angular/core'; 11 | import { MatPaginator, MatTableDataSource } from '@angular/material'; 12 | import { Resort } from '@app/models/resort.model'; 13 | 14 | @Component({ 15 | selector: 'app-favorites-table', 16 | templateUrl: './favorites-table.component.html', 17 | styleUrls: ['./favorites-table.component.scss'] 18 | }) 19 | export class FavoritesTableComponent implements OnChanges, OnInit { 20 | columnsToDisplay = ['name', 'actions']; 21 | dataSource = new MatTableDataSource(); 22 | @Output() delete = new EventEmitter(); 23 | @Input() resorts: Resort[]; 24 | @ViewChild(MatPaginator) paginator: MatPaginator; 25 | 26 | ngOnChanges(simpleChanges: SimpleChanges) { 27 | if (simpleChanges.resorts && simpleChanges.resorts.currentValue) { 28 | this.dataSource.data = simpleChanges.resorts.currentValue; 29 | } 30 | } 31 | 32 | ngOnInit() { 33 | this.dataSource.paginator = this.paginator; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/state/sidenav/sidenav.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { HideSidenav, ShowSidenav } from './sidenav.actions'; 2 | import { initialState, reducer } from './sidenav.reducer'; 3 | 4 | describe('Sidenav Reducer', () => { 5 | describe('unknown action', () => { 6 | it('should return the initial state', () => { 7 | const action = { type: 'NOOP' } as any; 8 | const result = reducer(initialState, action); 9 | 10 | expect(result).toBe(initialState); 11 | }); 12 | }); 13 | 14 | describe('HideSidenav', () => { 15 | it('should set the opened property to false', () => { 16 | const action = new HideSidenav(); 17 | const result = reducer(initialState, action); 18 | 19 | expect(result).toEqual({ 20 | ...initialState, 21 | opened: false 22 | }); 23 | }); 24 | }); 25 | 26 | describe('ShowSidenav', () => { 27 | it('should set the opened property to true', () => { 28 | const action = new ShowSidenav(); 29 | const result = reducer(initialState, action); 30 | 31 | expect(result).toEqual({ 32 | ...initialState, 33 | opened: true 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/app/state/dialog/dialog.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { CloseDialogs, OpenDialog } from './dialog.actions'; 2 | import { initialState, reducer } from './dialog.reducer'; 3 | 4 | describe('Sidenav Reducer', () => { 5 | describe('unknown action', () => { 6 | it('should return the initial state', () => { 7 | const action = { type: 'NOOP' } as any; 8 | const result = reducer(initialState, action); 9 | 10 | expect(result).toBe(initialState); 11 | }); 12 | }); 13 | 14 | describe('CloseDialogs', () => { 15 | it('should set the opened property to false', () => { 16 | const action = new CloseDialogs(); 17 | const result = reducer(initialState, action); 18 | 19 | expect(result).toEqual({ 20 | ...initialState, 21 | opened: false 22 | }); 23 | }); 24 | }); 25 | 26 | describe('ShowDialog', () => { 27 | it('should set the opened property to true', () => { 28 | const action = new OpenDialog(jest.fn()); 29 | const result = reducer(initialState, action); 30 | 31 | expect(result).toEqual({ 32 | ...initialState, 33 | opened: true 34 | }); 35 | }); 36 | }); 37 | }); 38 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build ---prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | google: { 7 | maps: { 8 | apiKey: 'AIzaSyBDwbyfhmy6ywu_C-oY7m7eHosBCLdiAK8' 9 | }, 10 | firebase: { 11 | apiKey: 'AIzaSyAkGfqfWiHAqNKOhRTll-Rhw1Qw7SZN1ro', 12 | authDomain: 'ngrx-course-210605.firebaseapp.com', 13 | databaseURL: 'https://ngrx-course-210605.firebaseio.com', 14 | projectId: 'ngrx-course-210605', 15 | storageBucket: 'ngrx-course-210605.appspot.com', 16 | messagingSenderId: '679702729839' 17 | } 18 | }, 19 | production: false 20 | }; 21 | 22 | /* 23 | * In development mode, to ignore zone related error stack frames such as 24 | * `zone.run`, `zoneDelegate.invokeTask` for easier debugging, you can 25 | * import the following file, but please comment it out in production mode 26 | * because it will have performance impact when throw error 27 | */ 28 | // import 'zone.js/dist/zone-error'; // Included with Angular CLI. 29 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { store } from './store'; 3 | import { HideSidenav, LoadResorts, ShowSidenav } from './store/actions'; 4 | import { Resort } from './store/models'; 5 | import { initialSidenavState } from './store/reducers'; 6 | import { Store } from './store/store'; 7 | 8 | @Component({ 9 | selector: 'app-root', 10 | templateUrl: './app.component.html', 11 | styleUrls: ['./app.component.scss'] 12 | }) 13 | export class AppComponent implements OnInit { 14 | resorts: Resort[]; 15 | sidenavOpened = initialSidenavState.opened; 16 | store: Store; 17 | 18 | ngOnInit() { 19 | this.store = store; 20 | this.store.dispatch(new LoadResorts()); 21 | this.store.subscribe(state => { 22 | this.resorts = state.resort.resorts; 23 | this.sidenavOpened = state.sidenav.opened; 24 | console.log(state); 25 | }); 26 | } 27 | 28 | hideSidenav() { 29 | this.store.dispatch(new HideSidenav()); 30 | } 31 | 32 | identifyResort(resort: Resort) { 33 | return resort.id; 34 | } 35 | 36 | showSidenav() { 37 | this.store.dispatch(new ShowSidenav()); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/state/dialog/dialog.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { MatDialog } from '@angular/material'; 3 | import { Actions, Effect, ofType } from '@ngrx/effects'; 4 | import { Action } from '@ngrx/store'; 5 | import { Observable } from 'rxjs'; 6 | import { map, switchMap, tap } from 'rxjs/operators'; 7 | import { CloseDialogs, DialogActionTypes, OpenDialog } from './dialog.actions'; 8 | 9 | @Injectable() 10 | export class DialogEffects { 11 | @Effect({ 12 | dispatch: false 13 | }) 14 | close: Observable = this.actions.pipe( 15 | ofType(DialogActionTypes.CloseDialogs), 16 | tap(() => this.matDialog.closeAll()) 17 | ); 18 | 19 | @Effect() 20 | open: Observable = this.actions.pipe( 21 | ofType(DialogActionTypes.OpenDialog), 22 | map((action: OpenDialog) => 23 | this.matDialog.open(action.componentType, { 24 | ...action.config, 25 | disableClose: true 26 | }) 27 | ), 28 | switchMap(matDialogRef => matDialogRef.backdropClick()), 29 | map(() => new CloseDialogs()) 30 | ); 31 | 32 | constructor(private actions: Actions, private matDialog: MatDialog) {} 33 | } 34 | -------------------------------------------------------------------------------- /src/app/core/shell/shell.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 19 | 20 | 21 | 22 | 23 | 26 | {{title}} 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/app/state/state.module.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ModuleWithProviders, 3 | NgModule, 4 | Optional, 5 | SkipSelf 6 | } from '@angular/core'; 7 | import { metaReducers, reducers } from '@app/state'; 8 | import { DialogEffects } from '@app/state/dialog/dialog.effects'; 9 | import { ResortEffects } from '@app/state/resort/resort.effects'; 10 | import { environment } from '@env/environment'; 11 | import { EffectsModule } from '@ngrx/effects'; 12 | import { StoreModule } from '@ngrx/store'; 13 | import { StoreDevtoolsModule } from '@ngrx/store-devtools'; 14 | 15 | @NgModule({ 16 | imports: [ 17 | StoreModule.forRoot(reducers, { metaReducers }), 18 | EffectsModule.forRoot([DialogEffects, ResortEffects]), 19 | !environment.production ? StoreDevtoolsModule.instrument() : [] 20 | ] 21 | }) 22 | export class StateModule { 23 | static forRoot(): ModuleWithProviders { 24 | return { 25 | ngModule: StateModule 26 | }; 27 | } 28 | 29 | constructor( 30 | @Optional() 31 | @SkipSelf() 32 | parentModule: StateModule 33 | ) { 34 | if (parentModule) { 35 | throw new Error( 36 | `${parentModule} has already been loaded. Import StateModule in the AppModule only.` 37 | ); 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/actions.ts: -------------------------------------------------------------------------------- 1 | import { Resort } from './models'; 2 | import { Action } from './store'; 3 | 4 | export enum SidenavActionTypes { 5 | HideSidenav = '[Sidenav] Hide Sidenav', 6 | ShowSidenav = '[Sidenav] Show Sidenav' 7 | } 8 | 9 | export class HideSidenav implements Action { 10 | readonly type = SidenavActionTypes.HideSidenav; 11 | } 12 | 13 | export class ShowSidenav implements Action { 14 | readonly type = SidenavActionTypes.ShowSidenav; 15 | } 16 | 17 | export type SidenavAction = HideSidenav | ShowSidenav; 18 | 19 | export enum ResortActions { 20 | LoadResorts = '[resorts] Load', 21 | LoadResortsFail = '[resorts] Load fail', 22 | LoadResortsSuccess = '[resorts] Load success' 23 | } 24 | 25 | export class LoadResorts implements Action { 26 | readonly type = ResortActions.LoadResorts; 27 | } 28 | 29 | export class LoadResortsFail implements Action { 30 | readonly type = ResortActions.LoadResortsFail; 31 | 32 | constructor(public error: Error) {} 33 | } 34 | 35 | export class LoadResortsSuccess implements Action { 36 | readonly type = ResortActions.LoadResortsSuccess; 37 | 38 | constructor(public resorts: Resort[]) {} 39 | } 40 | 41 | export type ResortAction = LoadResorts | LoadResortsSuccess | LoadResortsFail; 42 | -------------------------------------------------------------------------------- /jest-global-mocks.ts: -------------------------------------------------------------------------------- 1 | global['CSS'] = null; 2 | 3 | const mock = () => { 4 | let storage = {}; 5 | return { 6 | getItem: key => (key in storage ? storage[key] : null), 7 | setItem: (key, value) => (storage[key] = value || ''), 8 | removeItem: key => delete storage[key], 9 | clear: () => (storage = {}) 10 | }; 11 | }; 12 | 13 | Object.defineProperty(window, 'localStorage', { value: mock() }); 14 | Object.defineProperty(window, 'sessionStorage', { value: mock() }); 15 | Object.defineProperty(document, 'doctype', { 16 | value: '' 17 | }); 18 | Object.defineProperty(window, 'getComputedStyle', { 19 | value: () => { 20 | return { 21 | display: 'none', 22 | appearance: ['-webkit-appearance'], 23 | getPropertyValue: prop => { 24 | return ''; 25 | } 26 | }; 27 | } 28 | }); 29 | /** 30 | * ISSUE: https://github.com/angular/material2/issues/7101 31 | * Workaround for JSDOM missing transform property 32 | */ 33 | Object.defineProperty(document.body.style, 'transform', { 34 | value: () => { 35 | return { 36 | enumerable: true, 37 | configurable: true 38 | }; 39 | } 40 | }); 41 | 42 | Object.defineProperty(window, 'matchMedia', { 43 | value: () => ({ 44 | matches: false, 45 | addListener: function() {}, 46 | removeListener: function() {} 47 | }) 48 | }); 49 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "type": "node", 9 | "request": "launch", 10 | "name": "Jest Test", 11 | "program": "${workspaceFolder}/node_modules/.bin/jest", 12 | "args": [ 13 | "--runInBand", 14 | "--rootDir=${workspaceFolder}/course", 15 | "--config=package.json" 16 | ], 17 | "console": "integratedTerminal", 18 | "internalConsoleOptions": "neverOpen" 19 | }, 20 | { 21 | "name": "ng serve", 22 | "type": "chrome", 23 | "request": "launch", 24 | "url": "http://localhost:4200/", 25 | "webRoot": "${workspaceFolder}", 26 | "sourceMapPathOverrides": { 27 | "webpack:/./*": "${webRoot}/*", 28 | "webpack:/src/*": "${webRoot}/src/*", 29 | "webpack:/*": "*", 30 | "webpack:/./~/*": "${webRoot}/node_modules/*" 31 | } 32 | }, 33 | { 34 | "name": "ng e2e", 35 | "type": "node", 36 | "request": "launch", 37 | "program": "${workspaceFolder}/node_modules/protractor/bin/protractor", 38 | "protocol": "inspector", 39 | "args": ["${workspaceFolder}/protractor.conf.js"] 40 | } 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/app/state/resort/resort.actions.ts: -------------------------------------------------------------------------------- 1 | import { Resort } from '@app/models/resort.model'; 2 | import { Action } from '@ngrx/store'; 3 | 4 | export enum ResortActionTypes { 5 | DeleteResort = '[Resort] Delete', 6 | SearchResorts = '[Resort] Search', 7 | SearchResortsFail = '[Resort] Search Fail', 8 | SearchResortsSuccess = '[Resort] Search Success', 9 | SelectResort = '[Resort] Select' 10 | } 11 | 12 | export class DeleteResort implements Action { 13 | readonly type = ResortActionTypes.DeleteResort; 14 | 15 | constructor(public payload: { id: string }) {} 16 | } 17 | 18 | export class SearchResorts implements Action { 19 | readonly type = ResortActionTypes.SearchResorts; 20 | 21 | constructor(public q: string) {} 22 | } 23 | 24 | export class SearchResortsFail implements Action { 25 | readonly type = ResortActionTypes.SearchResortsFail; 26 | 27 | constructor(public error: Error) {} 28 | } 29 | 30 | export class SearchResortsSuccess implements Action { 31 | readonly type = ResortActionTypes.SearchResortsSuccess; 32 | 33 | constructor(public resorts: Resort[]) {} 34 | } 35 | 36 | export class SelectResort implements Action { 37 | readonly type = ResortActionTypes.SelectResort; 38 | 39 | constructor(public resort: Resort) {} 40 | } 41 | 42 | export type ResortActions = 43 | | DeleteResort 44 | | SearchResorts 45 | | SearchResortsFail 46 | | SearchResortsSuccess 47 | | SelectResort; 48 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/reducers.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ResortAction, 3 | ResortActions, 4 | SidenavAction, 5 | SidenavActionTypes 6 | } from './actions'; 7 | import { State } from './store'; 8 | 9 | export const initialSidenavState = { 10 | opened: false 11 | }; 12 | 13 | export const sidenavReducer = ( 14 | state: State = initialSidenavState, 15 | action: SidenavAction 16 | ): State => { 17 | switch (action.type) { 18 | case SidenavActionTypes.HideSidenav: 19 | return { 20 | ...state, 21 | opened: false 22 | }; 23 | case SidenavActionTypes.ShowSidenav: 24 | return { 25 | ...state, 26 | opened: true 27 | }; 28 | default: 29 | return state; 30 | } 31 | }; 32 | 33 | export const initialResortState = { 34 | error: null, 35 | loading: false, 36 | resorts: [] 37 | }; 38 | 39 | export const resortReducer = ( 40 | state: State = initialResortState, 41 | action: ResortAction 42 | ): State => { 43 | switch (action.type) { 44 | case ResortActions.LoadResorts: 45 | return { 46 | ...state, 47 | loading: true 48 | }; 49 | case ResortActions.LoadResortsFail: 50 | return { 51 | ...state, 52 | error: action.error, 53 | loading: false 54 | }; 55 | case ResortActions.LoadResortsSuccess: 56 | return { 57 | ...state, 58 | error: null, 59 | loading: false, 60 | resorts: action.resorts 61 | }; 62 | default: 63 | return state; 64 | } 65 | }; 66 | -------------------------------------------------------------------------------- /src/app/+favorites/containers/favorites/favorites.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { FlexLayoutModule } from '@angular/flex-layout'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MaterialModule } from '@app/material.module'; 6 | import { SharedModule } from '@app/shared/shared.module'; 7 | import { reducers } from '@app/state'; 8 | import { StoreModule } from '@ngrx/store'; 9 | import { FavoritesTableComponent } from './../../components/favorites-table/favorites-table.component'; 10 | import { FavoritesComponent } from './favorites.component'; 11 | 12 | describe('FavoritesComponent', () => { 13 | let component: FavoritesComponent; 14 | let fixture: ComponentFixture; 15 | 16 | beforeEach(async(() => { 17 | TestBed.configureTestingModule({ 18 | declarations: [FavoritesComponent, FavoritesTableComponent], 19 | imports: [ 20 | FlexLayoutModule, 21 | FormsModule, 22 | MaterialModule, 23 | NoopAnimationsModule, 24 | ReactiveFormsModule, 25 | SharedModule, 26 | StoreModule.forRoot(reducers) 27 | ] 28 | }).compileComponents(); 29 | })); 30 | 31 | beforeEach(() => { 32 | fixture = TestBed.createComponent(FavoritesComponent); 33 | component = fixture.componentInstance; 34 | fixture.detectChanges(); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /projects/simple-store/src/app/store/store.ts: -------------------------------------------------------------------------------- 1 | import { Subject } from 'rxjs'; 2 | import { filter } from 'rxjs/operators'; 3 | import { Action } from './store'; 4 | 5 | export interface Action { 6 | type: string; 7 | } 8 | 9 | export interface ReducerMap { 10 | [key: string]: Function; 11 | } 12 | 13 | export interface State { 14 | [key: string]: any; 15 | } 16 | 17 | export class Store { 18 | private effects: Function[] = []; 19 | private state$ = new Subject(); 20 | 21 | constructor( 22 | private reducers: ReducerMap, 23 | effects?: { new () }[], 24 | private state: State = {} 25 | ) { 26 | effects.forEach(Effect => { 27 | const effect = new Effect(); 28 | const props = Object.getOwnPropertyNames(effect); 29 | Object.getOwnPropertyNames(effect) 30 | .map(propertyName => effect[propertyName]) 31 | .filter(property => typeof property === 'function') 32 | .forEach(fn => this.effects.push(fn)); 33 | }); 34 | } 35 | 36 | dispatch(action: Action) { 37 | this.notify(action); 38 | this.state$.next(this.state); 39 | } 40 | 41 | notify(action: Action) { 42 | Object.keys(this.reducers).forEach(key => { 43 | this.state[key] = this.reducers[key](this.state[key], action); 44 | }); 45 | this.effects.forEach(effect => { 46 | effect(action) 47 | .pipe(filter(result => !!result)) 48 | .subscribe((a: Action) => this.dispatch(a)); 49 | }); 50 | } 51 | 52 | subscribe( 53 | next: (value: State) => void, 54 | error?: (error: any) => void, 55 | complete?: () => void 56 | ) { 57 | return this.state$.subscribe(next, error, complete); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/app/state/resort/resort.effects.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { ResortService } from '@app/core/services/resort.service'; 3 | import { SetMapZoom } from '@app/state/map/map.actions'; 4 | import { HideSidenav } from '@app/state/sidenav/sidenav.actions'; 5 | import { Actions, Effect, ofType } from '@ngrx/effects'; 6 | import { Action } from '@ngrx/store'; 7 | import { Observable, of } from 'rxjs'; 8 | import { catchError, exhaustMap, map } from 'rxjs/operators'; 9 | import { CloseDialogs } from './../dialog/dialog.actions'; 10 | import { 11 | ResortActionTypes, 12 | SearchResorts, 13 | SearchResortsFail, 14 | SearchResortsSuccess, 15 | SelectResort 16 | } from './resort.actions'; 17 | 18 | @Injectable() 19 | export class ResortEffects { 20 | @Effect() 21 | search: Observable = this.actions.pipe( 22 | ofType(ResortActionTypes.SearchResorts), 23 | exhaustMap(action => { 24 | return this.resortService.search(action.q).pipe( 25 | map(resorts => new SearchResortsSuccess(resorts)), 26 | catchError(error => of(new SearchResortsFail(error))) 27 | ); 28 | }) 29 | ); 30 | 31 | @Effect() 32 | closeDialogOnSelect: Observable = this.actions.pipe( 33 | ofType(ResortActionTypes.SelectResort), 34 | map(() => new CloseDialogs()) 35 | ); 36 | 37 | @Effect() 38 | hideSidenavOnSelect: Observable = this.actions.pipe( 39 | ofType(ResortActionTypes.SelectResort), 40 | map(() => new HideSidenav()) 41 | ); 42 | 43 | @Effect() 44 | setMapZoom: Observable = this.actions.pipe( 45 | ofType(ResortActionTypes.SelectResort), 46 | map(() => new SetMapZoom(12)) 47 | ); 48 | 49 | constructor(private actions: Actions, private resortService: ResortService) {} 50 | } 51 | -------------------------------------------------------------------------------- /src/app/core/shell/shell.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { ObservableMedia } from '@angular/flex-layout'; 3 | import { Title } from '@angular/platform-browser'; 4 | import { SearchDialogComponent } from '@app/containers/search-dialog/search-dialog.component'; 5 | import { sidenavIsOpen, State } from '@app/state'; 6 | import { HideSidenav, ShowSidenav } from '@app/state/sidenav/sidenav.actions'; 7 | import { select, Store } from '@ngrx/store'; 8 | import { Observable } from 'rxjs'; 9 | import { first } from 'rxjs/operators'; 10 | import { OpenDialog } from './../../state/dialog/dialog.actions'; 11 | 12 | @Component({ 13 | templateUrl: './shell.component.html', 14 | styleUrls: ['./shell.component.scss'] 15 | }) 16 | export class ShellComponent implements OnInit { 17 | opened: Observable; 18 | 19 | constructor( 20 | private titleService: Title, 21 | private media: ObservableMedia, 22 | private store: Store 23 | ) {} 24 | 25 | ngOnInit() { 26 | this.opened = this.store.pipe(select(sidenavIsOpen)); 27 | } 28 | 29 | get isMobile(): boolean { 30 | return this.media.isActive('xs') || this.media.isActive('sm'); 31 | } 32 | 33 | get title(): string { 34 | return this.titleService.getTitle(); 35 | } 36 | 37 | closeSidenav() { 38 | this.store.dispatch(new HideSidenav()); 39 | } 40 | 41 | openSidenav() { 42 | this.store.dispatch(new ShowSidenav()); 43 | } 44 | 45 | openDialog() { 46 | this.store.dispatch( 47 | new OpenDialog(SearchDialogComponent, { 48 | width: '320px' 49 | }) 50 | ); 51 | } 52 | 53 | toggleSidenav() { 54 | this.opened.pipe(first()).subscribe(open => { 55 | if (open) { 56 | return this.closeSidenav(); 57 | } 58 | this.openSidenav(); 59 | }); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientModule } from '@angular/common/http'; 2 | import { NgModule } from '@angular/core'; 3 | import { ReactiveFormsModule } from '@angular/forms'; 4 | import { BrowserModule } from '@angular/platform-browser'; 5 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 6 | import { ServiceWorkerModule } from '@angular/service-worker'; 7 | import { AppComponent } from '@app/app.component'; 8 | import { AppRoutingModule } from '@app/app.routing.module'; 9 | import { DashboardComponent } from '@app/containers/dashboard/dashboard.component'; 10 | import { PageNotFoundComponent } from '@app/containers/page-not-found/page-not-found.component'; 11 | import { SearchDialogComponent } from '@app/containers/search-dialog/search-dialog.component'; 12 | import { CoreModule } from '@app/core/core.module'; 13 | import { MaterialModule } from '@app/material.module'; 14 | import { SharedModule } from '@app/shared/shared.module'; 15 | import { StateModule } from '@app/state/state.module'; 16 | import { environment } from '@env/environment'; 17 | import { AngularFireModule } from 'angularfire2'; 18 | 19 | @NgModule({ 20 | declarations: [ 21 | AppComponent, 22 | DashboardComponent, 23 | PageNotFoundComponent, 24 | SearchDialogComponent 25 | ], 26 | entryComponents: [SearchDialogComponent], 27 | imports: [ 28 | AngularFireModule.initializeApp(environment.google.firebase, 'ngrx-course'), 29 | AppRoutingModule, 30 | BrowserModule, 31 | BrowserAnimationsModule, 32 | CoreModule.forRoot(), 33 | HttpClientModule, 34 | MaterialModule, 35 | ReactiveFormsModule, 36 | ServiceWorkerModule.register('./ngsw-worker.js', { 37 | enabled: environment.production 38 | }), 39 | SharedModule, 40 | StateModule.forRoot() 41 | ], 42 | providers: [], 43 | bootstrap: [AppComponent] 44 | }) 45 | export class AppModule {} 46 | -------------------------------------------------------------------------------- /src/app/shared/components/resort-autocomplete/resort-autocomplete.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, EventEmitter, Output } from '@angular/core'; 2 | import { FormControl } from '@angular/forms'; 3 | import { MatAutocompleteSelectedEvent } from '@angular/material'; 4 | import { Resort } from '@app/models/resort.model'; 5 | import { searchResults, State } from '@app/state'; 6 | import { SearchResorts } from '@app/state/resort/resort.actions'; 7 | import { select, Store } from '@ngrx/store'; 8 | import { Observable, Subject } from 'rxjs'; 9 | import { debounceTime, filter, takeUntil } from 'rxjs/operators'; 10 | 11 | @Component({ 12 | selector: 'app-resort-autocomplete', 13 | templateUrl: './resort-autocomplete.component.html', 14 | styleUrls: ['./resort-autocomplete.component.scss'] 15 | }) 16 | export class ResortAutocompleteComponent { 17 | resorts: Observable; 18 | searchFormControl = new FormControl(); 19 | @Output() selected = new EventEmitter(); 20 | 21 | private unsubscribe = new Subject(); 22 | 23 | constructor(private store: Store) {} 24 | 25 | ngOnDestroy() { 26 | this.unsubscribe.next(); 27 | this.unsubscribe.complete(); 28 | } 29 | 30 | ngOnInit() { 31 | this.resorts = this.store.pipe( 32 | select(searchResults), 33 | filter(results => results.length > 0) 34 | ); 35 | 36 | this.searchFormControl.valueChanges 37 | .pipe( 38 | filter(q => q.length > 1), 39 | debounceTime(500), 40 | takeUntil(this.unsubscribe) 41 | ) 42 | .subscribe(q => this.store.dispatch(new SearchResorts(q))); 43 | } 44 | 45 | display(resort?: Resort): string | undefined { 46 | return resort ? resort.name : undefined; 47 | } 48 | 49 | onOptionSelected(event: MatAutocompleteSelectedEvent) { 50 | const resort: Resort = event.option.value; 51 | this.selected.emit(resort); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/app/state/resort/resort.reducer.ts: -------------------------------------------------------------------------------- 1 | import { Resort } from '@app/models/resort.model'; 2 | import { createEntityAdapter, EntityAdapter, EntityState } from '@ngrx/entity'; 3 | import { ResortActions, ResortActionTypes } from './resort.actions'; 4 | 5 | export interface State extends EntityState { 6 | error?: Error | null; 7 | loading: boolean; 8 | searchResults: Resort[]; 9 | selectedResortId?: string; 10 | } 11 | 12 | export const adapter: EntityAdapter = createEntityAdapter(); 13 | 14 | export const initialState: State = adapter.getInitialState({ 15 | loading: false, 16 | resorts: [], 17 | searchResults: [] 18 | }); 19 | 20 | export function reducer(state = initialState, action: ResortActions): State { 21 | switch (action.type) { 22 | case ResortActionTypes.DeleteResort: 23 | return adapter.removeOne(action.payload.id, state); 24 | case ResortActionTypes.SearchResorts: 25 | return { 26 | ...state, 27 | loading: true, 28 | error: null, 29 | searchResults: [] 30 | }; 31 | case ResortActionTypes.SearchResortsFail: 32 | return { 33 | ...state, 34 | error: action.error, 35 | loading: false 36 | }; 37 | case ResortActionTypes.SearchResortsSuccess: 38 | return { 39 | ...state, 40 | error: null, 41 | loading: false, 42 | searchResults: action.resorts 43 | }; 44 | case ResortActionTypes.SelectResort: 45 | return adapter.addOne(action.resort, { 46 | ...state, 47 | selectedResortId: action.resort.id 48 | }); 49 | default: 50 | return state; 51 | } 52 | } 53 | 54 | export const getError = (state: State) => state.error; 55 | export const getLoading = (state: State) => state.loading; 56 | export const getSearchResults = (state: State) => state.searchResults; 57 | export const getSelectedResortId = (state: State) => state.selectedResortId; 58 | -------------------------------------------------------------------------------- /src/app/shared/components/resorts-map/resorts-map.component.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Component, 3 | Input, 4 | OnChanges, 5 | SimpleChanges, 6 | ViewChild 7 | } from '@angular/core'; 8 | import { Resort } from '@app/models/resort.model'; 9 | import { State } from '@app/state'; 10 | import { Store } from '@ngrx/store'; 11 | import { NguiMapComponent } from '@ngui/map'; 12 | 13 | interface Marker { 14 | position: number[]; 15 | title: string; 16 | } 17 | 18 | @Component({ 19 | selector: 'app-resorts-map', 20 | templateUrl: './resorts-map.component.html', 21 | styleUrls: ['./resorts-map.component.scss'] 22 | }) 23 | export class ResortsMapComponent implements OnChanges { 24 | @Input() height: number; 25 | @Input() resorts: Resort[]; 26 | @Input() selectedResort: Resort; 27 | @Input() zoom: number; 28 | 29 | bounds: google.maps.LatLngBounds; 30 | map: google.maps.Map; 31 | markers: Marker[] = []; 32 | @ViewChild(NguiMapComponent) nguiMapComponent: NguiMapComponent; 33 | title: string; 34 | 35 | constructor(private store: Store) {} 36 | 37 | ngOnChanges(simpleChanges: SimpleChanges) { 38 | if (simpleChanges.resorts && simpleChanges.resorts.currentValue) { 39 | this.markers = this.resorts.map(resort => ({ 40 | position: [Number(resort.lat), Number(resort.lng)], 41 | title: resort.name 42 | })); 43 | } 44 | } 45 | 46 | onMapClick() { 47 | this.nguiMapComponent.closeInfoWindow('infoWindow'); 48 | } 49 | 50 | onMapReady(map: google.maps.Map) { 51 | this.map = map; 52 | this.bounds = new google.maps.LatLngBounds(); 53 | } 54 | 55 | onMarkerClick(event) { 56 | const marker = event.target; 57 | this.title = event.target.getTitle(); 58 | this.nguiMapComponent.openInfoWindow('infoWindow', marker); 59 | } 60 | 61 | onMarkerInit(marker) { 62 | this.bounds.extend(marker.getPosition()); 63 | this.map.setCenter(this.bounds.getCenter()); 64 | if (this.resorts.length === 1) { 65 | this.map.setZoom(this.zoom); 66 | } else { 67 | this.map.fitBounds(this.bounds); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/app/containers/search-dialog/search-dialog.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 2 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 5 | import { MaterialModule } from '@app/material.module'; 6 | import { generateResort } from '@app/models/resort.model'; 7 | import { ResortAutocompleteComponent } from '@app/shared/components/resort-autocomplete/resort-autocomplete.component'; 8 | import { reducers, State } from '@app/state/index'; 9 | import { SelectResort } from '@app/state/resort/resort.actions'; 10 | import { Store, StoreModule } from '@ngrx/store'; 11 | import { SearchDialogComponent } from './search-dialog.component'; 12 | 13 | describe('SearchDialogComponent', () => { 14 | let component: SearchDialogComponent; 15 | let fixture: ComponentFixture; 16 | let store: Store; 17 | 18 | const resort = generateResort(); 19 | 20 | beforeEach(async(() => { 21 | TestBed.configureTestingModule({ 22 | declarations: [ResortAutocompleteComponent, SearchDialogComponent], 23 | imports: [ 24 | FormsModule, 25 | HttpClientTestingModule, 26 | MaterialModule, 27 | NoopAnimationsModule, 28 | ReactiveFormsModule, 29 | StoreModule.forRoot(reducers) 30 | ] 31 | }).compileComponents(); 32 | })); 33 | 34 | beforeEach(() => { 35 | fixture = TestBed.createComponent(SearchDialogComponent); 36 | component = fixture.componentInstance; 37 | fixture.detectChanges(); 38 | store = TestBed.get(Store); 39 | }); 40 | 41 | it('should create', () => { 42 | expect(component).toBeTruthy(); 43 | }); 44 | 45 | describe('onResortSelected', () => { 46 | it('should dispatch the SelectResort action', () => { 47 | const action = new SelectResort(resort); 48 | const spy = jest.spyOn(store, 'dispatch'); 49 | 50 | component.onResortSelected(resort); 51 | 52 | expect(spy).toHaveBeenCalledWith(action); 53 | }); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /src/app/state/dialog/dialog.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { MatDialog } from '@angular/material'; 3 | import { CloseDialogs, OpenDialog } from '@app/state/dialog/dialog.actions'; 4 | import { Actions } from '@ngrx/effects'; 5 | import { provideMockActions } from '@ngrx/effects/testing'; 6 | import { cold, hot } from 'jest-marbles'; 7 | import { Observable, Subject } from 'rxjs'; 8 | import { DialogEffects } from './dialog.effects'; 9 | 10 | describe('DialogEffects', () => { 11 | let actions: Observable; 12 | let effects: DialogEffects; 13 | let matDialog: MatDialog; 14 | 15 | beforeEach(() => { 16 | TestBed.configureTestingModule({ 17 | providers: [ 18 | DialogEffects, 19 | { 20 | provide: MatDialog, 21 | useValue: { 22 | closeAll: jest.fn(), 23 | open: jest.fn() 24 | } 25 | }, 26 | provideMockActions(() => actions) 27 | ] 28 | }); 29 | 30 | actions = TestBed.get(Actions); 31 | effects = TestBed.get(DialogEffects); 32 | matDialog = TestBed.get(MatDialog); 33 | }); 34 | 35 | it('should be created', () => { 36 | expect(effects).toBeTruthy(); 37 | }); 38 | 39 | describe('close', () => { 40 | it('should invoke the closeAll method on MatDialog', () => { 41 | const action = new CloseDialogs(); 42 | const spy = jest.spyOn(matDialog, 'closeAll'); 43 | 44 | actions = hot('-a', { a: action }); 45 | 46 | effects.close.subscribe(() => { 47 | expect(spy).toHaveBeenCalled(); 48 | }); 49 | }); 50 | }); 51 | 52 | describe('open', () => { 53 | it('should', () => { 54 | const action = new OpenDialog(null); 55 | const outcome = new CloseDialogs(); 56 | const backdropClick = new Subject(); 57 | const spy = jest.spyOn(matDialog, 'open').mockReturnValue({ 58 | backdropClick 59 | }); 60 | 61 | actions = hot('-a', { a: action }); 62 | const expected = cold('-a', { a: outcome }); 63 | 64 | effects.open.subscribe(() => { 65 | expect(spy).toHaveBeenCalled(); 66 | backdropClick.next(null); 67 | expect(effects.open).toBeObservable(expected); 68 | }); 69 | }); 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /src/app/state/index.ts: -------------------------------------------------------------------------------- 1 | import { environment } from '@env/environment'; 2 | import { 3 | ActionReducerMap, 4 | createFeatureSelector, 5 | createSelector, 6 | MetaReducer 7 | } from '@ngrx/store'; 8 | import { 9 | reducer as dialogReducer, 10 | State as DialogState 11 | } from './dialog/dialog.reducer'; 12 | import { 13 | getZoom, 14 | reducer as mapReducer, 15 | State as MapState 16 | } from './map/map.reducer'; 17 | import { 18 | adapter as resortAdapter, 19 | getError, 20 | getLoading, 21 | getSearchResults, 22 | getSelectedResortId, 23 | reducer as resortReducer, 24 | State as ResortState 25 | } from './resort/resort.reducer'; 26 | import { 27 | getOpened, 28 | reducer as sidenavReducer, 29 | State as SidenavState 30 | } from './sidenav/sidenav.reducer'; 31 | 32 | export enum Features { 33 | dialog = 'dialog', 34 | map = 'map', 35 | sidenav = 'sidenav', 36 | resort = 'resort' 37 | } 38 | 39 | export interface State { 40 | [Features.dialog]: DialogState; 41 | [Features.map]: MapState; 42 | [Features.sidenav]: SidenavState; 43 | [Features.resort]: ResortState; 44 | } 45 | 46 | export const reducers: ActionReducerMap = { 47 | dialog: dialogReducer, 48 | map: mapReducer, 49 | sidenav: sidenavReducer, 50 | resort: resortReducer 51 | }; 52 | 53 | export const metaReducers: MetaReducer[] = !environment.production 54 | ? [] 55 | : []; 56 | 57 | export const mapState = createFeatureSelector(Features.map); 58 | export const mapZoom = createSelector(mapState, getZoom); 59 | 60 | export const sidenavState = createFeatureSelector( 61 | Features.sidenav 62 | ); 63 | export const sidenavIsOpen = createSelector(sidenavState, getOpened); 64 | 65 | export const resortState = createFeatureSelector(Features.resort); 66 | export const { 67 | selectIds: resortIds, 68 | selectEntities: resortEntities, 69 | selectAll: resorts, 70 | selectTotal: totalResorts 71 | } = resortAdapter.getSelectors(resortState); 72 | export const resortError = createSelector(resortState, getError); 73 | export const resortIsLoading = createSelector(resortState, getLoading); 74 | export const searchResults = createSelector(resortState, getSearchResults); 75 | export const selectedResortId = createSelector( 76 | resortState, 77 | getSelectedResortId 78 | ); 79 | export const selectedResort = createSelector( 80 | resorts, 81 | selectedResortId, 82 | (entities, id) => id && entities.find(resort => resort.id === id) 83 | ); 84 | -------------------------------------------------------------------------------- /src/app/containers/dashboard/dashboard.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { DashboardComponent } from '@app/containers/dashboard/dashboard.component'; 3 | import { generateResort } from '@app/models/resort.model'; 4 | import { SharedModule } from '@app/shared/shared.module'; 5 | import { reducers, State } from '@app/state'; 6 | import { Store, StoreModule } from '@ngrx/store'; 7 | import { NguiMapModule } from '@ngui/map'; 8 | import { cold } from 'jest-marbles'; 9 | 10 | describe('DashboardComponent', () => { 11 | let component: DashboardComponent; 12 | let fixture: ComponentFixture; 13 | let store: Store; 14 | 15 | beforeEach(async(() => { 16 | TestBed.configureTestingModule({ 17 | declarations: [DashboardComponent], 18 | imports: [NguiMapModule, StoreModule.forRoot(reducers), SharedModule] 19 | }).compileComponents(); 20 | })); 21 | 22 | beforeEach(() => { 23 | fixture = TestBed.createComponent(DashboardComponent); 24 | component = fixture.componentInstance; 25 | fixture.detectChanges(); 26 | store = TestBed.get(Store); 27 | }); 28 | 29 | it('should create', () => { 30 | expect(component).toBeTruthy(); 31 | }); 32 | 33 | describe('ngOnInit', () => { 34 | it('should declare the mapZoom observable property', () => { 35 | const zoom = 5; 36 | const select = cold('-a', { a: zoom }); 37 | const spy = jest.spyOn(store, 'pipe').mockReturnValue(select); 38 | 39 | component.ngOnInit(); 40 | 41 | expect(spy).toHaveBeenCalled(); 42 | expect(component.mapZoom).toBeObservable(select); 43 | 44 | component.mapZoom.subscribe(value => { 45 | expect(value).toBe(zoom); 46 | }); 47 | }); 48 | 49 | it('should declare the resorts observable property', () => { 50 | const resorts = [generateResort()]; 51 | const select = cold('-a', { a: resorts }); 52 | const spy = jest.spyOn(store, 'pipe').mockReturnValue(select); 53 | 54 | component.ngOnInit(); 55 | 56 | expect(spy).toHaveBeenCalled(); 57 | expect(component.resorts).toBeObservable(select); 58 | 59 | component.resorts.subscribe(value => { 60 | expect(value).toBe(resorts); 61 | }); 62 | }); 63 | 64 | it('should declare the selectedResort observable property', () => { 65 | const resort = generateResort(); 66 | const select = cold('-a', { a: resort }); 67 | const spy = jest.spyOn(store, 'pipe').mockReturnValue(select); 68 | 69 | component.ngOnInit(); 70 | 71 | expect(spy).toHaveBeenCalled(); 72 | expect(component.selectedResort).toBeObservable(select); 73 | 74 | component.selectedResort.subscribe(value => { 75 | expect(value).toBe(resort); 76 | }); 77 | }); 78 | }); 79 | }); 80 | -------------------------------------------------------------------------------- /src/app/shared/components/resort-autocomplete/resort-autocomplete.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { CommonModule } from '@angular/common'; 2 | import { HttpClientTestingModule } from '@angular/common/http/testing'; 3 | import { 4 | async, 5 | ComponentFixture, 6 | fakeAsync, 7 | TestBed, 8 | tick 9 | } from '@angular/core/testing'; 10 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 11 | import { NoopAnimationsModule } from '@angular/platform-browser/animations'; 12 | import { MaterialModule } from '@app/material.module'; 13 | import { generateResort } from '@app/models/resort.model'; 14 | import { reducers, State } from '@app/state/index'; 15 | import { SearchResorts } from '@app/state/resort/resort.actions'; 16 | import { Store, StoreModule } from '@ngrx/store'; 17 | import { cold } from 'jest-marbles'; 18 | import { ResortAutocompleteComponent } from './resort-autocomplete.component'; 19 | 20 | describe('ResortAutocompleteComponent', () => { 21 | let component: ResortAutocompleteComponent; 22 | let fixture: ComponentFixture; 23 | let store: Store; 24 | 25 | const resort = generateResort(); 26 | 27 | beforeEach(async(() => { 28 | TestBed.configureTestingModule({ 29 | declarations: [ResortAutocompleteComponent], 30 | imports: [ 31 | CommonModule, 32 | FormsModule, 33 | HttpClientTestingModule, 34 | MaterialModule, 35 | NoopAnimationsModule, 36 | ReactiveFormsModule, 37 | StoreModule.forRoot(reducers) 38 | ] 39 | }).compileComponents(); 40 | })); 41 | 42 | beforeEach(() => { 43 | fixture = TestBed.createComponent(ResortAutocompleteComponent); 44 | component = fixture.componentInstance; 45 | fixture.detectChanges(); 46 | store = TestBed.get(Store); 47 | }); 48 | 49 | it('should create', () => { 50 | expect(component).toBeTruthy(); 51 | }); 52 | 53 | describe('ngOnInit', () => { 54 | it('should declare the resorts observable property', () => { 55 | const resorts = [resort]; 56 | const select = cold('-a', { a: resorts }); 57 | const spy = jest.spyOn(store, 'pipe').mockReturnValue(select); 58 | 59 | component.ngOnInit(); 60 | 61 | expect(spy).toHaveBeenCalled(); 62 | expect(component.resorts).toBeObservable(select); 63 | 64 | component.resorts.subscribe(value => { 65 | expect(value).toBe(resorts); 66 | }); 67 | }); 68 | 69 | it( 70 | 'should dispatch the SearchResults action when the search form control value changes', 71 | fakeAsync(() => { 72 | const q = 'Testing'; 73 | const action = new SearchResorts(q); 74 | const spy = jest.spyOn(store, 'dispatch'); 75 | 76 | component.searchFormControl.setValue(q); 77 | tick(500); 78 | 79 | expect(spy).toHaveBeenCalledWith(action); 80 | }) 81 | ); 82 | }); 83 | }); 84 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": ["node_modules/codelyzer"], 3 | "rules": { 4 | "arrow-return-shorthand": true, 5 | "callable-types": true, 6 | "class-name": true, 7 | "comment-format": [true, "check-space"], 8 | "curly": true, 9 | "deprecation": { 10 | "severity": "warn" 11 | }, 12 | "eofline": true, 13 | "forin": true, 14 | "import-blacklist": [true, "rxjs/Rx"], 15 | "import-spacing": true, 16 | "indent": [true, "spaces"], 17 | "interface-over-type-literal": true, 18 | "label-position": true, 19 | "max-line-length": [true, 140], 20 | "member-access": false, 21 | "member-ordering": [ 22 | true, 23 | { 24 | "order": [ 25 | "static-field", 26 | "instance-field", 27 | "static-method", 28 | "instance-method" 29 | ] 30 | } 31 | ], 32 | "no-arg": true, 33 | "no-bitwise": true, 34 | "no-console": [true, "debug", "info", "time", "timeEnd", "trace"], 35 | "no-construct": true, 36 | "no-debugger": true, 37 | "no-duplicate-super": true, 38 | "no-empty": false, 39 | "no-empty-interface": true, 40 | "no-eval": true, 41 | "no-inferrable-types": [true, "ignore-params"], 42 | "no-misused-new": true, 43 | "no-non-null-assertion": true, 44 | "no-shadowed-variable": true, 45 | "no-string-literal": false, 46 | "no-string-throw": true, 47 | "no-switch-case-fall-through": true, 48 | "no-trailing-whitespace": true, 49 | "no-unnecessary-initializer": true, 50 | "no-unused-expression": true, 51 | "no-use-before-declare": true, 52 | "no-var-keyword": true, 53 | "object-literal-sort-keys": false, 54 | "one-line": [ 55 | true, 56 | "check-open-brace", 57 | "check-catch", 58 | "check-else", 59 | "check-whitespace" 60 | ], 61 | "prefer-const": true, 62 | "quotemark": [true, "single"], 63 | "radix": true, 64 | "semicolon": [true, "always", "ignore-bound-class-methods"], 65 | "triple-equals": [true, "allow-null-check"], 66 | "typedef-whitespace": [ 67 | true, 68 | { 69 | "call-signature": "nospace", 70 | "index-signature": "nospace", 71 | "parameter": "nospace", 72 | "property-declaration": "nospace", 73 | "variable-declaration": "nospace" 74 | } 75 | ], 76 | "unified-signatures": true, 77 | "variable-name": false, 78 | "whitespace": [ 79 | true, 80 | "check-branch", 81 | "check-decl", 82 | "check-operator", 83 | "check-separator", 84 | "check-type" 85 | ], 86 | "no-output-on-prefix": true, 87 | "use-input-property-decorator": true, 88 | "use-output-property-decorator": true, 89 | "use-host-property-decorator": true, 90 | "no-input-rename": true, 91 | "no-output-rename": true, 92 | "use-life-cycle-interface": true, 93 | "use-pipe-transform-interface": true, 94 | "component-class-suffix": true, 95 | "directive-class-suffix": true 96 | } 97 | } 98 | -------------------------------------------------------------------------------- /src/app/state/resort/resort.reducer.spec.ts: -------------------------------------------------------------------------------- 1 | import { generateResort } from '@app/models/resort.model'; 2 | import { 3 | SearchResorts, 4 | SearchResortsFail, 5 | SearchResortsSuccess 6 | } from './resort.actions'; 7 | import { initialState, reducer } from './resort.reducer'; 8 | 9 | describe('Sidenav Reducer', () => { 10 | describe('unknown action', () => { 11 | it('should return the initial state', () => { 12 | const action = { type: 'NOOP' } as any; 13 | const result = reducer(initialState, action); 14 | 15 | expect(result).toBe(initialState); 16 | }); 17 | }); 18 | 19 | describe('SearchResorts', () => { 20 | const q = 'test'; 21 | 22 | it('should set the error property to null', () => { 23 | const action = new SearchResorts(q); 24 | const result = reducer(initialState, action); 25 | 26 | expect(result).toHaveProperty('error', null); 27 | }); 28 | 29 | it('should set the loading boolean to true', () => { 30 | const action = new SearchResorts(q); 31 | const result = reducer( 32 | { 33 | ...initialState, 34 | searchResults: [generateResort()] 35 | }, 36 | action 37 | ); 38 | 39 | expect(result).toHaveProperty('loading', true); 40 | }); 41 | 42 | it('should reset the searchResults array', () => { 43 | const action = new SearchResorts(q); 44 | const result = reducer(initialState, action); 45 | 46 | expect(result).toHaveProperty('searchResults', []); 47 | }); 48 | }); 49 | 50 | describe('SearchResortsFail', () => { 51 | const error = new Error('test'); 52 | 53 | it('should set the error property', () => { 54 | const action = new SearchResortsFail(error); 55 | const result = reducer(initialState, action); 56 | 57 | expect(result).toHaveProperty('error', error); 58 | }); 59 | 60 | it('should set the loading boolean to false', () => { 61 | const action = new SearchResortsFail(error); 62 | const result = reducer(initialState, action); 63 | 64 | expect(result).toHaveProperty('loading', false); 65 | }); 66 | }); 67 | 68 | describe('SearchResortsSuccess', () => { 69 | const resort = generateResort(); 70 | 71 | it('should set the error property to null', () => { 72 | const action = new SearchResortsSuccess([resort]); 73 | const result = reducer( 74 | { 75 | ...initialState, 76 | error: new Error('test') 77 | }, 78 | action 79 | ); 80 | 81 | expect(result).toHaveProperty('error', null); 82 | }); 83 | 84 | it('should set the loading boolean to false', () => { 85 | const action = new SearchResortsSuccess([resort]); 86 | const result = reducer(initialState, action); 87 | 88 | expect(result).toHaveProperty('loading', false); 89 | }); 90 | 91 | it('should reset the searchResults array', () => { 92 | const action = new SearchResortsSuccess([resort]); 93 | const result = reducer(initialState, action); 94 | 95 | expect(result).toHaveProperty('searchResults', [resort]); 96 | }); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /projects/simple-store/src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * This file includes polyfills needed by Angular and is loaded before the app. 3 | * You can add your own extra polyfills to this file. 4 | * 5 | * This file is divided into 2 sections: 6 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 7 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 8 | * file. 9 | * 10 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 11 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 12 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 13 | * 14 | * Learn more in https://angular.io/docs/ts/latest/guide/browser-support.html 15 | */ 16 | 17 | /*************************************************************************************************** 18 | * BROWSER POLYFILLS 19 | */ 20 | 21 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 22 | // import 'core-js/es6/symbol'; 23 | // import 'core-js/es6/object'; 24 | // import 'core-js/es6/function'; 25 | // import 'core-js/es6/parse-int'; 26 | // import 'core-js/es6/parse-float'; 27 | // import 'core-js/es6/number'; 28 | // import 'core-js/es6/math'; 29 | // import 'core-js/es6/string'; 30 | // import 'core-js/es6/date'; 31 | // import 'core-js/es6/array'; 32 | // import 'core-js/es6/regexp'; 33 | // import 'core-js/es6/map'; 34 | // import 'core-js/es6/weak-map'; 35 | // import 'core-js/es6/set'; 36 | 37 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 38 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 39 | 40 | /** IE10 and IE11 requires the following for the Reflect API. */ 41 | // import 'core-js/es6/reflect'; 42 | 43 | 44 | /** Evergreen browsers require these. **/ 45 | // Used for reflect-metadata in JIT. If you use AOT (and only Angular decorators), you can remove. 46 | import 'core-js/es7/reflect'; 47 | 48 | 49 | /** 50 | * Web Animations `@angular/platform-browser/animations` 51 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 52 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 53 | **/ 54 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 55 | 56 | /** 57 | * By default, zone.js will patch all possible macroTask and DomEvents 58 | * user can disable parts of macroTask/DomEvents patch by setting following flags 59 | */ 60 | 61 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 62 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 63 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 64 | 65 | /* 66 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 67 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 68 | */ 69 | // (window as any).__Zone_enable_cross_context_check = true; 70 | 71 | /*************************************************************************************************** 72 | * Zone JS is required by default for Angular itself. 73 | */ 74 | import 'zone.js/dist/zone'; // Included with Angular CLI. 75 | 76 | 77 | 78 | /*************************************************************************************************** 79 | * APPLICATION IMPORTS 80 | */ 81 | -------------------------------------------------------------------------------- /src/app/state/resort/resort.effects.spec.ts: -------------------------------------------------------------------------------- 1 | import { TestBed } from '@angular/core/testing'; 2 | import { ResortService } from '@app/core/services/resort.service'; 3 | import { generateResort } from '@app/models/resort.model'; 4 | import { CloseDialogs } from '@app/state/dialog/dialog.actions'; 5 | import { SetMapZoom } from '@app/state/map/map.actions'; 6 | import { Actions } from '@ngrx/effects'; 7 | import { provideMockActions } from '@ngrx/effects/testing'; 8 | import { cold, hot } from 'jest-marbles'; 9 | import { Observable } from 'rxjs'; 10 | import { 11 | SearchResorts, 12 | SearchResortsFail, 13 | SearchResortsSuccess, 14 | SelectResort 15 | } from './resort.actions'; 16 | import { ResortEffects } from './resort.effects'; 17 | 18 | describe('ResortEffects', () => { 19 | let actions: Observable; 20 | let effects: ResortEffects; 21 | let resortService: ResortService; 22 | 23 | beforeEach(() => { 24 | TestBed.configureTestingModule({ 25 | providers: [ 26 | ResortEffects, 27 | { 28 | provide: ResortService, 29 | useValue: {} 30 | }, 31 | provideMockActions(() => actions) 32 | ] 33 | }); 34 | 35 | actions = TestBed.get(Actions); 36 | effects = TestBed.get(ResortEffects); 37 | resortService = TestBed.get(ResortService); 38 | }); 39 | 40 | it('should be created', () => { 41 | expect(effects).toBeTruthy(); 42 | }); 43 | 44 | describe('closeDialogOnSelect', () => { 45 | it('should dispatch the CloseDialogs action', () => { 46 | const resort = generateResort(); 47 | const action = new SelectResort(resort); 48 | const outcome = new CloseDialogs(); 49 | 50 | actions = hot('-a', { a: action }); 51 | const expected = cold('-a', { a: outcome }); 52 | 53 | expect(effects.closeDialogOnSelect).toBeObservable(expected); 54 | }); 55 | }); 56 | 57 | describe('search', () => { 58 | it('should dispatch the SearchResortsSuccess action on success', () => { 59 | const q = 'Testing'; 60 | const resorts = [generateResort()]; 61 | const action = new SearchResorts(q); 62 | const outcome = new SearchResortsSuccess(resorts); 63 | 64 | actions = hot('-a', { a: action }); 65 | const response = cold('-a|', { a: resorts }); 66 | const expected = cold('--b', { b: outcome }); 67 | resortService.search = jest.fn(() => response); 68 | 69 | expect(effects.search).toBeObservable(expected); 70 | }); 71 | 72 | it('should dispatch the SearchResortsFail action on failure', () => { 73 | const q = 'Testing'; 74 | const error = new Error('Test Error'); 75 | const action = new SearchResorts(q); 76 | const outcome = new SearchResortsFail(error); 77 | 78 | actions = hot('-a', { a: action }); 79 | const response = cold('-#', {}, error); 80 | const expected = cold('--b', { b: outcome }); 81 | resortService.search = jest.fn(() => response); 82 | 83 | expect(effects.search).toBeObservable(expected); 84 | }); 85 | }); 86 | 87 | describe('setMapZoom', () => { 88 | it('should set thet map zoom', () => { 89 | const resort = generateResort(); 90 | const action = new SelectResort(resort); 91 | const outcome = new SetMapZoom(12); 92 | 93 | actions = hot('-a', { a: action }); 94 | const expected = cold('-a', { a: outcome }); 95 | 96 | expect(effects.setMapZoom).toBeObservable(expected); 97 | }); 98 | }); 99 | }); 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngrx-course", 3 | "version": "1.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "concurrently --prefix-colors white.bgBlue,white.bgRed --names angular,json-server --kill-others \"npm run serve:proxy\" \"npm run json-server\"", 7 | "serve": "ng serve --source-map", 8 | "serve:proxy": "ng serve --source-map --proxy-config proxy.conf.json", 9 | "build": "ng build", 10 | "build:ci": "ng build --prod --aot --no-progress", 11 | "start:simple-store": "concurrently --prefix-colors white.bgBlue,white.bgRed --names angular,json-server --kill-others \"npm run serve:simple-store:proxy\" \"npm run json-server\"", 12 | "serve:simple-store:proxy": "ng serve simple-store --source-map --proxy-config proxy.conf.json", 13 | "build:simple-store": "ng build simple-store --prod", 14 | "json-server": "json-server --watch data/db.json --routes data/routes.json", 15 | "test": "jest", 16 | "test:watch": "jest --watch", 17 | "test:ci": "jest --runInBand", 18 | "test:coverage": "jest --coverage", 19 | "lint": "ng lint", 20 | "e2e": "ng e2e" 21 | }, 22 | "private": true, 23 | "dependencies": { 24 | "@angular-devkit/core": "^0.6.8", 25 | "@angular-devkit/schematics": "^0.6.8", 26 | "@angular/animations": "^6.0.0", 27 | "@angular/cdk": "^6.3.3", 28 | "@angular/common": "^6.0.0", 29 | "@angular/compiler": "^6.0.0", 30 | "@angular/core": "^6.0.0", 31 | "@angular/flex-layout": "^6.0.0-beta.16", 32 | "@angular/forms": "^6.0.0", 33 | "@angular/http": "^6.0.0", 34 | "@angular/material": "^6.4.0", 35 | "@angular/platform-browser": "^6.0.0", 36 | "@angular/platform-browser-dynamic": "^6.0.0", 37 | "@angular/router": "^6.0.0", 38 | "@angular/service-worker": "^6.0.9", 39 | "@ngrx/effects": "^6.0.1", 40 | "@ngrx/entity": "^6.0.1", 41 | "@ngrx/router-store": "^6.0.1", 42 | "@ngrx/schematics": "^6.0.1", 43 | "@ngrx/store": "^6.0.1", 44 | "@ngrx/store-devtools": "^6.0.1", 45 | "@ngui/map": "^0.30.3", 46 | "angularfire2": "^5.0.0-rc.11", 47 | "core-js": "^2.5.4", 48 | "firebase": "^5.3.0", 49 | "rxjs": "^6.0.0", 50 | "zone.js": "^0.8.26" 51 | }, 52 | "devDependencies": { 53 | "@angular-devkit/build-angular": "~0.6.0", 54 | "@angular/cli": "~6.0.0", 55 | "@angular/compiler-cli": "^6.0.0", 56 | "@angular/language-service": "^6.0.0", 57 | "@types/googlemaps": "^3.30.11", 58 | "@types/jest": "^23.3.1", 59 | "@types/node": "~8.9.4", 60 | "codelyzer": "~4.2.1", 61 | "concurrently": "^3.6.1", 62 | "jest": "^22.4.4", 63 | "jest-marbles": "^2.0.0", 64 | "jest-preset-angular": "^5.2.3", 65 | "json-server": "^0.14.0", 66 | "protractor": "~5.3.0", 67 | "ts-node": "~5.0.1", 68 | "tslint": "~5.9.1", 69 | "typescript": "~2.7.2" 70 | }, 71 | "jest": { 72 | "collectCoverageFrom": [ 73 | "src/**/*.ts", 74 | "!src/**/*.module.ts", 75 | "!src/main.ts", 76 | "!src/polyfills.ts", 77 | "!src/environments/*.ts", 78 | "projects/simple-store/src/lib/**/*.ts", 79 | "!projects/simple-store/src/lib/**/*.module.ts", 80 | "!projects/simple-store/src/lib/index.ts", 81 | "!/setup-jest.ts", 82 | "!/jest-global-mocks.ts" 83 | ], 84 | "coverageReporters": [ 85 | "html", 86 | "text" 87 | ], 88 | "modulePathIgnorePatterns": [ 89 | "/dist" 90 | ], 91 | "moduleNameMapper": { 92 | "^@app/(.*)": "/src/app/$1", 93 | "^@env/(.*)": "/src/environments/$1" 94 | }, 95 | "preset": "jest-preset-angular", 96 | "setupTestFrameworkScriptFile": "/setup-jest.ts", 97 | "testMatch": [ 98 | "**/+(*.)+(spec|test).+(ts)?(x)" 99 | ], 100 | "testURL": "http://localhost" 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/app/core/shell/shell.component.spec.ts: -------------------------------------------------------------------------------- 1 | import { async, ComponentFixture, TestBed } from '@angular/core/testing'; 2 | import { By } from '@angular/platform-browser'; 3 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 4 | import { RouterTestingModule } from '@angular/router/testing'; 5 | import { SearchDialogComponent } from '@app/containers/search-dialog/search-dialog.component'; 6 | import { CoreModule } from '@app/core/core.module'; 7 | import { ShellComponent } from '@app/core/shell/shell.component'; 8 | import { reducers, State } from '@app/state'; 9 | import { OpenDialog } from '@app/state/dialog/dialog.actions'; 10 | import { HideSidenav, ShowSidenav } from '@app/state/sidenav/sidenav.actions'; 11 | import { Store, StoreModule } from '@ngrx/store'; 12 | import { cold } from 'jest-marbles'; 13 | 14 | describe('ShellComponent', () => { 15 | let component: ShellComponent; 16 | let fixture: ComponentFixture; 17 | let store: Store; 18 | 19 | beforeEach(async(() => { 20 | TestBed.configureTestingModule({ 21 | imports: [ 22 | RouterTestingModule, 23 | BrowserAnimationsModule, 24 | CoreModule, 25 | StoreModule.forRoot(reducers) 26 | ] 27 | }).compileComponents(); 28 | })); 29 | 30 | beforeEach(() => { 31 | fixture = TestBed.createComponent(ShellComponent); 32 | component = fixture.componentInstance; 33 | fixture.detectChanges(); 34 | store = TestBed.get(Store); 35 | }); 36 | 37 | it('should create', () => { 38 | expect(component).toBeTruthy(); 39 | }); 40 | 41 | describe('ngOnInit', () => { 42 | it('should declare the opened observable property', () => { 43 | const opened = false; 44 | const select = cold('-a', { a: opened }); 45 | const spy = jest.spyOn(store, 'pipe').mockReturnValue(select); 46 | 47 | component.ngOnInit(); 48 | 49 | expect(spy).toHaveBeenCalled(); 50 | expect(component.opened).toBeObservable(select); 51 | 52 | component.opened.subscribe(value => { 53 | expect(value).toBe(opened); 54 | }); 55 | }); 56 | }); 57 | 58 | describe('closeSidenav', () => { 59 | it('should dispatch the HideSidenav action', () => { 60 | const action = new HideSidenav(); 61 | const spy = jest.spyOn(store, 'dispatch'); 62 | 63 | component.closeSidenav(); 64 | 65 | expect(spy).toHaveBeenCalledWith(action); 66 | }); 67 | }); 68 | 69 | describe('openDialog', () => { 70 | it('should dispatch the OpenDialog action', () => { 71 | const action = new OpenDialog(SearchDialogComponent, { 72 | width: '320px' 73 | }); 74 | const spy = jest.spyOn(store, 'dispatch'); 75 | 76 | const debugElement = fixture.debugElement.query(By.css('.search')); 77 | debugElement.triggerEventHandler('click', null); 78 | 79 | expect(spy).toHaveBeenCalledWith(action); 80 | }); 81 | }); 82 | 83 | describe('openSidenav', () => { 84 | it('should dispatch the ShowSidenav action', () => { 85 | const action = new ShowSidenav(); 86 | const spy = jest.spyOn(store, 'dispatch'); 87 | 88 | component.openSidenav(); 89 | 90 | expect(spy).toHaveBeenCalledWith(action); 91 | }); 92 | }); 93 | 94 | describe('toggleSidenav', () => { 95 | it('should dispatch the ShowSidenav action first', () => { 96 | const action = new ShowSidenav(); 97 | const spy = jest.spyOn(store, 'dispatch'); 98 | 99 | const debugElement = fixture.debugElement.query(By.css('button')); 100 | debugElement.triggerEventHandler('click', null); 101 | 102 | expect(spy).toHaveBeenCalledWith(action); 103 | }); 104 | 105 | it('should dispatch the HideSidenav action second', () => { 106 | const action = new HideSidenav(); 107 | const spy = jest.spyOn(store, 'dispatch'); 108 | 109 | const debugElement = fixture.debugElement.query(By.css('button')); 110 | debugElement.triggerEventHandler('click', null); 111 | debugElement.triggerEventHandler('click', null); 112 | 113 | expect(spy).toHaveBeenCalledWith(action); 114 | }); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "course": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "styleext": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist/course", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": ["src/favicon.ico", "src/assets"], 26 | "styles": ["src/styles.scss"], 27 | "scripts": [] 28 | }, 29 | "configurations": { 30 | "production": { 31 | "fileReplacements": [ 32 | { 33 | "replace": "src/environments/environment.ts", 34 | "with": "src/environments/environment.prod.ts" 35 | } 36 | ], 37 | "optimization": true, 38 | "outputHashing": "all", 39 | "sourceMap": false, 40 | "extractCss": true, 41 | "namedChunks": false, 42 | "aot": true, 43 | "extractLicenses": true, 44 | "vendorChunk": false, 45 | "buildOptimizer": true 46 | } 47 | } 48 | }, 49 | "serve": { 50 | "builder": "@angular-devkit/build-angular:dev-server", 51 | "options": { 52 | "browserTarget": "course:build" 53 | }, 54 | "configurations": { 55 | "production": { 56 | "browserTarget": "course:build:production" 57 | } 58 | } 59 | }, 60 | "extract-i18n": { 61 | "builder": "@angular-devkit/build-angular:extract-i18n", 62 | "options": { 63 | "browserTarget": "course:build" 64 | } 65 | }, 66 | "test": { 67 | "builder": "@angular-devkit/build-angular:karma", 68 | "options": { 69 | "main": "src/test.ts", 70 | "polyfills": "src/polyfills.ts", 71 | "tsConfig": "src/tsconfig.spec.json", 72 | "karmaConfig": "src/karma.conf.js", 73 | "styles": ["styles.scss"], 74 | "scripts": [], 75 | "assets": ["src/favicon.ico", "src/assets"] 76 | } 77 | }, 78 | "lint": { 79 | "builder": "@angular-devkit/build-angular:tslint", 80 | "options": { 81 | "tsConfig": ["src/tsconfig.app.json", "src/tsconfig.spec.json"], 82 | "exclude": ["**/node_modules/**"] 83 | } 84 | } 85 | } 86 | }, 87 | "course-e2e": { 88 | "root": "e2e/", 89 | "projectType": "application", 90 | "architect": { 91 | "e2e": { 92 | "builder": "@angular-devkit/build-angular:protractor", 93 | "options": { 94 | "protractorConfig": "e2e/protractor.conf.js", 95 | "devServerTarget": "course:serve" 96 | } 97 | }, 98 | "lint": { 99 | "builder": "@angular-devkit/build-angular:tslint", 100 | "options": { 101 | "tsConfig": "e2e/tsconfig.e2e.json", 102 | "exclude": ["**/node_modules/**"] 103 | } 104 | } 105 | } 106 | }, 107 | "simple-store": { 108 | "root": "projects/simple-store/", 109 | "sourceRoot": "projects/simple-store/src", 110 | "projectType": "application", 111 | "prefix": "app", 112 | "schematics": { 113 | "@schematics/angular:component": { 114 | "styleext": "scss" 115 | } 116 | }, 117 | "architect": { 118 | "build": { 119 | "builder": "@angular-devkit/build-angular:browser", 120 | "options": { 121 | "outputPath": "dist/simple-store", 122 | "index": "projects/simple-store/src/index.html", 123 | "main": "projects/simple-store/src/main.ts", 124 | "polyfills": "projects/simple-store/src/polyfills.ts", 125 | "tsConfig": "projects/simple-store/tsconfig.app.json", 126 | "assets": [ 127 | "projects/simple-store/src/favicon.ico", 128 | "projects/simple-store/src/assets" 129 | ], 130 | "styles": ["projects/simple-store/src/styles.scss"], 131 | "scripts": [] 132 | }, 133 | "configurations": { 134 | "production": { 135 | "fileReplacements": [ 136 | { 137 | "replace": 138 | "projects/simple-store/src/environments/environment.ts", 139 | "with": 140 | "projects/simple-store/src/environments/environment.prod.ts" 141 | } 142 | ], 143 | "optimization": true, 144 | "outputHashing": "all", 145 | "sourceMap": false, 146 | "extractCss": true, 147 | "namedChunks": false, 148 | "aot": true, 149 | "extractLicenses": true, 150 | "vendorChunk": false, 151 | "buildOptimizer": true 152 | } 153 | } 154 | }, 155 | "serve": { 156 | "builder": "@angular-devkit/build-angular:dev-server", 157 | "options": { 158 | "browserTarget": "simple-store:build" 159 | }, 160 | "configurations": { 161 | "production": { 162 | "browserTarget": "simple-store:build:production" 163 | } 164 | } 165 | }, 166 | "extract-i18n": { 167 | "builder": "@angular-devkit/build-angular:extract-i18n", 168 | "options": { 169 | "browserTarget": "simple-store:build" 170 | } 171 | }, 172 | "test": { 173 | "builder": "@angular-devkit/build-angular:karma", 174 | "options": { 175 | "main": "projects/simple-store/src/test.ts", 176 | "polyfills": "projects/simple-store/src/polyfills.ts", 177 | "tsConfig": "projects/simple-store/tsconfig.spec.json", 178 | "karmaConfig": "projects/simple-store/karma.conf.js", 179 | "styles": ["projects/simple-store/src/styles.scss"], 180 | "scripts": [], 181 | "assets": [ 182 | "projects/simple-store/src/favicon.ico", 183 | "projects/simple-store/src/assets" 184 | ] 185 | } 186 | }, 187 | "lint": { 188 | "builder": "@angular-devkit/build-angular:tslint", 189 | "options": { 190 | "tsConfig": [ 191 | "projects/simple-store/tsconfig.app.json", 192 | "projects/simple-store/tsconfig.spec.json" 193 | ], 194 | "exclude": ["**/node_modules/**"] 195 | } 196 | } 197 | } 198 | }, 199 | "simple-store-e2e": { 200 | "root": "projects/simple-store-e2e/", 201 | "projectType": "application", 202 | "architect": { 203 | "e2e": { 204 | "builder": "@angular-devkit/build-angular:protractor", 205 | "options": { 206 | "protractorConfig": "projects/simple-store-e2e/protractor.conf.js", 207 | "devServerTarget": "simple-store:serve" 208 | }, 209 | "configurations": { 210 | "production": { 211 | "devServerTarget": "simple-store:serve:production" 212 | } 213 | } 214 | }, 215 | "lint": { 216 | "builder": "@angular-devkit/build-angular:tslint", 217 | "options": { 218 | "tsConfig": "projects/simple-store-e2e/tsconfig.e2e.json", 219 | "exclude": ["**/node_modules/**"] 220 | } 221 | } 222 | } 223 | } 224 | }, 225 | "defaultProject": "course", 226 | "cli": { 227 | "defaultCollection": "@ngrx/schematics" 228 | } 229 | } 230 | --------------------------------------------------------------------------------