10 Angular Tips: Modules, VSCode and best practices

Loiane Groner

Loiane Groner

Java, JavaScript + HTML5, Sencha, Cordova/Ionic, Angular, RxJS + all things reactive

Angular CLI

#1

VS Code

#2

VSCode config for the team!

Extensions

{
  // See http://go.microsoft.com/fwlink/?LinkId=827846
  // for the documentation about the extensions.json format
  "recommendations": [
    "loiane.angular-extension-pack",
    "loiane.ts-extension-pack"
  ]
}

extensions.json

TSLint, snippets, tests, debug, TS utilities, file formatting

Workspace config

{
  "editor.wordWrap": "off",
  "editor.minimap.enabled": false,

  "editor.codeActionsOnSave": { "source.fixAll.tslint": true },

  "files.autoSave": "afterDelay",
  "files.autoSaveDelay": 3000,

  "prettier.tabWidth": 2,
  "prettier.singleQuote": true,
  "prettier.useTabs": false,

  "html.format.wrapAttributes": "auto",
  "html.format.wrapLineLength": 0,
  "html.format.unformatted": "a, abbr, acronym, b, bdo, big, br, ...",

  "workbench.editor.enablePreview": false,

  "auto-rename-tag.activationOnLanguage": ["html", "xml"]
}

settings.json

VSCode tasks (Debug)

{
  "version": "0.2.0",
  "configurations": [
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome with ng serve",
      "url": "http://localhost:4200",
      "webRoot": "${workspaceRoot}"
    },
    {
      "type": "chrome",
      "request": "launch",
      "name": "Launch Chrome with ng test",
      "url": "http://localhost:9876/debug.html",
      "webRoot": "${workspaceRoot}"
    },
    {
      "type": "chrome",
      "request": "attach",
      "name": "Attach to Chrome",
      "port": 9222,
      "webRoot": "${workspaceRoot}"
    }
  ]
}

launch.json

Shortcuts for common tasks

{
  // See https://go.microsoft.com/fwlink/?LinkId=733558
  // for the documentation about the tasks.json format
  "version": "2.0.0",
  "tasks": [
    {
      "label": "build",
      "type": "npm",
      "script": "build",
      "presentation": {
        "reveal": "always"
      },
      "group": {
        "kind": "build",
        "isDefault": true
      },
      "problemMatcher": ["$tsc"]
    },
    {
      "label": "test",
      "type": "npm",
      "script": "test",
      "presentation": {
        "reveal": "always"
      },
      "group": {
        "kind": "test",
        "isDefault": true
      }
    }
  ]
}

tasks.json

keyboard shortcuts: tasks.json

// Place your key bindings in this file to overwrite the defaults
[
  // Tests
  {
    "key": "ctrl+shift+t",
    "command": "workbench.action.tasks.test"
  },

  // Lint
  {
    "key": "ctrl+shift+l",
    "command": "workbench.action.tasks.runTask",
    "args": "lint"
  },

  // Serve app
  {
    "key": "ctrl+shift+s",
    "command": "workbench.action.tasks.runTask",
    "args": "serve"
  }
]

keybindings-win.json

Environments

#3

What's your workflow?

Back-end code

Angular prototype

Back-end integration

Prod

build

You can config multiple environments

"scripts": {
  "ng": "ng",
  "dev": "ng serve --aot --configuration=dev",
  "start": "ng serve --proxy-config proxy.conf.js",
  "build":  "ng build --prod --aot --build-optimizer -op ../webapps/app"
},

package.json

Local development

Development with back-end

Production

Different Environments

export const environment = {
  production: false,
  baseUrl: '/'
};

environment.dev.ts

export const environment = {
  production: false,
  baseUrl: '/api/'
};

environment.ts

export const environment = {
  production: true,
  baseUrl: '../'
};

environment.prod.ts

angular.json

"configurations": {
  "production": {
    "fileReplacements": [
      {
        "replace": "src/environments/environment.ts",
        "with": "src/environments/environment.prod.ts"
      }
    ],
    ...
  },
  "dev": {
    "fileReplacements": [
      {
        "replace": "src/environments/environment.ts",
        "with": "src/environments/environment.dev.ts"
      }
    ]
  } 
  ...
}            

angular.json

Configuring the API endpoints

import { environment } from '@env/environment';

export class API {

  static readonly BASE_URL = environment.baseUrl;

  // LISTA DE CLIENTES
  static readonly CLIENTS = `${API.BASE_URL}clients`;
}

// para usar essa API:
// this.http.get(API.CLIENTES)

API.ts

Local REST server for prototyping

{
    "author" : [ {
        "id": 1,
        "name" : "Loiane"
      }, 
      {
        "id": 2,
        "name" : "abcdef"
      }
    ],
    "client" : []
}

myDb.json

npm install -g json-server
json-server --watch myDb.json

If you already have the JSON...

Angular Style Guide

#4

Modules

#5

Core

Feature 1

Shared

Feature 2

Feature 3

Core Module

  • Singleton Services
  • AuthService
  • APIService
  • etc 

Even with 'provideIn' services, Core modules are great to organize your code!

Shared Module

  • Reusable "dumb" componentes
  • Does not use components/classes from Core nor other modules
  • You can import 3rd-party libraries (material, ngx-bootstrap) and export them (single import)

Feature Module

  • Module X components
  • Only depend on Core or Shared (if possible)
  • Feature Module can have sub-modules (smaller bundle size for each chunk)

Modules, modules everywhere!

Lazy

Lazy Loading Strategies

const routes: Routes = [
  {
    path: '',
    redirectTo: 'module-1',
    pathMatch: 'full'
  },
  {
    path: 'module-1',
    component: Module1Component
  },
  {
    path: 'module-2',
    loadChildren: './module-2/module-2.module#Module2Module',
    data: { preload: true }  // pre-load - background
  },
  {
    path: 'module-3',
    loadChildren: './feature-3/module-3.module#Module3Module'
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, { 
    preloadingStrategy: AppCustomPreloader 
  })], // our custom logic
  exports: [RouterModule],
  providers: [AppCustomPreloader]
})
export class AppRoutingModule { }

Lazy Loading Strategies

import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';

export class AppCustomPreloader implements PreloadingStrategy {
  preload(route: Route, load: Function): Observable<any> {
    return route.data && route.data.preload ? load() : of(null);
  }
}

Containers and Components

size matters!

#6

  • Logical layer vs presentation layer
  • Easier to write tests
  • Easier to maintain the project later 

Presentational Components

  • Presentational / "dumb" components
  • Only displays values in the template (@Input properties)
  • Can emit events for any action (@Output properties)

Presentational Components

<mat-list>
  <app-task-item *ngFor="let task of tasks$ | async"
    [task]="task"
    (remove)="onRemove(task)"
    (edit)="onUpdate(task, $event)">
  </app-task-item>
</mat-list>

tasks-list.component.html

@Component({
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class TasksListComponent {

  @Input('tasks') tasks$: Observable<Task[]>;
  @Output() remove: EventEmitter<any> = new EventEmitter(false);
  @Output() edit: EventEmitter<any> = new EventEmitter(false);

  onRemove(task) {
    this.remove.emit(task);
  }

  onUpdate(task, changes) {
    this.edit.emit({task: task, updates: changes});
  }
}

tasks-list.component.ts

Containers

  • Smart Components
  • Components that help organizing the screen and functionality
  • Use presentational/dumb components
  • Listen to events from presentational components
  • Pass data to presentational components

Containers

<mat-card>
  <app-task-form (createTask)="onCreateTask($event)"></app-task-form>
    <mat-spinner *ngIf="isLoading$ | async; else taskList"></mat-spinner>
    <ng-template #taskList>
      <app-tasks-list
        [tasks]="tasks$"
        (remove)="onRemoveTask($event)"
        (edit)="onUpdateTask($event)">
      </app-tasks-list>
    </ng-template>
  <div class="error-msg" *ngIf="error$ | async as error">
    <p>{{ error }}</p>
  </div>
</mat-card>

tasks.component.html

Containers

export class TasksComponent implements OnInit {
  tasks$: Observable<Task[]>;
  isLoading$: Observable<boolean>;
  error$: Observable<string>;

  constructor(private taskStoreService: TaskStoreService) {}

  ngOnInit() {
    this.taskStoreService.dispatchLoadAction();
    this.tasks$ = this.taskStoreService.getTasks();
    this.isLoading$ = this.taskStoreService.getIsLoading();
    this.error$ = this.taskStoreService.getError();
  }

  onCreateTask(title) {
    this.taskStoreService.dispatchCreateAction(new Task(title));
  }

  onRemoveTask(task) {
    this.taskStoreService.dispatchRemoveAction(task);
  }

  onUpdateTask(event) {
    this.taskStoreService.dispatchUpdateAction(event.updates);
  }
}

tasks.component.ts

Smaller components are easier to maintain

<form (ngSubmit)="onSubmit()" novalidate>
  <mat-input-container class="example-full-width">
    <input matInput placeholder="What needs to be done?" type="text"
      name="title"
      [(ngModel)]="title"
      (keyup.escape)="clear()"
      autocomplete="off"
      autofocus>
  </mat-input-container>
</form>

component1.html

<mat-list-item class="list-item">
  <mat-checkbox color="primary" type="checkbox" [name]="task.id" [(ngModel)]="task.completed" (change)="onEdit()">
    <span [class.task-completed]="task.completed">{{task.title}}</span>
  </mat-checkbox>
  <span class="fill-remaining-space"></span>
  <span>
  <button mat-mini-fab (click)="onRemove()">
    <mat-icon>delete_forever</mat-icon>
  </button>
  <button mat-mini-fab (click)="openEditDialog()" [disabled]="task.completed" color="primary">
    <mat-icon>mode_edit</mat-icon>
  </button>
  </span>
</mat-list-item>

component2.html

<mat-list>
  <app-task-item *ngFor="let task of tasks$ | async"
    [task]="task"
    (remove)="onRemove(task)"
    (edit)="onUpdate(task, $event)">
  </app-task-item>
</mat-list>

component3.html

<mat-card>
  <form (ngSubmit)="onSubmit()" novalidate>
      <mat-input-container class="example-full-width">
        <input matInput placeholder="What needs to be done?" type="text"
          name="title"
          [(ngModel)]="title"
          (keyup.escape)="clear()"
          autocomplete="off"
          autofocus>
      </mat-input-container>
    </form>
    <mat-spinner *ngIf="isLoading$ | async; else taskList" style="margin:0 auto;"></mat-spinner>
    <ng-template #taskList>
     <mat-list>
      <mat-list-item class="list-item" *ngFor="let task of tasks$ | async">
      <mat-checkbox color="primary" type="checkbox" [name]="task.id" [(ngModel)]="task.completed" (change)="onEdit()">
        <span [class.task-completed]="task.completed">{{task.title}}</span>
      </mat-checkbox>
      <span class="fill-remaining-space"></span>
      <span>
      <button mat-mini-fab (click)="onRemove()">
        <mat-icon>delete_forever</mat-icon>
      </button>
      <button mat-mini-fab (click)="openEditDialog()" [disabled]="task.completed" color="primary">
        <mat-icon>mode_edit</mat-icon>
      </button>
      </span>
    </mat-list-item>
    </mat-list>
    </ng-template>
  <div class="error-msg" *ngIf="error$ | async as error">
    <p>{{ error }}</p>
  </div>
</mat-card>

component1.html

component3.html

Abstraction

#7

Wrappers

To keep consistency!

Http             HttpClient

@Injectable()
export class ApiService {
  constructor(public http: HttpClient) { }

  getRequest<T>(url: string, params?: any) {
    return this.http.get<ServerResponse<T>>(url, { 
        params: this.getQueryParams(params) 
    })
    .take(1); // as simple Ajax call, we only need 1 value
  }

  postRequest(url: string, body: any) {
    // POST 
  }

  private getQueryParams(params: any) {
    // logic to create the query params for http call
  }

  downloadFile(url: string, params?: any) {
    // logic for file download
  }

  uploadFile(file: AppFile | AppFile[], body?: any, params?: any) {
    // logic for file upload
  }
}  

api.service.ts

@Injectable()
export class TasksService {
  constructor(public http: ApiService) { }

  getAllTasksWithPaging(start = 0, limit = 100) {
    return this.http
      .getRequest<Task[]>(API.READ_TASKS, {start: start, limit: limit});
  }

  getById(id: number) {
     return this.http.getRequest<Task>(`${API.READ_TASKS}/${id}`);
  }
}  

tasks.service.ts

export interface ServerResponse<T> {
  data: T[];
  total: number;
  success?: boolean;
  errorMsg?: string;
}

Response

Inheritance vs composition

export abstract class BaseFormComponent implements IFormCanDeactivate {
  protected formSubmitAttempt: boolean;
  protected validateDirty = true;
  form: FormGroup;

  constructor() {
    this.formSubmitAttempt = false;
  }

  isFieldInvalid(field: string) {
    // some logic here
  }


  onSubmit() {
    this.formSubmitAttempt = true;
    // generic logic for all forms
  }

  onReset() { }

  canDeactivate(): Observable<boolean> {
    return this.modalService.showConfirm(
      'DoYouWantToLeavePage',
      'DoYouWantToLeavePageMsg'
    );
  }

  onCancel() {
    this.location.back();
  }
}
@Injectable()
export class CRUDService<T> {

  constructor(public http: HttpClient, private API_URL) {}

  load() {
    return this.http.get<T[]>(this.API_URL);
  }

  create(record: T) {
    return this.http.post<Task>(this.API_URL, record);
  }

  update(record: T) {
    return this.http.put<Task>(`${this.API_URL}/${record.id}`, record);
  }

  remove(id: string) {
    return this.http.delete<T>(`${this.API_URL}/${id}`);
  }
}
@Injectable()
export class TasksService extends CRUDService<Task> {

  constructor(public http: HttpClient) {
    super(http, API.TASKS_URL)
  }
}

Alias for Modules

#8

import { AuthService } from '../../../core/security/auth/auth.service';
import { environment } from '../../../../environments/environment';
import { AuthService } from '@my-project/core';
import { environment } from '@env/environment';
import { AuthService } from '../../../core/security/auth/auth.service';
import { environment } from '../../../../environments/environment';
import { AuthService } from '@my-project/core';
import { environment } from '@env/environment';
{
  "extends": "../tsconfig.json",
  "compilerOptions": {  
    ...
    
    "paths": {
      "@my-project/*": ["src/app/*"],
      "@env/*": ["src/environments/*"]
    }
  }
}

tsconfig.app.json

Build + bundle

#9

"scripts": {
  "ng": "ng",
  "build":  "ng build --prod --aot --build-optimizer -op ../webapps/app"
},

package.json

Don't forget the CI!

CLI v6.1

CLI v6.1

ng build --prod --source-map --vendor-source-map

 

"configurations": {
    "production": {
      "budgets": [
        {
         "type": "initial",
         "maximumWarning": "2mb",
         "maximumError": "5mb"
        }
      ],
      "fileReplacements": [
        {
          "replace": "src/environments/environment.ts",
          "with": "src/environments/environment.prod.ts"
        }
      ],
      ...
    }
    ...
}

Bundle Budgets

Docker Build - Quick Demo

https://docs.oracle.com/en/cloud/iaas/container-cloud/index.html

Shared Libraries

#10

Angular v6+

ng generate library my-lib --prefix lib

ng generate component my-component --project=my-lib

ng build my-lib

Angular v6+

import { MyLibModule } from 'my-lib';

@NgModule({
  imports: [
    BrowserModule,
    MyLibModule
  ],
  ...
})
export class AppModule { }

app.module.ts

<div style="text-align:center">
  <h1>
    Welcome to {{ title }}!
  </h1>
</div>
<lib-my-component></lib-my-component>

app.component.html

Angular v6+

"paths": {
      "my-lib": [
        "dist/my-lib"
      ],
      "my-lib/*": [
        "dist/my-lib/*"
      ]
    }

tsconfig

Angular v6+

Thank You!

References

Made with Slides.com