├── src ├── assets │ └── .gitkeep ├── app │ ├── app.component.scss │ ├── views │ │ ├── commits.view.ts │ │ ├── commits.view-with-fetch.html │ │ ├── commits.view.html │ │ ├── letView.container.ts │ │ ├── counter.container.ts │ │ ├── commits.view-with-fetch.ts │ │ ├── let.view.html │ │ ├── let.view.ts │ │ ├── counter.view.ts │ │ ├── commits-normal.view.ts │ │ ├── counter.view.html │ │ └── letView2.container.ts │ ├── components │ │ ├── commitList │ │ │ ├── commitList.component.scss │ │ │ ├── commit.ts │ │ │ ├── commitList.component.ts │ │ │ └── commitList.component.html │ │ ├── user │ │ │ └── user.component.ts │ │ ├── card │ │ │ ├── card.component.ts │ │ │ └── card.component.spec.ts │ │ ├── let │ │ │ └── let.component.ts │ │ └── counter │ │ │ └── counter.component.ts │ ├── app.component.ts │ ├── state │ │ └── index.ts │ ├── app.component.html │ ├── container │ │ └── commits.container.ts │ ├── app.component.spec.ts.old │ ├── api │ │ └── commits │ │ │ └── commits.service.ts │ └── app.module.ts ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── typings.d.ts ├── styles.scss ├── tsconfig.app.json ├── tsconfig.spec.json ├── main.ts ├── index.html ├── test.ts ├── test │ ├── let.directive.spec.ts │ ├── params.directive.spec.ts │ └── fetch.directive.spec.ts └── polyfills.ts ├── lib ├── loading-spinner │ └── loading-spinner.component.ts ├── router │ ├── route.component.ts │ ├── params.directive.ts │ └── path.directive.ts ├── let │ ├── page.component.ts │ └── let.directive.ts ├── fetch │ ├── fetch.component.ts │ └── url.directive.ts └── connect │ ├── connect.component.ts │ └── redux.directive.ts ├── e2e ├── app.po.ts ├── tsconfig.e2e.json └── app.e2e-spec.ts ├── .editorconfig ├── tsconfig.json ├── .gitignore ├── protractor.conf.js ├── karma.conf.js ├── .angular-cli.json ├── package.json ├── tslint.json └── README.md /src/assets/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yanxch/fun-with-angular-directives/HEAD/src/favicon.ico -------------------------------------------------------------------------------- /src/typings.d.ts: -------------------------------------------------------------------------------- 1 | /* SystemJS module definition */ 2 | declare var module: NodeModule; 3 | interface NodeModule { 4 | id: string; 5 | } 6 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import "~@angular/material/prebuilt-themes/indigo-pink.css"; 3 | -------------------------------------------------------------------------------- /src/app/views/commits.view.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'commits-view', 5 | templateUrl: 'commits.view.html' 6 | }) 7 | export class CommitsView {} 8 | 9 | -------------------------------------------------------------------------------- /lib/loading-spinner/loading-spinner.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'loading-spinner', 5 | template: ` 6 | ...Loading... 7 | ` 8 | }) 9 | export class LoadingComponent {} -------------------------------------------------------------------------------- /src/app/views/commits.view-with-fetch.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /src/app/components/commitList/commitList.component.scss: -------------------------------------------------------------------------------- 1 | :host { 2 | overflow: hidden; 3 | } 4 | 5 | :host .message { 6 | white-space: pre-line; 7 | } 8 | 9 | :host .mat-list .mat-list-item { 10 | display: block; 11 | height: auto; 12 | } -------------------------------------------------------------------------------- /src/app/components/commitList/commit.ts: -------------------------------------------------------------------------------- 1 | export class Commit { 2 | 3 | constructor(public id: number, 4 | public repo: string, 5 | public createdBy: string, 6 | public message: string) { 7 | 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /e2e/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/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "baseUrl": "./", 6 | "module": "es2015", 7 | "types": [] 8 | }, 9 | "exclude": [ 10 | "test.ts", 11 | "**/*.spec.ts" 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /lib/router/route.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {Route} from '@angular/router'; 3 | 4 | @Component({ 5 | selector: 'Route', 6 | template: ` 7 | 8 | ` 9 | }) 10 | export class RouteComponent { 11 | config: Route; 12 | } -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component } from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'app-root', 5 | templateUrl: './app.component.html', 6 | styleUrls: ['./app.component.scss'] 7 | }) 8 | export class AppComponent { 9 | title = 'app'; 10 | user = 'mmalerba'; 11 | } 12 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/e2e", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "target": "es5", 8 | "types": [ 9 | "jasmine", 10 | "jasminewd2", 11 | "node" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/app/views/commits.view.html: -------------------------------------------------------------------------------- 1 | 2 | {{username}} 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/app/components/user/user.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy, Input} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'user-view', 5 | template: ` 6 |

Hallo {{name}}

7 | `, 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class UserView { 11 | @Input() name: string; 12 | } -------------------------------------------------------------------------------- /e2e/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('angular-starter 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 | -------------------------------------------------------------------------------- /lib/let/page.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'Page', 5 | template: ` 6 | 7 | `, 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class Page { 11 | // Maybe we can validate the directives in here? 12 | // Otherwise it just makes a better DSL 13 | } -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "baseUrl": "./", 6 | "module": "commonjs", 7 | "types": [ 8 | "jasmine", 9 | "node" 10 | ] 11 | }, 12 | "files": [ 13 | "test.ts" 14 | ], 15 | "include": [ 16 | "**/*.spec.ts", 17 | "**/*.d.ts" 18 | ] 19 | } 20 | -------------------------------------------------------------------------------- /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/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // The file contents for the current environment will overwrite these during build. 2 | // The build system defaults to the dev environment which uses `environment.ts`, but if you do 3 | // `ng build --env=prod` then `environment.prod.ts` will be used instead. 4 | // The list of which env maps to which file can be found in `.angular-cli.json`. 5 | 6 | export const environment = { 7 | production: false 8 | }; 9 | -------------------------------------------------------------------------------- /lib/fetch/fetch.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, TemplateRef, ViewContainerRef, Input, OnInit, OnDestroy, ComponentFactoryResolver, Directive} from '@angular/core'; 2 | import {Observable} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'Fetch', 6 | template: ` 7 | 8 | ` 9 | }) 10 | export class Fetch { 11 | // Maybe we can validate the directives in here? 12 | // Otherwise it just makes a better DSL 13 | } 14 | -------------------------------------------------------------------------------- /src/app/views/letView.container.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'let-view-container', 5 | template: ` 6 | 7 | 8 | `, 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class LetViewContainer { 12 | 13 | ngDoCheck() { 14 | console.log('LetViewContainer Check Stuff'); 15 | } 16 | } -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "outDir": "./dist/out-tsc", 5 | "sourceMap": true, 6 | "declaration": false, 7 | "moduleResolution": "node", 8 | "emitDecoratorMetadata": true, 9 | "experimentalDecorators": true, 10 | "target": "es5", 11 | "typeRoots": [ 12 | "node_modules/@types" 13 | ], 14 | "lib": [ 15 | "es2017", 16 | "dom" 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/app/views/counter.container.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {of} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'counter-container', 6 | template: ` 7 | 8 | `, 9 | changeDetection: ChangeDetectionStrategy.OnPush 10 | }) 11 | export class CounterContainer { 12 | 13 | counter$ = of(100); 14 | 15 | ngDoCheck() { 16 | console.log('CounterContainer Check Stuff'); 17 | } 18 | } -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | AngularStarter 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/app/components/card/card.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'card', 5 | template: ` 6 | 7 | 8 | 9 | Default Title 10 | 11 | 12 | 13 | Default Content 14 | 15 | 16 | ` 17 | }) 18 | export class CardComponent { 19 | 20 | } -------------------------------------------------------------------------------- /src/app/state/index.ts: -------------------------------------------------------------------------------- 1 | import {Action} from '@ngrx/store'; 2 | 3 | export const INCREMENT = 'INCREMENT'; 4 | export const DECREMENT = 'DECREMENT'; 5 | export const RESET = 'RESET'; 6 | 7 | const initialState = 0; 8 | 9 | export function counterReducer(state: number = initialState, action: Action) { 10 | switch (action.type) { 11 | case INCREMENT: 12 | return state + 1; 13 | 14 | case DECREMENT: 15 | return state - 1; 16 | 17 | case RESET: 18 | return 0; 19 | 20 | default: 21 | return state; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/app/components/let/let.component.ts: -------------------------------------------------------------------------------- 1 | import {OnInit, Input, Component} from '@angular/core'; 2 | 3 | 4 | @Component({ 5 | selector: 'let-component', 6 | template: ` 7 |

Tasks:

{{ tasks | json }} 8 |

Documents:

{{ documents | json }} 9 |

Loading:

{{ loading }} 10 | ` 11 | }) 12 | export class LetComponent { 13 | @Input() 14 | tasks: any; 15 | 16 | @Input() 17 | documents: any; 18 | 19 | @Input() 20 | loading: boolean; 21 | 22 | ngDoCheck() { 23 | console.log('LetComponent Check Stuff'); 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/views/commits.view-with-fetch.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input} from '@angular/core'; 2 | import {Commit} from '../components/commitList/commit'; 3 | import {toCommits} from '../api/commits/commits.service'; 4 | 5 | @Component({ 6 | selector: 'commits-view-fetch', 7 | templateUrl: 'commits.view-with-fetch.html' 8 | }) 9 | export class CommitsViewWithFetch { 10 | commitsUrl(username) { 11 | return `https://api.github.com/users/${username}/events`; 12 | } 13 | 14 | toCommits(response: any[]) { 15 | return toCommits(response); 16 | } 17 | } 18 | 19 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 |

TEST LOL

7 |
8 | 9 | 10 |

Hermann LOL

11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |

Child Route Test

20 |
21 |
22 | -------------------------------------------------------------------------------- /src/app/views/let.view.html: -------------------------------------------------------------------------------- 1 |
10 | 11 | 20 | 21 |
-------------------------------------------------------------------------------- /src/app/components/commitList/commitList.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, Input, OnInit, ChangeDetectionStrategy} from '@angular/core'; 2 | import {Commit} from './commit'; 3 | 4 | @Component({ 5 | selector: 'commit-list', 6 | templateUrl: './commitList.component.html', 7 | styleUrls: ['./commitList.component.scss'], 8 | changeDetection: ChangeDetectionStrategy.OnPush 9 | }) 10 | export class CommitListComponent implements OnInit { 11 | @Input() 12 | commits: Commit[]; 13 | 14 | ngOnInit() { 15 | console.log('CommitListComponent'); 16 | console.log(this.commits); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /lib/let/let.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, TemplateRef, ViewContainerRef, OnInit, Input} from '@angular/core'; 2 | 3 | export class LetContext { 4 | [key: string]: any; 5 | } 6 | 7 | @Directive({ 8 | selector: '[let]' 9 | }) 10 | export class LetDirective implements OnInit { 11 | 12 | private _context = new LetContext(); 13 | 14 | @Input() 15 | set letFrom(value: any) { 16 | Object.assign(this._context, value); 17 | } 18 | 19 | constructor(private template: TemplateRef, 20 | private viewContainer: ViewContainerRef) {} 21 | 22 | ngOnInit() { 23 | this.viewContainer.createEmbeddedView(this.template, this._context); 24 | } 25 | } -------------------------------------------------------------------------------- /src/app/views/let.view.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {of, Subject, BehaviorSubject} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'let-view', 6 | templateUrl: 'let.view.html', 7 | changeDetection: ChangeDetectionStrategy.OnPush 8 | }) 9 | export class LetView { 10 | _tasks$ = new BehaviorSubject('task1'); 11 | 12 | 13 | tasks$ = this._tasks$.asObservable(); 14 | 15 | documents$ = of(['document1', 'document2']); 16 | loading$ = of(true); 17 | 18 | 19 | constructor() { 20 | setTimeout(() => this._tasks$.next('task2'), 2000); 21 | } 22 | 23 | ngDoCheck() { 24 | console.log('LetView Check Stuff'); 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/components/commitList/commitList.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 |

4 | 5 | home 6 | {{commit.repo}} 7 | 8 |

9 |

10 | face 11 | {{commit.createdBy}} 12 |

13 |

14 | message 15 | {{commit.message}} 16 |

17 | 18 | 19 |
20 |
-------------------------------------------------------------------------------- /src/app/views/counter.view.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {AppState} from '../app.module'; 3 | import {DECREMENT, INCREMENT, RESET} from '../state'; 4 | 5 | @Component({ 6 | selector: 'counter-view', 7 | templateUrl: 'counter.view.html' 8 | }) 9 | export class CounterView { 10 | 11 | counterSelector(state: AppState) { 12 | return state.counter; 13 | } 14 | 15 | incrementActionCreator(payload) { 16 | return { 17 | type: INCREMENT 18 | }; 19 | } 20 | 21 | decrementActionCreator(payload) { 22 | return { 23 | type: DECREMENT 24 | }; 25 | } 26 | 27 | resetActionCreator(payload) { 28 | return { 29 | type: RESET 30 | }; 31 | } 32 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/connect/connect.component.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | 3 | 4 | /** 5 | 6 | 14 | 15 | 16 | 17 | */ 18 | 19 | @Component({ 20 | selector: 'Connect', 21 | template: ` 22 | 23 | ` 24 | }) 25 | export class ConnectComponent { 26 | 27 | } -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /dist-server 6 | /tmp 7 | /out-tsc 8 | 9 | # dependencies 10 | /node_modules 11 | 12 | # IDEs and editors 13 | /.idea 14 | .project 15 | .classpath 16 | .c9/ 17 | *.launch 18 | .settings/ 19 | *.sublime-workspace 20 | 21 | # IDE - VSCode 22 | .vscode/* 23 | !.vscode/settings.json 24 | !.vscode/tasks.json 25 | !.vscode/launch.json 26 | !.vscode/extensions.json 27 | 28 | # misc 29 | /.sass-cache 30 | /connect.lock 31 | /coverage 32 | /libpeerconnection.log 33 | npm-debug.log 34 | yarn-error.log 35 | testem.log 36 | /typings 37 | 38 | # e2e 39 | /e2e/*.js 40 | /e2e/*.map 41 | 42 | # System Files 43 | .DS_Store 44 | Thumbs.db 45 | -------------------------------------------------------------------------------- /src/app/views/commits-normal.view.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {ActivatedRoute, ParamMap} from '@angular/router'; 3 | import {filter, map, tap} from 'rxjs/operators'; 4 | import {Observable} from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'commits-normal-view', 8 | template: ` 9 | 11 | 12 | ` 13 | }) 14 | export class CommitsNormalView { 15 | 16 | username$: Observable; 17 | 18 | constructor(private route: ActivatedRoute) { 19 | this.username$ = this.route.paramMap 20 | .pipe( 21 | filter(p => p.has('username')), 22 | map(p => p.get('username')), 23 | tap(u => console.log(u)) 24 | ); 25 | } 26 | } -------------------------------------------------------------------------------- /src/app/views/counter.view.html: -------------------------------------------------------------------------------- 1 | 13 | 14 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /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 | './e2e/**/*.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: 'e2e/tsconfig.e2e.json' 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; 29 | -------------------------------------------------------------------------------- /src/app/views/letView2.container.ts: -------------------------------------------------------------------------------- 1 | import {Component, ChangeDetectionStrategy} from '@angular/core'; 2 | import {BehaviorSubject, of} from 'rxjs'; 3 | 4 | @Component({ 5 | selector: 'let-view-container-2', 6 | template: ` 7 | 11 | 12 | 13 | `, 14 | changeDetection: ChangeDetectionStrategy.OnPush 15 | }) 16 | export class LetView2Container { 17 | 18 | _tasks$ = new BehaviorSubject('task1'); 19 | 20 | 21 | tasks$ = this._tasks$.asObservable(); 22 | 23 | documents$ = of(['document1', 'document2']); 24 | loading$ = of(true); 25 | 26 | 27 | constructor() { 28 | setTimeout(() => this._tasks$.next('task2'), 2000); 29 | } 30 | 31 | ngDoCheck() { 32 | console.log('LetView2Container Check Stuff'); 33 | } 34 | } -------------------------------------------------------------------------------- /src/app/container/commits.container.ts: -------------------------------------------------------------------------------- 1 | import {CommitsService} from '../api/commits/commits.service'; 2 | import {OnInit, Input, Component, SimpleChanges} from '@angular/core'; 3 | import {Commit} from '../components/commitList/commit'; 4 | import {Observable} from 'rxjs'; 5 | 6 | @Component({ 7 | selector: 'commits-container', 8 | template: '' 9 | // no styles in containers please :) 10 | }) 11 | export class CommitsContainer implements OnInit { 12 | 13 | @Input() 14 | username: string; 15 | 16 | commits$: Observable; 17 | 18 | constructor(private commitsApi: CommitsService) {} 19 | 20 | ngOnInit() { 21 | } 22 | 23 | ngOnChanges({ username }: SimpleChanges) { 24 | if (username.currentValue) { 25 | this.commits$ = this.loadCommits(username.currentValue); 26 | } 27 | } 28 | 29 | private loadCommits(username: string) { 30 | return this.commitsApi.commitsByUsername(username); 31 | } 32 | } -------------------------------------------------------------------------------- /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/cli'], 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/cli/plugins/karma') 14 | ], 15 | client:{ 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | reports: [ 'html', 'lcovonly' ], 20 | fixWebpackSourcePaths: true 21 | }, 22 | angularCli: { 23 | environment: 'dev' 24 | }, 25 | reporters: ['progress', 'kjhtml'], 26 | port: 9876, 27 | colors: true, 28 | logLevel: config.LOG_INFO, 29 | autoWatch: true, 30 | browsers: ['Chrome'], 31 | singleRun: false 32 | }); 33 | }; 34 | -------------------------------------------------------------------------------- /src/app/components/counter/counter.component.ts: -------------------------------------------------------------------------------- 1 | import {Component, EventEmitter, Input, Output, ChangeDetectionStrategy} from '@angular/core'; 2 | 3 | @Component({ 4 | selector: 'counter', 5 | template: ` 6 |
Current Count: {{ count }}
7 | 8 | 9 | 10 | `, 11 | changeDetection: ChangeDetectionStrategy.OnPush 12 | }) 13 | export class CounterComponent { 14 | @Input() 15 | count: number = 666; 16 | 17 | @Output() 18 | onIncrement = new EventEmitter(); 19 | @Output() 20 | onDecrement = new EventEmitter(); 21 | @Output() 22 | onReset = new EventEmitter(); 23 | 24 | constructor() { 25 | } 26 | 27 | ngDoCheck() { 28 | console.log('CounterComponent Check Stuff'); 29 | } 30 | 31 | increment() { 32 | this.onIncrement.emit(); 33 | } 34 | 35 | decrement() { 36 | this.onDecrement.emit(); 37 | } 38 | 39 | reset() { 40 | this.onReset.emit(); 41 | } 42 | } -------------------------------------------------------------------------------- /src/app/app.component.spec.ts.old: -------------------------------------------------------------------------------- 1 | import { TestBed, async } from '@angular/core/testing'; 2 | import { AppComponent } from './app.component'; 3 | describe('AppComponent', () => { 4 | beforeEach(async(() => { 5 | TestBed.configureTestingModule({ 6 | declarations: [ 7 | AppComponent 8 | ], 9 | }).compileComponents(); 10 | })); 11 | it('should create the app', async(() => { 12 | const fixture = TestBed.createComponent(AppComponent); 13 | const app = fixture.debugElement.componentInstance; 14 | expect(app).toBeTruthy(); 15 | })); 16 | it(`should have as title 'app'`, async(() => { 17 | const fixture = TestBed.createComponent(AppComponent); 18 | const app = fixture.debugElement.componentInstance; 19 | expect(app.title).toEqual('app'); 20 | })); 21 | it('should render title in a h1 tag', async(() => { 22 | const fixture = TestBed.createComponent(AppComponent); 23 | fixture.detectChanges(); 24 | const compiled = fixture.debugElement.nativeElement; 25 | expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!'); 26 | })); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/router/params.directive.ts: -------------------------------------------------------------------------------- 1 | import {Directive, OnInit, TemplateRef, ViewContainerRef, OnDestroy} from '@angular/core'; 2 | import {ActivatedRoute} from '@angular/router'; 3 | import {Subscription} from 'rxjs'; 4 | 5 | export class ParamsContext { 6 | [key: string]: any; 7 | } 8 | 9 | @Directive({ 10 | selector: '[params]' 11 | }) 12 | export class ParamsDirective implements OnInit, OnDestroy { 13 | 14 | context = new ParamsContext(); 15 | 16 | private _routeParamsSubscription: Subscription; 17 | 18 | constructor(private template: TemplateRef, 19 | private viewContainer: ViewContainerRef, 20 | private route: ActivatedRoute) {} 21 | 22 | ngOnInit() { 23 | this.viewContainer.createEmbeddedView(this.template, this.context); 24 | this._routeParamsSubscription = this.route 25 | .paramMap 26 | .subscribe((paramMap: any) => 27 | // Copy all route params on the context 28 | Object.assign(this.context, paramMap.params) 29 | ); 30 | } 31 | 32 | ngOnDestroy() { 33 | this._routeParamsSubscription.unsubscribe(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/app/api/commits/commits.service.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {Injectable} from '@angular/core'; 3 | import {Commit} from '../../components/commitList/commit'; 4 | import {map} from 'rxjs/operators'; 5 | 6 | @Injectable() 7 | export class CommitsService { 8 | 9 | constructor(private http: HttpClient) {} 10 | 11 | commitsByUsername(username: string) { 12 | const url = `https://api.github.com/users/${username}/events`; 13 | 14 | return this.http.get(url).pipe(map(toCommits)); 15 | } 16 | } 17 | 18 | 19 | // Pure functions --> easy to test 20 | const isPushEvent = (entry) => entry.type === 'PushEvent'; 21 | 22 | export const toCommits = (response: any[]) => 23 | response 24 | .filter(isPushEvent) 25 | .reduce((commits, pushEvent) => // [[Commit, Commit], [Commit, Commit, Commit]] => [Commit, Commit, Commit, Commit, Commit] 26 | commits.concat(pushEvent.payload.commits.map(commit => 27 | new Commit(commit.sha, 28 | pushEvent.repo.name, 29 | commit.author.name, 30 | commit.message)) 31 | ) 32 | , []) as Commit[]; -------------------------------------------------------------------------------- /src/app/components/card/card.component.spec.ts: -------------------------------------------------------------------------------- 1 | import {SpectatorWithHost, createHostComponentFactory} from '@netbasal/spectator'; 2 | import {CardComponent} from './card.component'; 3 | import {MatCardModule} from '@angular/material'; 4 | 5 | describe('Card', function() { 6 | let host: SpectatorWithHost; 7 | 8 | const createHost = createHostComponentFactory({ 9 | component: CardComponent, 10 | imports: [MatCardModule] 11 | }); 12 | 13 | it('should display the default title and content of the card', () => { 14 | // When 15 | host = createHost(``); 16 | // Then 17 | expect(host.query('.title')).toHaveText('Default Title'); 18 | expect(host.query('.content')).toHaveText('Default Content'); 19 | }); 20 | 21 | it('should display the default title and content of the card - Negative Test', () => { 22 | // When 23 | host = createHost(``); 24 | // Then 25 | // Negative Test to make sure we don't have false positives 26 | expect(host.query('.title')).not.toHaveText('No Title'); 27 | expect(host.query('.content')).not.toHaveText('No Content'); 28 | }); 29 | 30 | }); -------------------------------------------------------------------------------- /.angular-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "project": { 4 | "name": "angular-starter" 5 | }, 6 | "apps": [ 7 | { 8 | "root": "src", 9 | "outDir": "dist", 10 | "assets": [ 11 | "assets", 12 | "favicon.ico" 13 | ], 14 | "index": "index.html", 15 | "main": "main.ts", 16 | "polyfills": "polyfills.ts", 17 | "test": "test.ts", 18 | "tsconfig": "tsconfig.app.json", 19 | "testTsconfig": "tsconfig.spec.json", 20 | "prefix": "app", 21 | "styles": [ 22 | "styles.scss" 23 | ], 24 | "scripts": [], 25 | "environmentSource": "environments/environment.ts", 26 | "environments": { 27 | "dev": "environments/environment.ts", 28 | "prod": "environments/environment.prod.ts" 29 | } 30 | } 31 | ], 32 | "e2e": { 33 | "protractor": { 34 | "config": "./protractor.conf.js" 35 | } 36 | }, 37 | "lint": [ 38 | { 39 | "project": "src/tsconfig.app.json", 40 | "exclude": "**/node_modules/**" 41 | }, 42 | { 43 | "project": "src/tsconfig.spec.json", 44 | "exclude": "**/node_modules/**" 45 | }, 46 | { 47 | "project": "e2e/tsconfig.e2e.json", 48 | "exclude": "**/node_modules/**" 49 | } 50 | ], 51 | "test": { 52 | "karma": { 53 | "config": "./karma.conf.js" 54 | } 55 | }, 56 | "defaults": { 57 | "styleExt": "scss", 58 | "component": {} 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /lib/connect/redux.directive.ts: -------------------------------------------------------------------------------- 1 | import {TemplateRef, OnInit, ViewContainerRef, Directive, Input, OnDestroy} from '@angular/core'; 2 | import {Store, Action} from '@ngrx/store'; 3 | 4 | export type ReduxContext = { 5 | $implicit: any; 6 | [key: string]: any; 7 | }; 8 | 9 | export type ActionCreator = { 10 | (payload: T): Action; 11 | }; 12 | 13 | export type Selector = { 14 | (store: T): any; 15 | }; 16 | 17 | @Directive({ 18 | selector: '[redux]' 19 | }) 20 | export class ReduxDirective implements OnInit, OnDestroy { 21 | 22 | @Input('reduxMapSelectorToInput') inputs: any; 23 | @Input('reduxMapOutputToAction') outputs: any; 24 | 25 | constructor(private template: TemplateRef, 26 | private viewContainer: ViewContainerRef, 27 | private store: Store) {} 28 | 29 | contextOutputs = {}; 30 | 31 | ngOnInit() { 32 | Object.keys(this.inputs) 33 | .forEach(key => { 34 | this.inputs[key] = this.store.select(this.inputs[key]); 35 | }); 36 | 37 | Object.keys(this.outputs) 38 | .forEach(key => { 39 | this.outputs[key] = this.bindAction(this.outputs[key]); 40 | }); 41 | 42 | this.viewContainer.createEmbeddedView(this.template, { 43 | $implicit: null, 44 | ...this.inputs, 45 | ...this.outputs 46 | }); 47 | } 48 | 49 | ngOnDestroy() {} 50 | 51 | select(selector) { 52 | return this.store.select(selector); 53 | } 54 | 55 | bindAction(actionCreator: ActionCreator) { 56 | return (payload: T) => { 57 | this.store.dispatch(actionCreator(payload)); 58 | }; 59 | } 60 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "angular-starter", 3 | "version": "0.0.0", 4 | "license": "MIT", 5 | "scripts": { 6 | "ng": "ng", 7 | "start": "ng serve", 8 | "build": "ng build --prod", 9 | "test": "ng test", 10 | "lint": "ng lint", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "6.1.9", 16 | "@angular/cdk": "^6.4.7", 17 | "@angular/common": "6.1.9", 18 | "@angular/compiler": "6.1.9", 19 | "@angular/core": "6.1.9", 20 | "@angular/forms": "6.1.9", 21 | "@angular/http": "6.1.9", 22 | "@angular/material": "^6.4.7", 23 | "@angular/platform-browser": "6.1.9", 24 | "@angular/platform-browser-dynamic": "6.1.9", 25 | "@angular/router": "6.1.9", 26 | "@ngrx/store": "^6.1.0", 27 | "core-js": "^2.4.1", 28 | "redux": "^4.0.1", 29 | "rxjs": "^6.3.0", 30 | "zone.js": "^0.8.19" 31 | }, 32 | "devDependencies": { 33 | "@angular/cli": "~1.7.3", 34 | "@angular/compiler-cli": "6.1.9", 35 | "@angular/language-service": "6.1.9", 36 | "@netbasal/spectator": "^3.1.3", 37 | "@types/jasmine": "~2.8.3", 38 | "@types/jasminewd2": "~2.0.2", 39 | "@types/node": "~6.0.60", 40 | "codelyzer": "^4.0.1", 41 | "jasmine-core": "~2.8.0", 42 | "jasmine-spec-reporter": "~4.2.1", 43 | "karma": "~2.0.0", 44 | "karma-chrome-launcher": "~2.2.0", 45 | "karma-coverage-istanbul-reporter": "^1.2.1", 46 | "karma-jasmine": "~1.1.0", 47 | "karma-jasmine-html-reporter": "^0.2.2", 48 | "protractor": "~5.1.2", 49 | "redux-devtools": "^3.4.1", 50 | "ts-node": "~4.1.0", 51 | "tslint": "~5.9.1", 52 | "typescript": "2.9.2" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /lib/fetch/url.directive.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {ComponentFactoryResolver, Directive, Input, OnInit, TemplateRef, Type, ViewContainerRef} from '@angular/core'; 3 | import {LoadingComponent} from '../loading-spinner/loading-spinner.component'; 4 | 5 | /** 6 | * 7 | * 8 | * 9 | * 10 | */ 11 | 12 | export class UrlContext { 13 | $implicit: any; 14 | } 15 | 16 | @Directive({ 17 | selector: '[url]' 18 | }) 19 | export class FetchUrlDirective implements OnInit { 20 | 21 | context = new UrlContext(); 22 | url: string; 23 | 24 | @Input() 25 | set urlFrom(value: string) { 26 | this.url = value; 27 | this.fetch(this.url); 28 | } 29 | 30 | @Input('urlMap') mapFn = response => response; 31 | @Input('urlLoadingComponent') loadingComponent: Type = LoadingComponent; 32 | 33 | constructor(private template: TemplateRef, 34 | private componentFactoryResolver: ComponentFactoryResolver, 35 | private viewContainer: ViewContainerRef, 36 | private httpClient: HttpClient) {} 37 | 38 | ngOnInit() {} 39 | 40 | fetch(url :string) { 41 | if (url) { 42 | //Show LoadingComponent 43 | this.viewContainer.remove(); 44 | let componentFactory = this.componentFactoryResolver.resolveComponentFactory(this.loadingComponent); 45 | this.viewContainer.createComponent(componentFactory); 46 | 47 | this.httpClient.get(this.url) 48 | .subscribe( 49 | response => { 50 | this.viewContainer.remove(); 51 | this.viewContainer.createEmbeddedView(this.template, { $implicit: this.mapFn(response) }); 52 | }, 53 | error => { 54 | this.viewContainer.remove(); 55 | }); 56 | } 57 | } 58 | } -------------------------------------------------------------------------------- /src/test/let.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {LetDirective} from '../../lib/let/let.directive'; 3 | import {createHostComponentFactory, SpectatorWithHost} from '@netbasal/spectator'; 4 | import {of, BehaviorSubject} from 'rxjs'; 5 | import {By} from '@angular/platform-browser'; 6 | import {tick, fakeAsync} from '@angular/core/testing'; 7 | 8 | @Component({ selector: 'custom-host', template: '' }) 9 | class LetDirectiveHost { 10 | taskSubject = new BehaviorSubject('First Task'); 11 | task$ = this.taskSubject.asObservable(); 12 | 13 | constructor() { 14 | setTimeout(() => this.taskSubject.next('Second Task'), 900); 15 | } 16 | } 17 | ​ 18 | describe('Let Directive', function () { 19 | let host: SpectatorWithHost; 20 | ​ 21 | const createHost = createHostComponentFactory({ 22 | component: LetDirective, 23 | host: LetDirectiveHost 24 | }); 25 | ​ 26 | it('binds the Template Variable to "First Task"', () => { 27 | // 28 | // Given 29 | host = createHost(` 30 |
35 |

Tasks: {{ task }}

36 |
37 | `); 38 | // 39 | // When 40 | host.detectChanges(); 41 | // Then 42 | ​ const p = host.hostDebugElement.query(By.css('p')).nativeElement; 43 | expect(p).toHaveText('Tasks: First Task'); 44 | }); 45 | 46 | it('updates the Template Variable to "Second Task" after one second', fakeAsync(() => { 47 | // 48 | // Given 49 | host = createHost(` 50 |
55 |

Tasks: {{ task }}

56 |
57 | `); 58 | // 59 | // When 60 | tick(1000); 61 | host.detectChanges(); 62 | // 63 | // Then 64 | ​ const p = host.hostDebugElement.query(By.css('p')).nativeElement; 65 | expect(p).toHaveText('Tasks: Second Task'); 66 | })); 67 | }); -------------------------------------------------------------------------------- /lib/router/path.directive.ts: -------------------------------------------------------------------------------- 1 | import {Component, Directive, Input, OnInit, TemplateRef, Type, ViewChild, ViewContainerRef, Optional, SkipSelf, Self, AfterViewInit} from '@angular/core'; 2 | import {ActivatedRoute, Router, Route} from '@angular/router'; 3 | import {take} from 'rxjs/operators'; 4 | import {RouteComponent} from './route.component'; 5 | 6 | export class RouteContext { 7 | [key: string]: any; 8 | } 9 | 10 | @Directive({ 11 | selector: '[path]' 12 | }) 13 | export class RouteDirective implements OnInit { 14 | 15 | @Input('path') path: string; 16 | 17 | @Input('pathMatch') match: string; 18 | @Input('pathOutlet') outlet: string; 19 | 20 | config: Route; 21 | 22 | constructor(private template: TemplateRef, 23 | private router: Router, 24 | @SkipSelf() @Optional() private parent: RouteComponent) {} 25 | 26 | ngOnInit() { 27 | if (this.parent) { 28 | console.log('Found a RouteDirective Parent. I am a child!'); 29 | console.log('I should add my configuration as children'); 30 | } 31 | this.config = { 32 | path: this.path, 33 | component: RouterRenderComponent, 34 | data: { template: this.template } 35 | }; 36 | 37 | if (this.match) { 38 | this.config.pathMatch = this.match; 39 | } 40 | 41 | if (this.outlet) { 42 | this.config.outlet = this.outlet; 43 | } 44 | 45 | this.router.config.push(this.config); 46 | 47 | } 48 | } 49 | 50 | @Component({ 51 | selector: 'Router-Component', 52 | template: ` 53 | 54 | ` 55 | }) 56 | export class RouterRenderComponent implements OnInit { 57 | 58 | context = new RouteContext(); 59 | 60 | @ViewChild('container', {read: ViewContainerRef}) container: ViewContainerRef; 61 | 62 | private template: TemplateRef; 63 | 64 | constructor(private route: ActivatedRoute) {} 65 | 66 | ngOnInit() { 67 | this.route.data.pipe(take(1)).subscribe(data => this.template = data.template); 68 | this.container.createEmbeddedView(this.template, this.context); 69 | 70 | this.route.paramMap 71 | .subscribe((paramMap: any) => { 72 | Object.assign(this.context, paramMap.params); 73 | }); 74 | } 75 | } -------------------------------------------------------------------------------- /src/test/params.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {Component} from '@angular/core'; 2 | import {BehaviorSubject} from 'rxjs'; 3 | import {SpectatorWithHost, createHostComponentFactory} from '@netbasal/spectator'; 4 | import {ParamsDirective} from '../../lib/router/params.directive'; 5 | import {By} from '@angular/platform-browser'; 6 | import {ActivatedRoute} from '@angular/router'; 7 | import {tick, fakeAsync} from '@angular/core/testing'; 8 | 9 | @Component({ selector: 'custom-host', template: '' }) 10 | class ParamsDirectiveHost { 11 | constructor() {} 12 | } 13 | 14 | class MockedActivatedRoute { 15 | 16 | paramMap = new BehaviorSubject({ 17 | params: { 18 | usernameParam: 'testValue', 19 | xxxParam: 'testValue2' 20 | } 21 | }); 22 | 23 | constructor() { 24 | setTimeout(() => { 25 | this.paramMap.next({ 26 | params: { 27 | usernameParam: 'New Value', 28 | xxxParam: 'testValue2' 29 | } 30 | }) 31 | }, 900); 32 | } 33 | } 34 | 35 | describe('Params Directive', function () { 36 | let host: SpectatorWithHost; 37 | ​ 38 | const createHost = createHostComponentFactory({ 39 | component: ParamsDirective, 40 | host: ParamsDirectiveHost, 41 | componentProviders: [ 42 | { provide: ActivatedRoute, useClass: MockedActivatedRoute } 43 | ] 44 | }); 45 | 46 | it('binds the Template Variable to "testValue"', () => { 47 | // 48 | // Given 49 | host = createHost(` 50 |
51 |

Route Param: {{username}}

52 |
53 | `); 54 | // 55 | // When 56 | host.detectChanges(); 57 | // 58 | // Then 59 | const p = host.hostDebugElement.query(By.css('p')).nativeElement; 60 | expect(p).toHaveText('Route Param: testValue'); 61 | }); 62 | 63 | it('updates the Template Variable to "Test Value 2"', fakeAsync(() => { 64 | // 65 | // Given 66 | host = createHost(` 67 |
68 |

Route Param: {{username}}

69 |
70 | `); 71 | // 72 | // When 73 | tick(1000); 74 | host.detectChanges(); 75 | // 76 | // Then 77 | const p = host.hostDebugElement.query(By.css('p')).nativeElement; 78 | expect(p).toHaveText('Route Param: New Value'); 79 | })); 80 | }); -------------------------------------------------------------------------------- /src/test/fetch.directive.spec.ts: -------------------------------------------------------------------------------- 1 | import {HttpClient} from '@angular/common/http'; 2 | import {Component} from '@angular/core'; 3 | import {By} from '@angular/platform-browser'; 4 | import {createHostComponentFactory, SpectatorWithHost} from '@netbasal/spectator'; 5 | import {FetchUrlDirective} from '../../lib/fetch/url.directive'; 6 | import {LoadingComponent} from '../../lib/loading-spinner/loading-spinner.component'; 7 | import {defer} from 'rxjs'; 8 | import {fakeAsync} from '@angular/core/testing'; 9 | 10 | @Component({ selector: 'custom-host', template: '' }) 11 | class FetchUrlDirectiveHost { 12 | constructor() {} 13 | } 14 | 15 | describe('Fetch Url Directive', function () { 16 | let host: SpectatorWithHost; 17 | let httpClientSpy; 18 | let createHost; 19 | 20 | beforeEach(() => { 21 | // TODO: spy on other methods too 22 | httpClientSpy = jasmine.createSpyObj('HttpClient', ['get']); 23 | httpClientSpy.get.and.returnValue(asyncData({ 24 | firstname: 'Max', 25 | surname: 'Mustermann', 26 | info: 'Is having fun.' 27 | })); 28 | createHost = createHostComponentFactory({ 29 | component: FetchUrlDirective, 30 | host: FetchUrlDirectiveHost, 31 | declarations: [LoadingComponent], 32 | componentProviders: [ 33 | { provide: HttpClient, useValue: httpClientSpy } 34 | ], 35 | entryComponents: [LoadingComponent] 36 | }); 37 | }); 38 | 39 | it('fetches and shows remote data', fakeAsync(() => { 40 | // 41 | // Given 42 | host = createHost(` 43 |
44 |

Firstname: {{user.firstname}}

45 |

Surname: {{user.surname}}

46 |

Info: {{user.info}}

47 |
48 | `); 49 | // 50 | // When 51 | host.detectChanges(); 52 | // 53 | // Then 54 | const firstname = host.hostDebugElement.query(By.css('.first')).nativeElement; 55 | const surname = host.hostDebugElement.query(By.css('.second')).nativeElement; 56 | const info = host.hostDebugElement.query(By.css('.third')).nativeElement; 57 | 58 | expect(firstname).toHaveText('Firstname: Max'); 59 | expect(surname).toHaveText('Surname: Mustermann'); 60 | expect(info).toHaveText('Is having fun.'); 61 | })); 62 | }); 63 | 64 | export function asyncData(data: T) { 65 | return defer(() => Promise.resolve(data)); 66 | } -------------------------------------------------------------------------------- /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 | * Required to support Web Animations `@angular/platform-browser/animations`. 51 | * Needed for: All but Chrome, Firefox and Opera. http://caniuse.com/#feat=web-animation 52 | **/ 53 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 54 | 55 | /** 56 | * By default, zone.js will patch all possible macroTask and DomEvents 57 | * user can disable parts of macroTask/DomEvents patch by setting following flags 58 | */ 59 | 60 | // (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 61 | // (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 62 | // (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 63 | 64 | /* 65 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 66 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 67 | */ 68 | // (window as any).__Zone_enable_cross_context_check = true; 69 | 70 | /*************************************************************************************************** 71 | * Zone JS is required by default for Angular itself. 72 | */ 73 | import 'zone.js/dist/zone'; // Included with Angular CLI. 74 | 75 | 76 | 77 | /*************************************************************************************************** 78 | * APPLICATION IMPORTS 79 | */ 80 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "node_modules/codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs", 22 | "rxjs/Rx" 23 | ], 24 | "import-spacing": true, 25 | "indent": [ 26 | true, 27 | "spaces" 28 | ], 29 | "interface-over-type-literal": true, 30 | "label-position": true, 31 | "max-line-length": [ 32 | true, 33 | 140 34 | ], 35 | "member-access": false, 36 | "member-ordering": [ 37 | true, 38 | { 39 | "order": [ 40 | "static-field", 41 | "instance-field", 42 | "static-method", 43 | "instance-method" 44 | ] 45 | } 46 | ], 47 | "no-arg": true, 48 | "no-bitwise": true, 49 | "no-console": [ 50 | true, 51 | "debug", 52 | "info", 53 | "time", 54 | "timeEnd", 55 | "trace" 56 | ], 57 | "no-construct": true, 58 | "no-debugger": true, 59 | "no-duplicate-super": true, 60 | "no-empty": false, 61 | "no-empty-interface": true, 62 | "no-eval": true, 63 | "no-inferrable-types": [ 64 | true, 65 | "ignore-params" 66 | ], 67 | "no-misused-new": true, 68 | "no-non-null-assertion": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "directive-selector": [ 121 | true, 122 | "attribute", 123 | "app", 124 | "camelCase" 125 | ], 126 | "component-selector": [ 127 | true, 128 | "element", 129 | "app", 130 | "kebab-case" 131 | ], 132 | "no-output-on-prefix": true, 133 | "use-input-property-decorator": true, 134 | "use-output-property-decorator": true, 135 | "use-host-property-decorator": true, 136 | "no-input-rename": true, 137 | "no-output-rename": true, 138 | "use-life-cycle-interface": true, 139 | "use-pipe-transform-interface": true, 140 | "component-class-suffix": true, 141 | "directive-class-suffix": true 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import {HttpClientModule} from '@angular/common/http'; 2 | import {NgModule} from '@angular/core'; 3 | import {MatButtonModule, MatIconModule, MatListModule, MatCardModule} from '@angular/material'; 4 | import {BrowserModule} from '@angular/platform-browser'; 5 | import {BrowserAnimationsModule} from '@angular/platform-browser/animations'; 6 | import {Route, RouterModule} from '@angular/router'; 7 | import {StoreModule} from '@ngrx/store'; 8 | import {ConnectComponent} from '../../lib/connect/connect.component'; 9 | import {ReduxDirective} from '../../lib/connect/redux.directive'; 10 | import {Fetch} from '../../lib/fetch/fetch.component'; 11 | import {FetchUrlDirective} from '../../lib/fetch/url.directive'; 12 | import {LetDirective} from '../../lib/let/let.directive'; 13 | import {Page} from '../../lib/let/page.component'; 14 | import {LoadingComponent} from '../../lib/loading-spinner/loading-spinner.component'; 15 | import {ParamsDirective} from '../../lib/router/params.directive'; 16 | import {RouteDirective, RouterRenderComponent} from '../../lib/router/path.directive'; 17 | import {RouteComponent} from '../../lib/router/route.component'; 18 | import {CommitsService} from './api/commits/commits.service'; 19 | import {AppComponent} from './app.component'; 20 | import {CommitListComponent} from './components/commitList/commitList.component'; 21 | import {CounterComponent} from './components/counter/counter.component'; 22 | import {LetComponent} from './components/let/let.component'; 23 | import {UserView} from './components/user/user.component'; 24 | import {CommitsContainer} from './container/commits.container'; 25 | import {counterReducer} from './state'; 26 | import {CommitsNormalView} from './views/commits-normal.view'; 27 | import {CommitsView} from './views/commits.view'; 28 | import {CounterContainer} from './views/counter.container'; 29 | import {CounterView} from './views/counter.view'; 30 | import {LetView} from './views/let.view'; 31 | import {LetViewContainer} from './views/letView.container'; 32 | import {LetView2Container} from './views/letView2.container'; 33 | import {CardComponent} from './components/card/card.component'; 34 | import {CommitsViewWithFetch} from './views/commits.view-with-fetch'; 35 | 36 | const routes: Route[] = [ 37 | { path: 'commits/:usernameParam', component: CommitsView }, 38 | { path: 'let', component: LetViewContainer }, 39 | { path: 'let2', component: LetView2Container}, 40 | { path: 'connect', component: CounterView }, 41 | { path: 'commits-normal/:username', component: CommitsNormalView}, 42 | { path: 'commits-fetch/:usernameParam', component: CommitsViewWithFetch} 43 | ]; 44 | 45 | export interface AppState { 46 | counter: number; 47 | } 48 | 49 | @NgModule({ 50 | declarations: [ 51 | AppComponent, 52 | Fetch, 53 | FetchUrlDirective, 54 | LoadingComponent, 55 | RouteComponent, 56 | ParamsDirective, 57 | RouteDirective, 58 | RouterRenderComponent, 59 | CommitListComponent, 60 | CommitsView, 61 | UserView, 62 | LetDirective, 63 | LetView, 64 | Page, 65 | CounterComponent, 66 | ConnectComponent, 67 | ReduxDirective, 68 | CounterView, 69 | CommitsContainer, 70 | CommitsNormalView, 71 | LetViewContainer, 72 | CounterContainer, 73 | LetComponent, 74 | LetView2Container, 75 | CardComponent, 76 | CommitsViewWithFetch 77 | ], 78 | imports: [ 79 | BrowserModule, 80 | BrowserAnimationsModule, 81 | RouterModule.forRoot(routes, { useHash: true }), 82 | StoreModule.forRoot({ counter: counterReducer}), 83 | HttpClientModule, 84 | MatButtonModule, 85 | MatCardModule, 86 | MatIconModule, 87 | MatListModule 88 | ], 89 | providers: [ 90 | CommitsService 91 | ], 92 | bootstrap: [AppComponent], 93 | entryComponents: [LoadingComponent, RouterRenderComponent] 94 | }) 95 | export class AppModule { } 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # I just wanna have fun - Angular Structural Directives 2 | 3 | This is just a fun project to show the power of structural directives. 4 | 5 | In case you didn't notice. I was slightly influenced by React Render-Children pattern. I don't chase after the React architecture, but I tried to achieve stuff that the React community does simply better: 6 | - Containers Component, who should not have a templates, nor styles 7 | - Default OnPush 8 | - Clear Separation of concerns, meaning Page or View Components, which are effectively "dumb", but do not have a dependency to the state management library, http-client or router. 9 | 10 | I guess it's possible that every usecase could also be done with 'normal' directives, which would include `..`. Since structural directives hide this structure from us I decided to go with them. 11 | 12 | Again: I just wanna have fun :) 13 | 14 | ## let 15 | 16 | As known from other existing projects like [ngrx-utils](https://github.com/ngrx-utils/ngrx-utils) this directives allows you to define template variables on a more global scope within a template. This behaviour is already known from the core ngIf Directive, but can also find usage without the need of a condition. 17 | Merely it is used because you only want to subscribe once and you don't wanna manage the subscription in your component code. 18 | ``` 19 | // let.view.html 20 | 29 | 30 |

Tasks:

{{ tasks | json }} 31 |

Documents:

{{ documents | json }} 32 |

Loading:

{{ loading }} 33 | 34 |
35 | 36 | @Component({ 37 | selector: 'let-view', 38 | templateUrl: 'let.view.html' 39 | }) 40 | export class LetView { 41 | tasks$ = of(['task1', 'task2']); 42 | documents$ = of(['document1', 'document2']); 43 | loading$ = of(true); 44 | } 45 | ``` 46 | The main difference to the already given solution of ngrx-utils is that internally the from object is spread into the context object of the strucural directive, which allows us to define the variables like `let task=task`and access their values directly. 47 | 48 | ## Route Params 49 | 50 | This structural directive allows you to read parameters from the current active route and pass it via inputs into your page component. Therefore your page component does not have to be dependent on the ActiveRoute directly, gaining separation of concerns. 51 | ``` 52 | 53 | 54 | 55 | ``` 56 | 57 | ## Route Definition 58 | 59 | This is a naughty one. It allows to configure routes at runtime. 60 | ``` 61 | 62 | 63 | 64 |

TEST Route

65 |
66 | 67 | 68 |

Next One

69 |
70 | 71 | // access params in the same way as with the *params directive 72 | 73 | 74 | 75 | ``` 76 | It doesn't support childroutes for now. 77 | 78 | ## Fetch 79 | 80 | Fetch data via HTTP-Call. 81 | ``` 82 | 83 | 84 | 85 | ``` 86 | Multiple structural directives can be combined. In the next example the `*params` directive is combined wit the `*url` one. 87 | 88 | ``` 89 | 90 | 91 | 92 | 93 | 94 | ``` 95 | Fetch allows to pass a map function, which takes the response and maps it to whatever you like. 96 | 97 | ## Connect Redux 98 | 99 | Connect to NGRX decaratively. 100 | ``` 101 | // counter.view.html 102 | 114 | 115 | 119 | 120 | 121 | 122 | 123 | @Component({ 124 | selector: 'counter-view', 125 | templateUrl: 'counter.view.html' 126 | }) 127 | export class CounterView { 128 | 129 | counterSelector(state: AppState) { 130 | return state.counter; 131 | } 132 | 133 | incrementActionCreator(payload) { 134 | return { 135 | type: INCREMENT 136 | }; 137 | } 138 | 139 | decrementActionCreator(payload) { 140 | return { 141 | type: DECREMENT 142 | }; 143 | } 144 | 145 | resetActionCreator(payload) { 146 | return { 147 | type: RESET 148 | }; 149 | } 150 | } 151 | ``` 152 | It takes selector functions and binds them to the inputs. 153 | It takes action-creator functions and binds them to the store. 154 | So no ngrx dependency is needed in your page component. 155 | 156 | 157 | --------------------------------------------------------------------------------