├── 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 |
2 |
3 |
Favorites
4 |
5 |
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 |
--------------------------------------------------------------------------------