Initial commit

This commit is contained in:
kamil.mysliwiec
2017-08-26 10:12:55 +02:00
commit 35518da691
183 changed files with 12876 additions and 0 deletions

62
.angular-cli.json Normal file
View File

@@ -0,0 +1,62 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"project": {
"name": "scalio-base2-front"
},
"apps": [
{
"root": "src",
"outDir": "dist",
"assets": [
"assets",
"favicon.ico"
],
"index": "index.html",
"main": "main.ts",
"polyfills": "polyfills.ts",
"test": "test.ts",
"tsconfig": "tsconfig.app.json",
"testTsconfig": "tsconfig.spec.json",
"prefix": "app",
"styles": [
"styles.scss"
],
"scripts": [
"assets/js/prism.js"
],
"environmentSource": "environments/environment.ts",
"environments": {
"dev": "environments/environment.ts",
"prod": "environments/environment.prod.ts"
}
}
],
"e2e": {
"protractor": {
"config": "./protractor.conf.js"
}
},
"lint": [
{
"project": "src/tsconfig.app.json",
"exclude": "**/node_modules/**"
},
{
"project": "src/tsconfig.spec.json",
"exclude": "**/node_modules/**"
},
{
"project": "e2e/tsconfig.e2e.json",
"exclude": "**/node_modules/**"
}
],
"test": {
"karma": {
"config": "./karma.conf.js"
}
},
"defaults": {
"styleExt": "scss",
"component": {}
}
}

13
.editorconfig Normal file
View File

@@ -0,0 +1,13 @@
# Editor configuration, see http://editorconfig.org
root = true
[*]
charset = utf-8
indent_style = space
indent_size = 2
insert_final_newline = true
trim_trailing_whitespace = true
[*.md]
max_line_length = off
trim_trailing_whitespace = false

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# See http://help.github.com/ignore-files/ for more about ignoring files.
# compiled output
/dist
/tmp
/out-tsc
# dependencies
/node_modules
# IDEs and editors
/.idea
.project
.classpath
.c9/
*.launch
.settings/
*.sublime-workspace
# IDE - VSCode
/.vscode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
# misc
/.sass-cache
/connect.lock
/coverage
/libpeerconnection.log
npm-debug.log
testem.log
/typings
# e2e
/e2e/*.js
/e2e/*.map
# System Files
.DS_Store
Thumbs.db
# Mock API
/api/node_modules

28
README.md Normal file
View File

@@ -0,0 +1,28 @@
# ScalioBase2Front
This project was generated with [Angular CLI](https://github.com/angular/angular-cli) version 1.2.3.
## Development server
Run `ng serve` for a dev server. Navigate to `http://localhost:4200/`. The app will automatically reload if you change any of the source files.
## Code scaffolding
Run `ng generate component component-name` to generate a new component. You can also use `ng generate directive|pipe|service|class|module`.
## Build
Run `ng build` to build the project. The build artifacts will be stored in the `dist/` directory. Use the `-prod` flag for a production build.
## Running unit tests
Run `ng test` to execute the unit tests via [Karma](https://karma-runner.github.io).
## Running end-to-end tests
Run `ng e2e` to execute the end-to-end tests via [Protractor](http://www.protractortest.org/).
Before running the tests make sure you are serving the app via `ng serve`.
## Further help
To get more help on the Angular CLI use `ng help` or go check out the [Angular CLI README](https://github.com/angular/angular-cli/blob/master/README.md).

14
e2e/app.e2e-spec.ts Normal file
View File

@@ -0,0 +1,14 @@
import { DocsPage } from './app.po';
describe('docs App', () => {
let page: DocsPage;
beforeEach(() => {
page = new DocsPage();
});
it('should display welcome message', () => {
page.navigateTo();
expect(page.getParagraphText()).toEqual('Welcome to app!');
});
});

11
e2e/app.po.ts Normal file
View File

@@ -0,0 +1,11 @@
import { browser, by, element } from 'protractor';
export class DocsPage {
navigateTo() {
return browser.get('/');
}
getParagraphText() {
return element(by.css('app-root h1')).getText();
}
}

14
e2e/tsconfig.e2e.json Normal file
View File

@@ -0,0 +1,14 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../out-tsc/e2e",
"baseUrl": "./",
"module": "commonjs",
"target": "es5",
"types": [
"jasmine",
"jasminewd2",
"node"
]
}
}

33
karma.conf.js Normal file
View File

@@ -0,0 +1,33 @@
// Karma configuration file, see link for more information
// https://karma-runner.github.io/0.13/config/configuration-file.html
module.exports = function (config) {
config.set({
basePath: '',
frameworks: ['jasmine', '@angular/cli'],
plugins: [
require('karma-jasmine'),
require('karma-chrome-launcher'),
require('karma-jasmine-html-reporter'),
require('karma-coverage-istanbul-reporter'),
require('@angular/cli/plugins/karma')
],
client:{
clearContext: false // leave Jasmine Spec Runner output visible in browser
},
coverageIstanbulReporter: {
reports: [ 'html', 'lcovonly' ],
fixWebpackSourcePaths: true
},
angularCli: {
environment: 'dev'
},
reporters: ['progress', 'kjhtml'],
port: 9876,
colors: true,
logLevel: config.LOG_INFO,
autoWatch: true,
browsers: ['Chrome'],
singleRun: false
});
};

7620
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

66
package.json Normal file
View File

@@ -0,0 +1,66 @@
{
"name": "scalio-base2-front",
"version": "0.0.0",
"license": "MIT",
"scripts": {
"ng": "ng",
"start": "ng serve --sourcemap=false",
"build": "ng build",
"test": "ng test",
"lint": "ng lint",
"e2e": "ng e2e"
},
"private": true,
"dependencies": {
"@angular/animations": "^4.3.1",
"@angular/cdk": "^2.0.0-beta.8",
"@angular/common": "^4.3.1",
"@angular/compiler": "^4.3.1",
"@angular/core": "^4.3.1",
"@angular/flex-layout": "^2.0.0-beta.8",
"@angular/forms": "^4.3.1",
"@angular/http": "^4.3.1",
"@angular/material": "^2.0.0-beta.8",
"@angular/platform-browser": "^4.3.1",
"@angular/platform-browser-dynamic": "^4.3.1",
"@angular/router": "^4.3.1",
"@ngrx/core": "^1.2.0",
"@ngrx/effects": "^4.0.1",
"@ngrx/router-store": "^4.0.0",
"@ngrx/store": "^4.0.0",
"@ngrx/store-devtools": "^4.0.0",
"@ultimate/ngxerrors": "^1.3.0",
"angular2-highlight-js": "^5.0.2",
"core-js": "^2.4.1",
"hammerjs": "^2.0.8",
"lodash": "^4.17.4",
"ng2-prism": "^2.3.2",
"ng2-progressbar": "^1.3.0",
"ngx-perfect-scrollbar": "^4.5.3",
"rxjs": "^5.4.1",
"zone.js": "^0.8.14"
},
"devDependencies": {
"@angular/cli": "1.2.3",
"@angular/compiler-cli": "^4.0.0",
"@angular/language-service": "^4.0.0",
"@types/jasmine": "~2.5.53",
"@types/jasminewd2": "~2.0.2",
"@types/lodash": "^4.14.71",
"@types/node": "~6.0.60",
"codelyzer": "~3.0.1",
"concurrently": "^3.5.0",
"jasmine-core": "~2.6.2",
"jasmine-spec-reporter": "~4.1.0",
"karma": "~1.7.0",
"karma-chrome-launcher": "~2.1.1",
"karma-cli": "~1.0.1",
"karma-coverage-istanbul-reporter": "^1.2.1",
"karma-jasmine": "~1.1.0",
"karma-jasmine-html-reporter": "^0.2.2",
"protractor": "~5.1.2",
"ts-node": "~3.0.4",
"tslint": "~5.3.2",
"typescript": "~2.3.3"
}
}

28
protractor.conf.js Normal file
View File

@@ -0,0 +1,28 @@
// Protractor configuration file, see link for more information
// https://github.com/angular/protractor/blob/master/lib/config.ts
const { SpecReporter } = require('jasmine-spec-reporter');
exports.config = {
allScriptsTimeout: 11000,
specs: [
'./e2e/**/*.e2e-spec.ts'
],
capabilities: {
'browserName': 'chrome'
},
directConnect: true,
baseUrl: 'http://localhost:4200/',
framework: 'jasmine',
jasmineNodeOpts: {
showColors: true,
defaultTimeoutInterval: 30000,
print: function() {}
},
onPrepare() {
require('ts-node').register({
project: 'e2e/tsconfig.e2e.json'
});
jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } }));
}
};

View File

@@ -0,0 +1,77 @@
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { environment } from '../environments/environment';
import { HomepageComponent } from './homepage/homepage.component';
import { IntroductionComponent } from './homepage/pages/introduction/introduction.component';
import { FirstStepsComponent } from './homepage/pages/first-steps/first-steps.component';
import { ControllersComponent } from './homepage/pages/controllers/controllers.component';
import { ComponentsComponent } from './homepage/pages/components/components.component';
import { ModulesComponent } from './homepage/pages/modules/modules.component';
import { MiddlewaresComponent } from './homepage/pages/middlewares/middlewares.component';
import { PipesComponent } from './homepage/pages/pipes/pipes.component';
import { GuardsComponent } from './homepage/pages/guards/guards.component';
import { ExceptionFiltersComponent } from './homepage/pages/exception-filters/exception-filters.component';
const routes: Routes = [
{
path: '',
component: HomepageComponent,
children: [
{
path: '',
component: IntroductionComponent,
},
{
path: 'first-steps',
component: FirstStepsComponent,
},
{
path: 'controllers',
component: ControllersComponent,
},
{
path: 'components',
component: ComponentsComponent,
},
{
path: 'modules',
component: ModulesComponent,
},
{
path: 'middlewares',
component: MiddlewaresComponent,
},
{
path: 'pipes',
component: PipesComponent,
},
{
path: 'guards',
component: GuardsComponent,
},
{
path: 'exception-filters',
component: ExceptionFiltersComponent,
}
]
},
{
path: '**',
redirectTo: '',
pathMatch: 'full',
}
];
@NgModule({
imports: [
RouterModule.forRoot(
routes,
{
enableTracing: !environment.production,
},
),
],
exports: [RouterModule]
})
export class AppRoutingModule {}

View File

View File

@@ -0,0 +1 @@
<router-outlet></router-outlet>

View File

@@ -0,0 +1,32 @@
import { TestBed, async } from '@angular/core/testing';
import { AppComponent } from './app.component';
describe('AppComponent', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [
AppComponent
],
}).compileComponents();
}));
it('should create the app', async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app).toBeTruthy();
}));
it(`should have as title 'app'`, async(() => {
const fixture = TestBed.createComponent(AppComponent);
const app = fixture.debugElement.componentInstance;
expect(app.title).toEqual('app');
}));
it('should render title in a h1 tag', async(() => {
const fixture = TestBed.createComponent(AppComponent);
fixture.detectChanges();
const compiled = fixture.debugElement.nativeElement;
expect(compiled.querySelector('h1').textContent).toContain('Welcome to app!');
}));
});

17
src/app/app.component.ts Normal file
View File

@@ -0,0 +1,17 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
constructor(private readonly router: Router) { }
ngOnInit() {
this.router.events
.filter((ev) => ev instanceof NavigationEnd)
.subscribe(() => window.scroll(0, 0));
}
}

59
src/app/app.module.ts Normal file
View File

@@ -0,0 +1,59 @@
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { PerfectScrollbarModule } from 'ngx-perfect-scrollbar';
import { AppComponent } from './app.component';
import { AppRoutingModule } from './app-routing.module';
import { CoreModule } from './core/core.module';
import { HomepageComponent } from './homepage/homepage.component';
import { HeaderComponent } from './homepage/header/header.component';
import { FooterComponent } from './homepage/footer/footer.component';
import { MenuComponent } from './homepage/menu/menu.component';
import { MenuItemComponent } from './homepage/menu/menu-item/menu-item.component';
import { IntroductionComponent } from './homepage/pages/introduction/introduction.component';
import { FirstStepsComponent } from './homepage/pages/first-steps/first-steps.component';
import { ControllersComponent } from './homepage/pages/controllers/controllers.component';
import { BasePageComponent } from './homepage/pages/page/page.component';
import { MatchHeightDirective } from './common/directives/match-height.directive';
import { ComponentsComponent } from './homepage/pages/components/components.component';
import { ModulesComponent } from './homepage/pages/modules/modules.component';
import { MiddlewaresComponent } from './homepage/pages/middlewares/middlewares.component';
import { PipesComponent } from './homepage/pages/pipes/pipes.component';
import { ExceptionFiltersComponent } from './homepage/pages/exception-filters/exception-filters.component';
import { GuardsComponent } from './homepage/pages/guards/guards.component';
import { DependencyInjectionComponent } from './homepage/pages/advanced/dependency-injection/dependency-injection.component';
import { AsyncComponentsComponent } from './homepage/pages/advanced/async-components/async-components.component';
@NgModule({
imports: [
BrowserModule,
AppRoutingModule,
CoreModule,
PerfectScrollbarModule.forRoot({
suppressScrollX: true,
}),
],
declarations: [
AppComponent,
HomepageComponent,
HeaderComponent,
FooterComponent,
MenuComponent,
MenuItemComponent,
IntroductionComponent,
FirstStepsComponent,
ControllersComponent,
BasePageComponent,
MatchHeightDirective,
ComponentsComponent,
ModulesComponent,
MiddlewaresComponent,
PipesComponent,
ExceptionFiltersComponent,
GuardsComponent,
DependencyInjectionComponent,
AsyncComponentsComponent,
],
bootstrap: [AppComponent]
})
export class AppModule {}

View File

@@ -0,0 +1 @@
export * from './router.animations';

View File

@@ -0,0 +1,18 @@
import { trigger, animate, style, group, query, transition } from '@angular/animations';
export const routerTransition = trigger('routerTransition', [
transition('* <=> *', [
query(':enter, :leave', style({ position: 'fixed', width: '100%' })
, { optional: true }),
group([
query(':enter', [
style({ opacity: '0.0' }),
animate('0.4s 0.6s ease-in-out', style({ opacity: '1.0' }))
], { optional: true }),
query(':leave', [
style({ opacity: '1.0' }),
animate('0.4s ease-in-out', style({ opacity: '0.0' }))
], { optional: true }),
]),
]),
]);

View File

@@ -0,0 +1,39 @@
import {
Directive, ElementRef, AfterViewChecked,
Input, HostListener, Renderer2, NgZone
} from '@angular/core';
@Directive({
selector: '[matchHeight]'
})
export class MatchHeightDirective implements AfterViewChecked {
constructor(
private readonly zone: NgZone,
private readonly rednerer: Renderer2,
private readonly el: ElementRef) {}
ngAfterViewChecked() {
setTimeout(() => this.zone.run(() => this.matchHeight(this.el.nativeElement)), 800);
}
@HostListener('window:resize')
onResize() {
setTimeout(() => this.matchHeight(this.el.nativeElement), 500);
}
matchHeight(parent: HTMLElement) {
if (!parent) {
return;
}
const children = Array.from(parent.children);
children.forEach((x: HTMLElement) => {
this.rednerer.setStyle(x, 'height', 'initial');
});
const itemHeights = children.map(x => x.getBoundingClientRect().height);
const maxHeight = itemHeights.reduce((prev, curr) => {
return curr > prev ? curr : prev;
}, 0);
children.forEach((x: HTMLElement) => this.rednerer.setStyle(x, 'height', `${maxHeight}px`));
}
}

1
src/app/common/index.ts Normal file
View File

@@ -0,0 +1 @@
export * from './animations';

0
src/app/constants.ts Normal file
View File

View File

@@ -0,0 +1,6 @@
import { Injectable } from '@angular/core';
@Injectable()
export abstract class ConfigService {
abstract readonly API_URL: string;
}

View File

@@ -0,0 +1,5 @@
import { ConfigService } from './config.service';
export class DevelopmentConfigService extends ConfigService {
readonly API_URL = 'http://localhost:3001';
}

View File

@@ -0,0 +1,5 @@
import { ConfigService } from './config.service';
export class ProductionConfigService extends ConfigService {
readonly API_URL = 'http://prod-api.scali.io';
}

View File

@@ -0,0 +1,21 @@
import { NgModule } from '@angular/core';
import { ConfigService } from './config/config.service';
import { environment } from '../../environments/environment';
import { DevelopmentConfigService } from './config/dev-config.service';
import { ProductionConfigService } from './config/prod-config.service';
@NgModule({
imports: [],
providers: [
{
provide: ConfigService,
useFactory() {
return !environment.production ?
new DevelopmentConfigService() : new ProductionConfigService();
},
}
],
exports: [],
})
export class CoreModule { }

View File

@@ -0,0 +1,18 @@
<footer>
<div class="credits">
<p>
Made by <a
href="https://kamilmysliwiec.com"
target="blank"
title="Kamil Mysliwiec Blog | Full-Stack Software Engineer">
Kamil Myśliwiec</a> ©2017,
MIT License
</p>
</div>
<div class="social-wrapper">
<a href="https://www.facebook.com/kamil.myslik" target="blank"><i class="fa fa-facebook"></i></a>
<a href="https://twitter.com/kammysliwiec" target="blank"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/kamilmysliwiec" target="blank"><i class="fa fa-github"></i></a>
<a href="https://kamilmysliwiec.com" title="Kamil Mysliwiec Blog | Full-Stack Software Engineer" target="blank"><i class="fa fa-globe"></i></a>
</div>
</footer>

View File

@@ -0,0 +1,67 @@
@import "../../../scss/variables.scss";
@import "../../../scss/utils.scss";
:host {
@extend .transition;
@extend .box-sizing;
background: $black-color;
display: block;
height: 85px;
padding: 16px 2px 16px 52px;
position: absolute;
bottom: 0;
left: 0;
right: 0;
margin: 0;
width: auto;
@include media(medium) {
padding: 15px;
margin: 30px -30px 0;
width: calc(100% + 60px);
position: static;
height: auto;
text-align: center;
}
}
footer {
display: block;
position: relative;
a {
@extend .transition;
color: $grey-color;
&:hover {
color: $white-color;
}
}
}
.social-wrapper {
@extend .center-top;
position: absolute;
right: 40px;
@include media(medium) {
@include transform(translateX(0) translateY(0));
position: static;
text-align: center;
}
a {
@extend .transition;
display: inline-block;
vertical-align: middle;
color: $darksilver-color;
font-size: 22px;
margin-left: 10px;
cursor: pointer;
&:first-of-type {
font-size: 20px;
}
&:hover {
color: $white-color;
}
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FooterComponent } from './footer.component';
describe('FooterComponent', () => {
let component: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FooterComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FooterComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,9 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-footer',
templateUrl: './footer.component.html',
styleUrls: ['./footer.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FooterComponent {}

View File

@@ -0,0 +1,13 @@
<header>
<div class="icon-wrapper" (click)="toggle.emit()" [class.opened]="isSidebarOpened">
<div class="nav-icon">
<img src="assets/menu.png" alt="Open navigation sidebar" />
</div>
</div>
<ng-content></ng-content>
<div class="social-wrapper">
<a href="https://twitter.com/nestframework" target="blank"><i class="fa fa-twitter"></i></a>
<a href="https://github.com/kamilmysliwiec/nest" target="blank"><i class="fa fa-github"></i></a>
<a href="http://nestjs.com" title="Nest - A node.js framework built on top of TypeScript" target="blank"><i class="fa fa-globe"></i></a>
</div>
</header>

View File

@@ -0,0 +1,80 @@
@import "../../../scss/variables.scss";
@import "../../../scss/utils.scss";
:host {
background: $black-color;
border-bottom: 6px solid $red-color;
display: block;
width: 100%;
height: 85px;
}
header {
height: 100%;
width: 100%;
display: block;
position: relative;
}
.icon-wrapper {
@extend .transition;
height: 100%;
width: 80px;
top: 0;
bottom: 0;
background: $darkgrey-color;
&:hover {
cursor: pointer;
background: #212121;
}
&.opened {
width: 345px;
@include media(large) { width: 300px; }
@include media(normal) { width: 80px; }
}
.nav-icon {
@extend .center-element;
}
@include media(medium) { background: transparent; }
}
:host /deep/ .logo-wrapper {
display: block;
width: 100%;
height: 100%;
text-align: center;
a {
display: inline-block;;
padding-top: 10px;
}
}
.social-wrapper,
.icon-wrapper {
position: absolute;
}
.social-wrapper {
@extend .center-top;
right: 40px;
a {
@extend .transition;
display: inline-block;
vertical-align: middle;
color: $darksilver-color;
font-size: 22px;
margin-left: 10px;
cursor: pointer;
&:first-of-type {
font-size: 20px;
}
&:hover {
color: $white-color;
}
}
@include media(medium) { display: none; }
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HeaderComponent } from './header.component';
describe('HeaderComponent', () => {
let component: HeaderComponent;
let fixture: ComponentFixture<HeaderComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HeaderComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HeaderComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,12 @@
import { Component, Output, EventEmitter, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-header',
templateUrl: './header.component.html',
styleUrls: ['./header.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HeaderComponent {
@Output() toggle = new EventEmitter<void>();
@Input() isSidebarOpened = true;
}

View File

@@ -0,0 +1,26 @@
<app-header
(toggle)="toggleSidebar()"
[isSidebarOpened]="isSidebarOpened"
>
<div class="logo-wrapper">
<a href="http://docs.nestjs.com/" title="Documentation | Nest - A node.js framework built on top of TypeScript">
<img src="assets/logo.png" alt="Nest - A node.js framework built on top of TypeScript" />
</a>
</div>
</app-header>
<div class="container-fluid" matchHeight>
<app-menu [class.opened]="isSidebarOpened" perfect-scrollbar></app-menu>
<div class="container" [class.wide]="!isSidebarOpened" (click)="checkWindowWidth()">
<div class="page-wrapper">
<router-outlet></router-outlet>
</div>
<app-footer>
<p>
Made by
<a href="https://kamilmysliwiec.com" title="Kamil Mysliwiec Blog | Full-Stack Software Engineer">
Kamil Myśliwiec
</a>
</p>
</app-footer>
</div>
</div>

View File

@@ -0,0 +1,62 @@
@import "../../scss/utils.scss";
@import "../../scss/variables.scss";
.container-fluid {
position: relative;
display: block;
table-layout: fixed;
width: 100%;
min-height: 100vh;
}
.container {
@extend .transition;
@extend .box-sizing;
@include transform(translateX(345px));
width: calc(100% - 345px);
padding: 45px 50px 100px;
display: inline-block;
vertical-align: top;
position: relative;
&.wide {
@include transform(translateX(0));
width: 100%;
display: block;
}
@include media(large) {
@include transform(translateX(300px));
width: calc(100% - 300px);
&.wide { width: 100%; }
}
@include media(medium) {
@include transform(translateX(0));
width: 100%;
padding: 20px 30px 0;
}
}
.page-wrapper {
color: $grey-color;
line-height: 26px;
a {
font-weight: 500;
color: $red-color;
&:hover {
color: #0894e2;
}
}
h1, h2, h3, h4, h5, h6 {
color: $black-color;
}
h3 { font-size: 24px; }
@include media(normal) {
p {
text-align: justify;
}
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { HomepageComponent } from './homepage.component';
describe('HomepageComponent', () => {
let component: HomepageComponent;
let fixture: ComponentFixture<HomepageComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ HomepageComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(HomepageComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,31 @@
import { Component, ViewEncapsulation, HostListener, AfterViewInit, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-homepage',
templateUrl: './homepage.component.html',
styleUrls: ['./homepage.component.scss'],
encapsulation: ViewEncapsulation.None,
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomepageComponent implements AfterViewInit {
isSidebarOpened = true;
toggleSidebar() {
this.isSidebarOpened = !this.isSidebarOpened;
}
ngAfterViewInit() {
this.checkWindowWidth(window.innerWidth);
}
@HostListener('window:resize', ['$event'])
onResize(event) {
this.checkWindowWidth(event.target.innerWidth);
}
checkWindowWidth(innerWidth?: number) {
innerWidth = innerWidth ? innerWidth : window.innerWidth;
if (innerWidth <= 768) {
this.isSidebarOpened = false;
}
}
}

View File

@@ -0,0 +1,22 @@
<div class="nav-item" [class.opened]="isOpen" *ngIf="children; else withoutChildren">
<div class="heading" (click)="toggle()">
<h3>{{ title }}</h3>
<i class="fa fa-angle-right arrow-icon"></i>
</div>
<ul class="sub-nav">
<li *ngFor="let item of children">
<a
[routerLink]="item.path"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }">{{ item.title }}</a>
</li>
</ul>
</div>
<ng-template #withoutChildren>
<div class="heading">
<a
[routerLink]="path"
routerLinkActive="active"
[routerLinkActiveOptions]="{ exact: true }"><h3>{{ title }}</h3></a>
</div>
</ng-template>

View File

@@ -0,0 +1,72 @@
@import "../../../../scss/variables.scss";
@import "../../../../scss/utils.scss";
:host {
position: relative;
display: block;
}
h3 {
@extend .transition;
text-transform: uppercase;
font-weight: bold;
color: $black-color;
font-size: 15px;
letter-spacing: 0.2px;
&:hover {
color: $red-color;
}
}
.active h3 {
color: $red-color;
}
.arrow-icon {
@extend .transition;
position: absolute;
top: 1px;
right: 0;
font-size: 22px;
cursor: pointer;
}
li {
display: block;
padding: 10px 0;
a {
color: $black-color;
font-size: 15px;
text-decoration: none;
cursor: pointer;
&:hover {
color: $red-color;
}
&.active { font-weight: 500; }
}
}
.nav-item {
ul {
display: none;
}
&.opened {
ul {
display: block;
}
.arrow-icon {
@include transform(rotate(90deg));
color: $red-color;
}
}
}
.heading {
cursor: pointer;
&:hover {
.arrow-icon {
color: $red-color;
}
}
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuItemComponent } from './menu-item.component';
describe('MenuItemComponent', () => {
let component: MenuItemComponent;
let fixture: ComponentFixture<MenuItemComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MenuItemComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MenuItemComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,18 @@
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-menu-item',
templateUrl: './menu-item.component.html',
styleUrls: ['./menu-item.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuItemComponent {
@Input() isOpen = false;
@Input() children = [];
@Input() path: string;
@Input() title: string;
toggle() {
this.isOpen = !this.isOpen;
}
}

View File

@@ -0,0 +1,9 @@
<nav class="nav-container">
<app-menu-item
*ngFor="let item of items"
[isOpen]="item.isOpened"
[title]="item.title"
[path]="item?.path"
[children]="item.children"
></app-menu-item>
</nav>

View File

@@ -0,0 +1,23 @@
@import "../../../scss/variables.scss";
@import "../../../scss/utils.scss";
:host {
@extend .transition;
@extend .box-sizing;
@include transform(translateX(-100%));
padding: 55px 30px 40px 45px;
width: 345px;
background: $white-color;
box-shadow: 0 0 25px rgba(0, 0, 0, 0.1);
position: absolute !important;
bottom: 0;
top: 0;
z-index: 1000;
overflow: hidden;
&.opened {
@include transform(translateX(0));
}
@include media(large) { width: 300px; }
@include media(medium) { padding: 20px 25px 20px 25px; }
}

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MenuComponent } from './menu.component';
describe('MenuComponent', () => {
let component: MenuComponent;
let fixture: ComponentFixture<MenuComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MenuComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MenuComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,94 @@
import { Component, Input, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-menu',
templateUrl: './menu.component.html',
styleUrls: ['./menu.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class MenuComponent {
@Input() isSidebarOpened = true;
readonly items = [
{
title: 'Introduction',
isOpened: false,
path: '/'
},
{
title: 'Overview',
isOpened: true,
children: [
{ title: 'First Steps', path: '/first-steps' },
{ title: 'Controllers', path: '/controllers' },
{ title: 'Components', path: '/components' },
{ title: 'Modules', path: '/modules' },
{ title: 'Middlewares', path: '/middlewares' },
{ title: 'Exception Filters', path: '/exception-filters' },
{ title: 'Pipes', path: '/pipes' },
{ title: 'Guards', path: '/guards' },
]
},
{
title: 'Advanced',
isOpened: false,
children: [
{ title: 'Dependency Injection', path: '/advanced/dependency-injection' },
{ title: 'Async Components', path: '/advanced/async-components' },
{ title: 'Hierarchical Injection', path: '/advanced/hierarchical-injection' },
{ title: 'Unit Testing', path: '/advanced/unit-testing' },
{ title: 'E2E Testing', path: '/advanced/e2e-testing' },
]
},
{
title: 'WebSockets',
isOpened: false,
children: [
{ title: 'Gateways', path: '/websockets/gateways' },
{ title: 'Adapter', path: '/websockets/adapter' },
{ title: 'Pipes', path: '/websockets/pipes' },
{ title: 'Exception Filters', path: '/websockets/exception-filters' },
{ title: 'Guards', path: '/websockets/guards' },
]
},
{
title: 'Microservices',
isOpened: false,
children: [
{ title: 'Basics', path: '/microservices/basics' },
{ title: 'Redis', path: '/websockets/redis' },
{ title: 'Custom Transport', path: '/websockets/custom-transport' },
{ title: 'Pipes', path: '/websockets/pipes' },
{ title: 'Exception Filters', path: '/websockets/exception-filters' },
{ title: 'Guards', path: '/websockets/guards' },
]
},
{
title: 'Recipes',
isOpened: false,
children: [
{ title: 'SQL (TypeORM) [TBC]', path: '/recipes/sql-typeorm' },
{ title: 'NoSQL (MongoDB) [TBC]', path: '/recipes/nosql-mongodb' },
{ title: 'CQRS + Event Sourcing [TBC]', path: '/recipes/nosql-mongodb' },
{ title: 'Passport JWT integration [TBC]', path: '/recipes/nosql-mongodb' },
{ title: 'Swagger [TBC]', path: '/recipes/nosql-mongodb' },
{ title: 'Open Graph [TBC]', path: '/recipes/open-graph' },
{ title: 'Redis Socket.io [TBC]', path: '/recipes/redis-socketio' },
],
},
{
title: 'FAQ',
isOpened: false,
children: [
{ title: 'Best Practices', path: '/faq/best-practices' },
{ title: 'Express Instance', path: '/faq/express-instance' },
{ title: 'Module Reference', path: '/faq/module-reference' },
{ title: 'Global Route Prefix', path: '/faq/global-prefix' },
{ title: 'Lifecycle Events', path: '/faq/lifecycle-events' },
{ title: 'Hybrid Application', path: '/faq/hybrid-application' },
{ title: 'Lazy Microservice Client', path: '/faq/lazy-microservice' },
{ title: 'Multiple Simultaneous Servers', path: '/faq/multiple-servers' },
{ title: 'Examples', path: '/faq/examples' },
]
}
];
}

View File

@@ -0,0 +1,3 @@
<p>
async-components works!
</p>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { AsyncComponentsComponent } from './async-components.component';
describe('AsyncComponentsComponent', () => {
let component: AsyncComponentsComponent;
let fixture: ComponentFixture<AsyncComponentsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ AsyncComponentsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(AsyncComponentsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-async-components',
templateUrl: './async-components.component.html',
styleUrls: ['./async-components.component.scss']
})
export class AsyncComponentsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,3 @@
<p>
dependency-injection works!
</p>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { DependencyInjectionComponent } from './dependency-injection.component';
describe('DependencyInjectionComponent', () => {
let component: DependencyInjectionComponent;
let fixture: ComponentFixture<DependencyInjectionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ DependencyInjectionComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(DependencyInjectionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-dependency-injection',
templateUrl: './dependency-injection.component.html',
styleUrls: ['./dependency-injection.component.scss']
})
export class DependencyInjectionComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,67 @@
<div class="content">
<h3>Components</h3>
<p>
Almost everything is a component Service, Repository, Factory [...] and they can be <strong>injected</strong> into controllers or to another components through <code>constructor</code>.
</p>
<figure><img src="/assets/Components_1.png" /></figure>
<p>
In the previous article, we've built a simple <code>CatsController</code>.
</p>
<p>
The controllers should only handle HTTP requests and delegate more complex tasks to the <strong>components</strong>.
The components are a plain TypeScript classes with <code>@Component()</code> decorator.
</p>
<blockquote>
<strong>Hint</strong> Since Nest enables the possibility to design, organize the dependencies in more OO-way, we strongly recommend
to follow the <strong>SOLID</strong> principles.
</blockquote>
<p>
Let's create a <code>CatsService</code> component:
</p>
<span class="filename">cats.service.ts</span>
<pre><code class="language-typescript">{{ catsService }}</code></pre>
<p>
There's nothing specifically about components. Here's a <code>CatsService</code>, basic class with one property and two methods.
The only difference is that it has the <code>@Component()</code> decorator. The <code>@Component()</code> attaches the metadata, so Nest knows that this class is a Nest component.
</p>
<blockquote>
<strong>Notice</strong> There's a <code>Cat</code> interface here. I didn't mention about it, because the schema is exact same as in <code>CreateCatDto</code> class.
</blockquote>
<p>
Since we've the service class already done, let's use it inside the <code>CatsController</code>:
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ catsController }}</code></pre>
<p>
The <code>CatsService</code> is injected through the class constructor.
Don't be afraid about the <code>private readonly</code> shortened syntax.
It means that we've created and initialized the <code>catsService</code> member in the same location.
</p>
<h4>Dependency Injection</h4>
<p>
Nest is built around the strong design pattern, which is commonly known as a <strong>Dependency Injection</strong>.
There's a great article about this concept in the <a href="https://angular.io/guide/dependency-injection" target="blank">offical Angular documentation</a>.
</p>
<blockquote>
<strong>Hint</strong> Learn more about the <strong>Dependency Injection</strong> in Nest <a href="http://docs.nestjs.com/advanced/dependency-injection" target="blank">here</a>.
</blockquote>
<p>
It's extremely easy to manage dependencies with <strong>TypeScript</strong>, because Nest will recognize your dependencies just by <strong>type</strong>.
This single line:
</p>
<pre><code class="language-typescript">{{ constructorSyntax }}</code></pre>
<p>
Is everything what you have to do. There is one important thing to knowyou must have <code>emitDecoratorMetadata</code> option set to <code>true</code> in your <code>tsconfig.json</code> file.
That's it.
</p>
<h4>Last step</h4>
<p>
The last thing is to tell the module that something called <code>CatsService</code> truly exists.
The only way to do it is to open the <code>app.module.ts</code> file, and add the service into the <code>components</code> array of the <code>@Module()</code> decorator.
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ appModule }}</code></pre>
<p>
Now Nest will smoothly resolve the dependencies of the <code>CatsController</code> class.
</p>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ComponentsComponent } from './components.component';
describe('ComponentsComponent', () => {
let component: ComponentsComponent;
let fixture: ComponentFixture<ComponentsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ComponentsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ComponentsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,73 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-components',
templateUrl: './components.component.html',
styleUrls: ['./components.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ComponentsComponent extends BasePageComponent {
get catsService() {
return `
import { Component } from '@nestjs/common';
import { Cat } from './interfaces/cat.interface';
@Component()
export class CatsService {
private readonly cats: Cat[] = [];
create(cat: Cat) {
this.cats.push(cat);
}
findAll(): Cat[] {
return this.cats;
}
}
`;
}
get catsController() {
return `
import { Controller, Get, Post, Body } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
import { CatsService } from './cats.service';
import { Cat } from './interfaces/cat.interface';
@Controller('cats')
export class CatsController {
constructor(private readonly catsService: CatsService) {}
@Post()
async create(@Body() createCatDto: CreateCatDto) {
this.catsService.create(createCatDto);
}
@Get()
async findAll(): Promise<Cat[]> {
return this.catsService.findAll();
}
}`;
}
get constructorSyntax() {
return `
constructor(private readonly catsService: CatsService) {}`;
}
get appModule() {
return `
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
import { CatsService } from './cats/cats.service';
@Module({
controllers: [CatsController],
components: [CatsService],
})
export class ApplicationModule {}
`;
}
}

View File

@@ -0,0 +1,198 @@
<div class="content">
<h3>Controllers</h3>
<p>The controllers layer is responsible for handling incoming <strong>requests</strong> (mostly HTTP).</p>
<figure><img src="/assets/Controllers_1.png" /></figure>
<p>
To tell Nest that <code>CatsController</code> is a controller, you have to attach <strong>metadata</strong> to the class.
Metadata can be attached using <strong>decorators</strong>.
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ catsController }}</code></pre>
<h4>Metadata</h4>
<p>
We're using <code>@Controller('cats')</code> here. This decorator is <strong>obligatory</strong>.
The <code>cats</code> is a prefix for each route in the class. The prefix is <strong>optional</strong>, but it reduces redundant boilerplate code,
so you don't have to repeat yourself every time, when you'd decide to create new endpoint.
</p>
<p>
There's a single public method, the <code>findAll()</code>, which returns an empty array.
The <code>@Get()</code> decorator tells Nest that it's necessary to create an endpoint for this route path,
and map every appropriate request to this handler. Since we declared the prefix for every route (<code>cats</code>),
Nest will map every <code>/cats</code> GET request here.
</p>
<p>
When client would call this endpoint, Nest will return with 200 status code, and the parsed <strong>JSON</strong>, so in this case - just an empty array.
How is that possible?
</p>
<p>
There're <strong>two possible approaches</strong> of manipulating the response:
</p>
<table>
<tr>
<td>Standard (recommended)</td>
<td>
We're treating the handlers in the same way as a plain functions. When we return the JavaScript object or array, it'd be <strong>automatically</strong>
transformed to JSON. When we return the string, Nest will send just a string.
<br />
<br />
Furthermore, the response <strong>status code</strong> is always 200 by default, except POST requests, when it's 201.
We can easily change this behaviour by adding the <code>@HttpCode(...)</code> decorator at a handler-level.
</td>
</tr>
<tr>
<td>Express</td>
<td>
We can use the express <a href="http://expressjs.com/en/api.html#res" target="blank">response object</a>,
which we can inject using <code>@Res()</code> decorator in the function signature, for example <code>findAll(@Res() response)</code>
</td>
</tr>
</table>
<blockquote>
<strong>Notice!</strong> It's forbidden to use both two approaches at the same time. Nest detects whether handler is using <code>@Res()</code>, and if it's thruth - the standard way is disabled for this single route.
</blockquote>
<h4>Request object</h4>
<p>
A lot of endpoints need an access to the client <strong>request</strong> details.
In fact, Nest is using express <a href="http://expressjs.com/en/api.html#req" target="blank">request object</a>.
We can force Nest to inject the request object into handler using <code>@Req()</code> decorator.
</p>
<blockquote>
<strong>Hint</strong> There's a <code>@types/express</code> package and we strongly recommend to use it.
</blockquote>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ requestObject }}</code></pre>
<p>
The request object contains headers, params, and e.g. body of the request, but in most cases, it's not necessary to grab them manually.
We can use <strong>dedicated decorators</strong> instead, such as <code>@Body()</code> or <code>@Query()</code>, which are available out of the box.
Below is a comparision of the decorators with the plain express objects.
</p>
<table>
<tbody>
<tr>
<td><code>@Request()</code></td>
<td><code>req</code></td>
</tr>
<tr>
<td><code>@Response()</code></td>
<td><code>res</code></td>
</tr>
<tr>
<td><code>@Next()</code></td>
<td><code>next</code></td>
</tr>
<tr>
<td><code>@Session()</code></td>
<td><code>req.session</code></td>
</tr>
<tr>
<td><code>@Param(param?: string)</code></td>
<td><code>req.params</code> / <code>req.params[param]</code></td>
</tr>
<tr>
<td><code>@Body(param?: string)</code></td>
<td><code>req.body</code> / <code>req.body[param]</code></td>
</tr>
<tr>
<td><code>@Query(param?: string)</code></td>
<td><code>req.query</code> / <code>req.query[param]</code></td>
</tr>
<tr>
<td><code>@Headers(param?: string)</code></td>
<td><code>req.headers</code> / <code>req.headers[param]</code></td>
</tr>
</tbody>
</table>
<h4>More endpoints</h4>
<p>
We've already created an endpoint to fetch the data. It'd be great to provide a way of creating the new records too.
So.. What we're waiting for?! Let's create the <strong>POST</strong> handler:
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ postEndpoint }}</code></pre>
<p>
It's really easy. Nest provides the rest of those endpoints decorators in the same fashion -
<code>@Put()</code>, <code>@Delete()</code> etc..
</p>
<h4>Async / await</h4>
<p>
We love modern JavaScript, and we know that the data extraction is mostly <strong>asynchronous</strong>.
That's why Nest supports <code>async</code> functions, and works pretty well with them.
</p>
<blockquote>
<strong>Hint</strong> Learn more about <code>async / await</code> <a href="https://kamilmysliwiec.com/typescript-2-1-introduction-async-await" target="blank">here</a>!
</blockquote>
<p>
Every async function has to return the <code>Promise</code>. It means that you can return deffered value and
Nest will resolve it by itself. Let's have a look on the below example:
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ asyncExample }}</code></pre>
<p>
It's fully valid.
</p>
<p>
Furthermore, Nest route handlers are even more powerful. They can return the RxJS
<a href="http://reactivex.io/rxjs/class/es6/Observable.js~Observable.html" target="blank">observable streams</a>.
It makes the migration between simple web application and the Nest microservice much easier.
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ observableExample }}</code></pre>
<h4>POST handler</h4>
<p>
That's weird that the POST route handler doesn't accept any client params. We should definitely
expect the <code>@Body()</code> argument here.
</p>
<p>
Firstly, we need to establish the <strong>DTO</strong> (Data Transfer Object) schema. We could do it using <strong>TypeScript</strong>
interfaces, or by simple classes. What may be surprising, we recommend to use <strong>classes</strong>. Why?
The classes are the part of the JavaScript ES6 standard, so they're just plain functions. On the other hand, TypeScript interfaces are removed
during the transpilation, Nest can't refer to them. It's important, because features such as <strong>Pipes</strong> enables additional possibilities when they've access to the metatype
of the variable.
</p>
<p>
Let's create the <code>CreateCatDto</code>:
</p>
<span class="filename">dto/create-cat.dto.ts</span>
<pre><code class="language-typescript">{{ createCatSchema }}</code></pre>
<p>
It has only three basic properties. All of them are marked as a <code>readonly</code>, because we should
always try to make our functions as pure as possible.
</p>
<p>
Now we can use the schema inside the <code>CatsController</code>:
</p>
<span class="filename">cats.controller.ts</span>
<pre><code class="language-typescript">{{ exampleWithBody }}</code></pre>
<p>
<a href="http://expressjs.com/" target="blank">Expressjs</a> doesn't parse the body by default.
We need the middleware, which name is <a href="https://github.com/expressjs/body-parser" target="blank">body-parser</a>.
The usage is really simple, because Nest instance provides the <code>use()</code> method. It's a wrapper to the native express <code>use()</code> function:
</p>
<span class="filename">server.ts</span>
<pre><code class="language-typescript">{{ bodyParser }}</code></pre>
<h4>Last step</h4>
<p>
The controller is prepared, and ready to use, but Nest doesn't know that <code>CatsController</code> exists,
so it won't create an instance of this class. We need to tell about it.
</p>
<p>
The controller always belongs to the module, which mentioned about its in <code>controllers</code> array within <code>@Module()</code> decorator.
Since we don't have any other modules except the root <code>AppModule</code>, let's use it for now:
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ appModule }}</code></pre>
<p>
Tada! We attached the metadata to the module class, so now Nest can easily reflect which controllers have to be initialized.
</p>
<h4>Express approach</h4>
<p>
The second way of manipulating the response is to use express <a href="http://expressjs.com/en/api.html#res" target="blank">response object</a>.
It was the only available option until <strong>Nest 4</strong>. To inject the response object, we need to use <code>@Res()</code> decorator.
To show the differences, i'm going to rewrite the <code>CatsController</code>:
</p>
<pre><code class="language-typescript">{{ expressWay }}</code></pre>
<p>
This manner is much less clear from my point of view. I definitely prefer the first approach, but to make the Nest <strong>backward compatible</strong>
with the previous versions, this method is still available. Also, the <strong>response object</strong> gives more flexibility - you've full control of the response.
</p>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ControllersComponent } from './controllers.component';
describe('ControllersComponent', () => {
let component: ControllersComponent;
let fixture: ComponentFixture<ControllersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ControllersComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ControllersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,132 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-controllers',
templateUrl: './controllers.component.html',
styleUrls: ['./controllers.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ControllersComponent extends BasePageComponent {
get catsController(): string {
return `
import { Controller, Get } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll() {
return [];
}
}`;
}
get requestObject(): string {
return `
import { Controller, Get, Req } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Get()
findAll(@Req() request) {
return [];
}
}`;
}
get postEndpoint() {
return `
import { Controller, Get, Post } from '@nestjs/common';
@Controller('cats')
export class CatsController {
@Post()
create() {
// TODO: Add some logic here
}
@Get()
findAll() {
return [];
}
}`;
}
get asyncExample() {
return `
@Get()
async findAll(): Promise<any[]> {
return [];
}`;
}
get observableExample() {
return `
@Get()
findAll(): Observable<any[]> {
return Observable.of([]);
}`;
}
get createCatSchema() {
return `
export class CreateCatDto {
readonly name: string;
readonly age: number;
readonly breed: string;
}`;
}
get exampleWithBody() {
return `
@Post()
async create(@Body() createCatDto: CreateCatDto) {
// TODO: Add some logic here
}`;
}
get appModule() {
return `
import { Module } from '@nestjs/common';
import { CatsController } from './cats/cats.controller';
@Module({
controllers: [CatsController],
})
export class ApplicationModule {}`;
}
get bodyParser() {
return `
import * as bodyParser from 'body-parser';
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './modules/app.module';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
app.use(bodyParser.json());
await app.listen(3000);
}
bootstrap();`;
}
get expressWay() {
return `
import { Controller, Get, Post, Res, Body, HttpStatus } from '@nestjs/common';
import { CreateCatDto } from './dto/create-cat.dto';
@Controller('cats')
export class CatsController {
@Post()
create(@Res() res, @Body() createCatDto: CreateCatDto) {
// TODO: Add some logic here
res.status(HttpStatus.CREATED).send();
}
@Get()
findAll(@Res() res) {
res.status(HttpStatus.OK).json([]);
}
}`;
}
}

View File

@@ -0,0 +1,11 @@
<div class="content">
<h3>Exception Filters</h3>
<p>
In Nest there's an <strong>exceptions layer</strong>, which responsibility is to catch the unhandled exceptions and
return the appropriate response to the end-user.
</p>
<figure><img src="/assets/Filter_1.png" /></figure>
<p>
Every unrecognized exception is handled, and when it was HTTP request, user receives the below response:
</p>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ExceptionFiltersComponent } from './exception-filters.component';
describe('ExceptionFiltersComponent', () => {
let component: ExceptionFiltersComponent;
let fixture: ComponentFixture<ExceptionFiltersComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ExceptionFiltersComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ExceptionFiltersComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-exception-filters',
templateUrl: './exception-filters.component.html',
styleUrls: ['./exception-filters.component.scss']
})
export class ExceptionFiltersComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,39 @@
<div class="content">
<h3>First Steps</h3>
<p>
In this set of articles you'll learn the <strong>core fundamentals</strong> of Nest.
The main idea is to get familiar with essential Nest application building blocks.
You'll build a basic CRUD application which features covers a lot of ground at an introductory level.
</p>
<h3>Setup</h3>
<p>
Setting up a new project is quite simple with <a href="https://github.com/kamilmysliwiec/nest-typescript-starter" target="blank">Starter repository</a>.
Just make sure that you have <a href="https://www.npmjs.com/" target="blank">npm</a> installed then use following commands in your OS terminal:
</p>
<pre><code class="language-bash">
$ git clone https://github.com/kamilmysliwiec/nest-typescript-starter.git project
$ cd project
$ npm install
$ npm run start</code></pre>
<p>The <code>project</code> directory will contain several core files inside <code>src</code> directory.
Following the convention, newly created modules should be placed inside <code>modules</code> directory.
<table>
<tr>
<td><code>server.ts</code></td>
<td>The entry file of the application. It uses <code>NestFactory</code> to create the Nest application instance.</td>
</tr>
<tr>
<td><code>modules/app.module.ts</code></td>
<td>Defines <code>AppModule</code>, the root module of the application. Right now it doesn't declare any metadata, because we didn't create any component and controller yet.</td>
</tr>
</table>
<p>
The <code>server.ts</code> contains single async function, which responsibility is to <strong>bootstrap</strong> our application:
</p>
<span class="filename">server.ts</span>
<pre><code class="language-typescript">{{ bootstrap }}</code></pre>
<p>
We should always create the Nest application instance using the <code>NestFactory</code>.
The <code>create()</code> method returns object, which fulfils <code>INestApplication</code> interface, and provides a set of usable methods, which are well described in the next articles.
</p>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { FirstStepsComponent } from './first-steps.component';
describe('FirstStepsComponent', () => {
let component: FirstStepsComponent;
let fixture: ComponentFixture<FirstStepsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ FirstStepsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(FirstStepsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,23 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-first-steps',
templateUrl: './first-steps.component.html',
styleUrls: ['./first-steps.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class FirstStepsComponent extends BasePageComponent {
get bootstrap(): string {
return `
import { NestFactory } from '@nestjs/core';
import { ApplicationModule } from './modules/app.module';
async function bootstrap() {
const app = await NestFactory.create(ApplicationModule);
await app.listen(3000);
}
bootstrap();
`;
}
}

View File

@@ -0,0 +1,3 @@
<p>
guards works!
</p>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { GuardsComponent } from './guards.component';
describe('GuardsComponent', () => {
let component: GuardsComponent;
let fixture: ComponentFixture<GuardsComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ GuardsComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(GuardsComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-guards',
templateUrl: './guards.component.html',
styleUrls: ['./guards.component.scss']
})
export class GuardsComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,38 @@
<div class="content">
<h3>Introduction</h3>
<p>Nest is a powerful web framework for <a href="http://nodejs.org" target="_blank">Node.js</a>, which helps you effortlessly build efficient, scalable applications. It uses modern JavaScript, is built with <a href="http://www.typescriptlang.org" target="_blank">TypeScript</a> and combines best concepts of both OOP (Object Oriented Progamming) and FP (Functional Programming).</p>
<p>Nest is using well-known—<a href="https://expressjs.com/" target="_blank">Express</a> library under the hood. This means that you can quickly start using Nest without worrying about third party plugins.</p>
<h3>Installation</h3>
<p>Install the TypeScript Starter Project with <strong>Git</strong>:</p>
<pre><code class="language-bash">
$ git clone https://github.com/kamilmysliwiec/nest-typescript-starter.git project
$ cd project
$ npm install
$ npm run start</code></pre>
<p>Start a New Project from Scratch with <strong>NPM</strong>:</p>
<pre><code class="language-bash">
$ npm i --save @nestjs/core @nestjs/common @nestjs/microservices @nestjs/websockets @nestjs/testing reflect-metadata rxjs</code></pre>
<h3>Philosophy</h3>
<p>JavaScript is awesome. Now, the front end world is rich in variety of tools. We have a lot of amazing projects / libraries such as <a href="https://angular.io/" target="_blank">Angular</a>, <a href="https://github.com/facebook/react" target="_blank">React</a> or <a href="https://github.com/vuejs/vue" target="_blank">Vue</a>, which improves our development process and makes our applications fast and extensible.</p>
<p><a href="http://nodejs.org" target="_blank">Node.js</a> gave us a possibility to use this language also on the server side. There are a lot of superb libraries, helpers and tools for node, but non of them do not solve the main problem - the architecture.</p>
<p>We want to create scalable, loosely coupled and easy to maintain applications. That's why the <strong>Nest</strong> has been introduced. Let's show the entire world node.js potential together!</p>
<h3>Features</h3>
<ul>
<li>Built on top of <a href="http://www.typescriptlang.org" target="_blank">TypeScript</a></li>
<li>Easy to learn - syntax is similar to <a href="https://angular.io/" target="_blank">Angular</a></li>
<li>Based on well-known libraries (<a href="https://github.com/expressjs/express" target="_blank">Express</a> / <a href="https://github.com/socketio/socket.io" target="_blank">socket.io</a>) so its a familiar experience</li>
<li>Supremely useful Dependency Injection support, built-in asynchronous <strong>Inversion of Control</strong> container</li>
<li><strong>Hierarchical injector</strong> - increase abstraction in your application by creating reusable, loosely coupled modules with type injection</li>
<li><strong>WebSockets</strong> module (based on <a href="https://github.com/socketio/socket.io" target="_blank">socket.io</a>, although you can use any other library using <code>WsAdapter</code>)</li>
<li>Own modularity system (split your system into reusable modules)</li>
<li>Reactive <strong>microservices</strong> support with message patterns (built-in transport via TCP / <a href="https://redis.io/" target="_blank">Redis</a>, but you can use any other type of communication using <code>CustomTransportStrategy</code>)</li>
<li>Exceptions handler layer, exception filters, <strong>synchronous &amp; asynchronous pipes</strong> layer (e.g. validation purposes)</li>
<li><strong>Guards</strong> & Reflector - attach additional logic in more declaritive way (e.g. role-based access control)</li>
<li>Testing utilities (both <strong>e2e & unit</strong> tests)</li>
</ul>
<h3>People</h3>
<ul>
<li>Author - <a href="https://kamilmysliwiec.com" title="Kamil Mysliwiec Blog | Full-Stack Software Engineer" target="blank">Kamil Myśliwiec</a></li>
<li>Website - <a href="http://nestjs.com" title="Nest - A node.js framework built on top of TypeScript" target="blank">http://nestjs.com</a></li>
</ul>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { IntroductionComponent } from './introduction.component';
describe('IntroductionComponent', () => {
let component: IntroductionComponent;
let fixture: ComponentFixture<IntroductionComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ IntroductionComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntroductionComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,10 @@
import { Component, ChangeDetectionStrategy } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-introduction',
templateUrl: './introduction.component.html',
styleUrls: ['./introduction.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class IntroductionComponent extends BasePageComponent {}

View File

@@ -0,0 +1,89 @@
<div class="content">
<h3>Middlewares</h3>
<p>
The middleware is a function, which is called <strong>before</strong> route handler.
Middleware functions have an access to the <a href="http://expressjs.com/en/4x/api.html#req" target="blank">request</a>
and <a href="http://expressjs.com/en/4x/api.html#res" target="blank">response</a> objects, so they can modify them. They can also be something like a <strong>barrier</strong> - if middleware function won't call <code>next()</code>,
the request will never be handled by the route handler.
</p>
<figure><img src="/assets/Middlewares_1.png" /></figure>
<p>
Middleware is a class with <code>@Middleware()</code> decorator. This class should implement the <code>NestMiddleware</code> interface.
Let's create an example, the <code>LoggerMiddleware</code> class.
</p>
<span class="filename">logger.middleware.ts</span>
<pre><code class="language-typescript">{{ loggerMiddleware }}</code></pre>
<p>
The <code>resolve()</code> method has to return the regular expressjs middleware <code>(req, res, next) => void</code>.
</p>
<h4>Dependency Injection</h4>
<p>
There's no exception when it comes to the middlewares. Same as components and controllers, they can inject dependencies which belong to the same module.
</p>
<h4>Where to put the middlewares?</h4>
<p>
The middlewares shouldn't be listed in the <code>@Module()</code> decorator.
We've to setup them using <code>configure()</code> method of the module class.
Modules which include middlewares have to implement the <code>NestModule</code> interface.
Let's setup the <code>LoggerMiddleware</code> at the <code>ApplicationModule</code> level.
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ applicationModule }}</code></pre>
<blockquote>
<strong>Hint</strong> We could pass here (inside <code>forRoutes()</code>) the single object and just use <code>RequestMethod.ALL</code>.
</blockquote>
<p>
In above example we setuped <code>LoggerMiddleware</code> for <code>/cats</code> route handlers, which we've registered inside <code>CatsController</code>.
The <code>MiddlewareConsumer</code> is a helper class. It provides several methods to work with the middlewares. All of them can be simply <strong>chained</strong>.
Let's go through those methods.
</p>
<p>
The <code>forRoutes()</code> can take single object, multiple objects, controller class and even multiple controller classes.
In most cases you'll probably just pass the <strong>controllers</strong> and separate them by comma. Below is an example with the single controller:
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ applicationModuleByControllers }}</code></pre>
<blockquote>
<strong>Hint</strong> The <code>apply()</code> method can take single middleware or an <strong>array of middlewares</strong>.
</blockquote>
<h4>Pass arguments to the middleware</h4>
<p>
Sometimes the behaviour of the middleware depends on the custom values e.g. array of user roles, options object etc.
We can pass additional arguments to the <code>resolve()</code> using <code>with()</code> method. See below example:
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ applicationModuleWithMethod }}</code></pre>
<p>
We passed custom string - <code>ApplicationModule</code> to the <code>with()</code> method.
Now we've to adjust the <code>resolve()</code> method of the <code>LoggerMiddleware</code>.
</p>
<span class="filename">logger.middleware.ts</span>
<pre><code class="language-typescript">{{ loggerMiddlewareWithArgs }}</code></pre>
<p>
The value of the <code>name</code> property would be <code>ApplicationModule</code>.
</p>
<h4>Async Middlewares</h4>
<p>
There's no contraindications to return the <code>async</code> function from the <code>resolve()</code> method.
Also, it's possible to make the <code>resolve()</code> method <code>async</code> too. This pattern is called <strong>deffered middleware</strong>.
</p>
<span class="filename">logger.middleware.ts</span>
<pre><code class="language-typescript">{{ defferedMiddleware }}</code></pre>
<h4>Functional Middlewares</h4>
<p>
The <code>LoggerMiddleware</code> is quite short. It has no members, no additional methods, no dependencies.
Why we can't just use simple function? It's a good question, cause in fact - we can do it.
This type of the middleware is called <strong>functional middleware</strong>.
Let's transform the logger into the function.
</p>
<span class="filename">logger.middleware.ts</span>
<pre><code class="language-typescript">{{ functionalMiddleware }}</code></pre>
<p>
Now use it within the <code>ApplicationModule</code>.
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ applyFunctionalMiddleware }}</code></pre>
<blockquote>
<strong>Notice</strong> Let's consider to use <strong>functional middlewares</strong> everytime when your middleware doesn't have any dependencies.
</blockquote>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { MiddlewaresComponent } from './middlewares.component';
describe('MiddlewaresComponent', () => {
let component: MiddlewaresComponent;
let fixture: ComponentFixture<MiddlewaresComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ MiddlewaresComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MiddlewaresComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,135 @@
import { Component, OnInit } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-middlewares',
templateUrl: './middlewares.component.html',
styleUrls: ['./middlewares.component.scss']
})
export class MiddlewaresComponent extends BasePageComponent {
get loggerMiddleware() {
return `
import { Middleware, NestMiddleware, ExpressMiddleware } from '@nestjs/common';
@Middleware()
export class LoggerMiddleware implements NestMiddleware {
resolve(...args: any[]): ExpressMiddleware {
return (req, res, next) => {
console.log('Request...');
next();
};
}
}`;
}
get applicationModule() {
return `
import { Module, NestModule, MiddlewaresConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
modules: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(LoggerMiddleware).forRoutes(
{ path: '/cats', method: RequestMethod.GET },
{ path: '/cats', method: RequestMethod.POST },
);
}
}`;
}
get applicationModuleByControllers() {
return `
import { Module, NestModule, MiddlewaresConsumer, RequestMethod } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
@Module({
modules: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(LoggerMiddleware).forRoutes(CatsController);
}
}`;
}
get applicationModuleWithMethod() {
return `
import { Module, NestModule, MiddlewaresConsumer } from '@nestjs/common';
import { LoggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';
@Module({
modules: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(LoggerMiddleware)
.with('ApplicationModule')
.forRoutes(CatsController);
}
}`;
}
get loggerMiddlewareWithArgs() {
return `
import { Middleware, NestMiddleware, ExpressMiddleware } from '@nestjs/common';
@Middleware()
export class LoggerMiddleware implements NestMiddleware {
resolve(name: string): ExpressMiddleware {
return (req, res, next) => {
console.log(\`[\${name}\] Request...\`); // [ApplicationModule] Request...
next();
};
}
}`;
}
get defferedMiddleware() {
return `
import { Middleware, NestMiddleware, ExpressMiddleware } from '@nestjs/common';
@Middleware()
export class LoggerMiddleware implements NestMiddleware {
async resolve(name: string): Promise<ExpressMiddleware> {
await someAsyncFn();
return async (req, res, next) => {
await someAsyncFn();
console.log(\`[\${name}\] Request...\`); // [ApplicationModule] Request...
next();
};
}
}`;
}
get functionalMiddleware() {
return `
export const loggerMiddleware = (req, res, next) => {
console.log(\`Request...\`);
next();
};`;
}
get applyFunctionalMiddleware() {
return `
import { Module, NestModule, MiddlewaresConsumer } from '@nestjs/common';
import { loggerMiddleware } from './common/middlewares/logger.middleware';
import { CatsModule } from './cats/cats.module';
import { CatsController } from './cats/cats.controller';
@Module({
modules: [CatsModule],
})
export class ApplicationModule implements NestModule {
configure(consumer: MiddlewaresConsumer): void {
consumer.apply(loggerMiddleware).forRoutes(CatsController);
}
}`;
}
}

View File

@@ -0,0 +1,89 @@
<div class="content">
<h3>Modules</h3>
<p>
Module is a class with <code>@Module()</code> decorator. The <code>@Module()</code> decorator provides metadata, which <strong>Nest</strong> is using to organize the application structure.
</p>
<figure><img src="/assets/Modules_1.png" /></figure>
<p>
Every Nest application has at least one module, the <strong>root module</strong>.
The root module is the place, where Nest is starting to arrange the application tree.
In fact, the root module could be the only module in your application, especially when the app is small, but it doesn't make sense.
In most cases you'll have several modules, each with closely related set of <strong>capabilities</strong>.
</p>
<p>
The <code>@Module()</code> decorator takes the single object whose properties describe the module.
Read about them in the below table:
</p>
<table>
<tr>
<td><code>components</code></td>
<td>the components that will be instantiated by the Nest injector and may be shared at least across this module.</td>
</tr>
<tr>
<td><code>controllers</code></td>
<td>the set of controllers which have to be created</td>
</tr>
<tr>
<td><code>modules</code></td>
<td>the list of imported modules that export the components which are necessary in this module</td>
</tr>
<tr>
<td><code>exports</code></td>
<td>the subset of <code>components</code> that should be available in the other modules</td>
</tr>
</table>
<p>
The module <strong>encapsulates</strong> components by default. It means that it isn't possible to inject the components which aren't directly the part of the current module, or they're not exported from the imported modules.
</p>
<h3>CatsModule</h3>
<p>
The <code>CatsController</code> and <code>CatsService</code> belong to the same application domain.
They should be moved to the feature module, the <code>CatsModule</code>.
</p>
<span class="filename">cats/cats.module.ts</span>
<pre><code class="language-typescript">{{ catsModule }}</code></pre>
<p>
I've created the <code>cats.module.ts</code> file and moved everything related to this module into the
<code>cats</code> directory. The last thing we need to do is to import this module into the root module which name is <code>ApplicationModule</code>.
</p>
<span class="filename">app.module.ts</span>
<pre><code class="language-typescript">{{ appModule }}</code></pre>
<p>
It's everything. Now Nest knows that besides <code>ApplicationModule</code>, it's essential to register the <code>CatsModule</code> too.
</p>
<h3>Shared Module</h3>
<p>
In Nest, modules <strong>are singletons</strong> by default, so you can share the <strong>same instance</strong> of the component between 2..* modules without any effort.
</p>
<blockquote>
<strong>Notice</strong> In the previous versions of the Nest (< 4), modules weren't singletons, we had to use <code>@Shared()</code> decorator which now is <strong>deprecated</strong>.
</blockquote>
<figure><img src="/assets/Shared_Module_1.png" /></figure>
<p>
Every module is a <strong>Shared Module</strong> in fact. Once created is reused by the each module.
Let's imagine that we're gonna share the <code>CatsService</code> instance between few modules.
</p>
<span class="filename">cats.module.ts</span>
<pre><code class="language-typescript">{{ catsModuleShared }}</code></pre>
<p>
Now each module which would import the <code>CatsModule</code> has an access to the <code>CatsService</code>, and will share the same instance with all of the modules which import this module too.
</p>
<blockquote>
<strong>Notice</strong> Never export the controller!
</blockquote>
<h3>Single Scope</h3>
<p>
They're modules which shouldn't be shared at all. To prevent module from being singleton, you can use <code>@SingleScope()</code>
decorator, which makes that Nest will always create the new instance of the module, when it's imported by another one.
</p>
<pre><code class="language-typescript">{{ singleScope }}</code></pre>
<h3>Dependency Injection</h3>
<p>
It's natural that module can <strong>inject</strong> components, which belongs to it (e.g. for the configuration purposes):
</p>
<span class="filename">cats.module.ts</span>
<pre><code class="language-typescript">{{ catsModuleDi }}</code></pre>
<p>
However, modules can't be injected by the components, because it creates a <a href="/advanced/circular-dependency">circular dependency</a>.
</p>
</div>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ModulesComponent } from './modules.component';
describe('ModulesComponent', () => {
let component: ModulesComponent;
let fixture: ComponentFixture<ModulesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ ModulesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ModulesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,81 @@
import { Component, OnInit, ChangeDetectionStrategy } from '@angular/core';
import { BasePageComponent } from '../page/page.component';
@Component({
selector: 'app-modules',
templateUrl: './modules.component.html',
styleUrls: ['./modules.component.scss'],
changeDetection: ChangeDetectionStrategy.OnPush
})
export class ModulesComponent extends BasePageComponent {
get catsModule() {
return `
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
components: [CatsService],
})
export class CatsModule {}
`;
}
get appModule() {
return `
import { Module } from '@nestjs/common';
import { CatsModule } from './cats/cats.module';
@Module({
modules: [CatsModule],
})
export class ApplicationModule {}
`;
}
get catsModuleShared() {
return `
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
components: [CatsService],
exports: [CatsService]
})
export class CatsModule {}
`;
}
get singleScope() {
return `
import { Module, SingleScope } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@SingleScope()
@Module({
controllers: [CatsController],
components: [CatsService],
exports: [CatsService]
})
export class CatsModule {}`;
}
get catsModuleDi() {
return `
import { Module } from '@nestjs/common';
import { CatsController } from './cats.controller';
import { CatsService } from './cats.service';
@Module({
controllers: [CatsController],
components: [CatsService],
})
export class CatsModule {
constructor(private readonly catsService: CatsService) {}
}`;
}
}

View File

@@ -0,0 +1,33 @@
declare var Prism;
import { ChangeDetectorRef, ElementRef, AfterViewChecked, Component } from '@angular/core';
@Component({
selector: 'app-base-page',
template: ``,
})
export class BasePageComponent implements AfterViewChecked {
private isHljsInitialized = false;
constructor(
private readonly cd: ChangeDetectorRef,
private readonly el: ElementRef) {}
ngAfterViewChecked() {
this.initHljs();
}
private initHljs() {
if (this.isHljsInitialized) {
return;
}
const tags = this.el.nativeElement.querySelectorAll('code');
[].forEach.call(tags, (code: HTMLElement) => {
if (code.className) {
Prism.highlightElement(code);
this.isHljsInitialized = true;
this.cd.markForCheck();
}
}
);
}
}

View File

@@ -0,0 +1,3 @@
<p>
pipes works!
</p>

View File

@@ -0,0 +1,25 @@
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { PipesComponent } from './pipes.component';
describe('PipesComponent', () => {
let component: PipesComponent;
let fixture: ComponentFixture<PipesComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [ PipesComponent ]
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(PipesComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should be created', () => {
expect(component).toBeTruthy();
});
});

View File

@@ -0,0 +1,15 @@
import { Component, OnInit } from '@angular/core';
@Component({
selector: 'app-pipes',
templateUrl: './pipes.component.html',
styleUrls: ['./pipes.component.scss']
})
export class PipesComponent implements OnInit {
constructor() { }
ngOnInit() {
}
}

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { rootReducers } from './root-reducers';
import { rootInitialState } from './initial-state';
import { AppState } from './common';
import { rootEffects } from './root-effects';
const options = {
initialState: rootInitialState,
};
@NgModule({
imports: [
StoreModule.forRoot<AppState>(
rootReducers,
options,
),
EffectsModule.forRoot(rootEffects),
],
exports: [
StoreModule,
EffectsModule,
]
})
export class AppStoreModule { }

View File

@@ -0,0 +1 @@
export * from './interfaces';

View File

@@ -0,0 +1,4 @@
export interface Action<T> {
type: string;
payload?: T;
}

View File

@@ -0,0 +1,8 @@
import { RouterReducerState } from '@ngrx/router-store';
import { UserState } from '../../user/interfaces/user-state.interface';
export interface AppState {
router: RouterReducerState;
user: UserState;
}

View File

@@ -0,0 +1,2 @@
export * from './action.interface';
export * from './app-state.interface';

View File

@@ -0,0 +1,7 @@
import { AppState } from './common';
type Partial<T> = {
[P in keyof T]?: T[P];
};
export const rootInitialState: Partial<AppState> = {};

View File

@@ -0,0 +1,3 @@
export const rootEffects = [
];

View File

@@ -0,0 +1,8 @@
import { routerReducer } from '@ngrx/router-store';
import { userReducer } from './user/reducer';
export const rootReducers: any = {
router: routerReducer,
user: userReducer,
};

View File

@@ -0,0 +1,3 @@
export interface UserState {
accessToken: string;
}

Some files were not shown because too many files have changed in this diff Show More