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

10 Angular Tips: Modules, VSCode and best practices

By Loiane Groner

10 Angular Tips: Modules, VSCode and best practices

Are you working on Angular projects and still have some questions about the structure of your project? How to organize the modules and components of an application to facilitate data flow and project maintenance? How to make the whole team follow the same standards and improve productivity? Is there any easier way to work with prototyping even by integrating the front end with the server? How do I decrease the impact of migrations from one version to another? In this talk I share some of the experiences and decisions about code-sharing improvements and components I've learned by developing angular projects that are in production.

  • 3,250