Showing posts with label Angular 8. Show all posts
Showing posts with label Angular 8. Show all posts

Sunday 8 September 2019

Building Angular apps with source maps and vendor chunk

Here is a quick tip about controlling the generation of source maps and vendor chunks in Angular 8 apps. Sourcemaps are built default in Angular according to the documentation @ https://angular.io/cli/build To be specific, the following command worked for me:


ng build --prod --sourceMap

In addition, the vendor chunk is now baked into the main chunk. To create a separate vendor chunk, run this: Angular 8 will put the vendor chunk into the main for optimizing the js code.
ng build --prod --sourceMap --vendor-chunk=true
In addition, it is recommended to analyze the bundle size with for example Webpack Bundle Analyzer like this: npm install -g webpack-bundle-analyzer Then add the following Npm run scripts to package.json:
    "buildwithstats": "ng b --sourceMap --prod --stats-json",
    "analyze": "webpack-bundle-analyzer --port 9901 dist/stats.json",
Now we have an interactive TreeMap view we can zoom into and see what is taking up space in our bundle!

Saturday 3 August 2019

Misc OpenId Connect setup for Angular 8

This article presents some sample basic setup for OpenId Connect setup for Angular 8, targeting the npm library openid-client and Thinktecture IdentityServer 4 as the STS. The following AutService sets up the UserManager first on the client side. I must mention here that the setup ended up quite similar to the setup I did for an ASP.NET Core backend API I did for a React frontend SPA.
import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { catchError } from 'rxjs/operators';
import { UserManager, User, WebStorageStateStore, Log } from 'oidc-client';
import { Constants } from '../constants';
import { Utils } from './utils';
import { AuthContext } from '../model/auth-context';

@Injectable()
export class AuthService {
  private _userManager: UserManager;
  private _user: User;
  authContext: AuthContext;

  constructor(private httpClient: HttpClient) {
    Log.logger = console;
    const config = {
      authority: Constants.stsAuthority,
      client_id: Constants.clientId,
      redirect_uri: `${Constants.clientRoot}assets/oidc-login-redirect.html`,
      scope: 'openid projects-api profile',
      response_type: 'id_token token',
      post_logout_redirect_uri: `${Constants.clientRoot}?postLogout=true`,
      userStore: new WebStorageStateStore({ store: window.localStorage }),
      automaticSilentRenew: true,
      silent_redirect_uri: `${Constants.clientRoot}assets/silent-redirect.html`
    };
    this._userManager = new UserManager(config);
    this._userManager.getUser().then(user => {
      if (user && !user.expired) {
        this._user = user;
        this.loadSecurityContext();
      }
    });
    this._userManager.events.addUserLoaded(args => {
      this._userManager.getUser().then(user => {
        this._user = user;
        this.loadSecurityContext();
      });
    });
  }

  login(): Promise<any> {
    console.log('Inside the new auth service!');
    return this._userManager.signinRedirect();
  }

  logout(): Promise<any> {
    return this._userManager.signoutRedirect();
  }

  isLoggedIn(): boolean {
    return this._user && this._user.access_token && !this._user.expired;
  }

  getAccessToken(): string {
    return this._user ? this._user.access_token : '';
  }

  signoutRedirectCallback(): Promise<any> {
    return this._userManager.signoutRedirectCallback();
  }

  loadSecurityContext() {
    this.httpClient.get<AuthContext>(`${Constants.apiRoot}Account/AuthContext`).subscribe(context => {
      this.authContext = context;
    }, error => console.error(Utils.formatError(error)));
  }
}


The login redirect file oidc-login-redirect.html goes into the assets folder of Angular 8 project.
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.4.1/oidc-client.min.js"></script>
<script>
  var config = {
    userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })
  };
  var mgr = new Oidc.UserManager(config);
  mgr.signinRedirectCallback().then(() => {
        window.history.replaceState({},
            window.document.title,
            window.location.origin);
        window.location = "/";
    }, error => {
        console.error(error);
    });

</script>

The silent redirect that automatically renews the token is in the file silent-redirect.html in the assets folder:
<script src="https://cdnjs.cloudflare.com/ajax/libs/oidc-client/1.5.1/oidc-client.min.js"></script>
<script>
  var config = {
    userStore: new Oidc.WebStorageStateStore({ store: window.localStorage })
  };
new Oidc.UserManager(config).signinSilentCallback()
        .catch((err) => {
            console.log(err);
        });

</script>
Then, the config in the ThinkTecture IdentityServer looks somewhat like this:
using System.Collections.Generic;
using IdentityServer4;
using IdentityServer4.Models;

namespace SecuringAngularApps.STS
{
    public class Config
    {
        public static IEnumerable<ApiResource> GetApiResources()
        {
            return new List<ApiResource>
            {
                new ApiResource("projects-api", "Projects API")
            };
        }

        public static IEnumerable<Client> GetClients()
        {
            return new List<Client>
            {
                new Client
                {
                    ClientId = "spa-client",
                    ClientName = "Projects SPA",
                    AllowedGrantTypes = GrantTypes.Implicit,
                    AllowAccessTokensViaBrowser = true,
                    RequireConsent = false,

                    RedirectUris =           { "http://localhost:4200/assets/oidc-login-redirect.html","http://localhost:4200/assets/silent-redirect.html" },
                    PostLogoutRedirectUris = { "http://localhost:4200/?postLogout=true" },
                    AllowedCorsOrigins =     { "http://localhost:4200/" },

                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile,
                        "projects-api"
                    },
                    IdentityTokenLifetime=120,
                    AccessTokenLifetime=120

                },
                new Client
                {
                    ClientId = "mvc",
                    ClientName = "MVC Client",
                    AllowedGrantTypes = GrantTypes.HybridAndClientCredentials,

                    ClientSecrets =
                    {
                        new Secret("secret".Sha256())
                    },

                    RedirectUris           = { "http://localhost:4201/signin-oidc" },
                    PostLogoutRedirectUris = { "http://localhost:4201/signout-callback-oidc" },

                    AllowedScopes =
                    {
                        IdentityServerConstants.StandardScopes.OpenId,
                        IdentityServerConstants.StandardScopes.Profile
                    },
                    AllowOfflineAccess = true

                }
            };
        }

        public static IEnumerable<IdentityResource> GetIdentityResources()
            {
                return new List<IdentityResource>
            {
                new IdentityResources.OpenId(),
                new IdentityResources.Profile(),
            };
            }
        }
    }
And in the API backend we wire up the STS like this in Asp.net Core in the Startup.cs:
using System;
using System.Linq;
using SecuringAngularApps.STS.Data;
using SecuringAngularApps.STS.Models;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.AspNetCore.Authentication.OpenIdConnect;
using Microsoft.AspNetCore.Authentication.Cookies;
using IdentityServer4.Services;
using SecuringAngularApps.STS.Quickstart.Account;

namespace SecuringAngularApps.STS
{
    public class Startup
    {
        public IConfiguration Configuration { get; }
        public IHostingEnvironment Environment { get; }

        public Startup(IConfiguration configuration, IHostingEnvironment environment)
        {
            Configuration = configuration;
            Environment = environment;
        }

        public void ConfigureServices(IServiceCollection services)
        {
            services.AddDbContext<ApplicationDbContext>(options =>
                options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

            services.AddIdentity<ApplicationUser, IdentityRole>()
                .AddEntityFrameworkStores<ApplicationDbContext>()
                .AddDefaultTokenProviders();

            services.AddCors(options =>
            {
                options.AddPolicy("CorsPolicy", corsBuilder =>
                {
                    corsBuilder.AllowAnyHeader()
                    .AllowAnyMethod()
                    .AllowAnyOrigin()
                    .AllowCredentials();
                });
            });
            services.AddTransient<IProfileService, CustomProfileService>();

            services.AddMvc();

            var builder = services.AddIdentityServer(options =>
                {
                    options.Events.RaiseErrorEvents = true;
                    options.Events.RaiseInformationEvents = true;
                    options.Events.RaiseFailureEvents = true;
                    options.Events.RaiseSuccessEvents = true;
                })
                .AddInMemoryIdentityResources(Config.GetIdentityResources())
                .AddInMemoryApiResources(Config.GetApiResources())
                .AddInMemoryClients(Config.GetClients())
                .AddAspNetIdentity<ApplicationUser>()
                .AddProfileService<CustomProfileService>();


            if (Environment.IsDevelopment())
            {
                builder.AddDeveloperSigningCredential();
            }
            else
            {
                throw new Exception("need to configure key material");
            }
        }

        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
                app.UseDatabaseErrorPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            app.UseStaticFiles();
            app.UseCors("CorsPolicy");

            app.UseIdentityServer();
            app.UseMvcWithDefaultRoute();
        }
    }
}

Thursday 1 August 2019

Intellisense of spy objects (mocks) in Jasmin tests

When creating unit tests or integration tests for Angular 8, we often use mocking - such as mocking services. We sometimes want to fix up the intellisense of our mocks when we create a spy object using Jasmine (of which Angular tests most often are written in - the 'NUnit for Javascript world'). Here is how we can achieve that. First off, create a new file called Spied.ts and add this Typescript:
export type Spied<T> = {
  [Method in keyof T]: jasmine.Spy;
};
A little bit of terminology here for .NET coders concerning Javascript tests:
  • Spy object : Mock object
  • Using .and.returnValue(of(somedata)) : Equal to using Moq Setup method to return some data for given method
  • Expect in Jasmin : Similar to Assert in MSTest and NUnit.
This builds a mapped type that maps to a jasmine.Spy object, see the explanation of a mapped type here: https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-1.html#mapped-types We now can declare our mock objects as a 'Spied' object like this example:
let mockHeroService: Spied<HeroService>
mockHeroService = jasmine.createSpyObj(['getHeroes', 'addHero', 'deleteHero']);
The great thing about this then is that we now have decent Intellisense in place! Look at this video from VsCode as proof! To handle dependency injection scenarios do like in this example:
import { VoterService, ISession } from "src/app/events";
import { of } from "rxjs";
import { Spied } from "src/app/common/Spied";
import { HttpClient } from "@angular/common/http";

describe('VoterService', () => {
  let voterService: VoterService;
  let mockHttp: Spied<HttpClient>;

  beforeEach(() => {
    mockHttp = <Spied<HttpClient>>jasmine.createSpyObj('mockHttp', ['delete', 'post']);
    voterService = new VoterService(mockHttp);
    console.log('Inside beforeEach');
  });

  describe('deleteVoter', () => {

    it('should remove the voter from the list of voters', () => {
      var session = { id: 6, name: "John", voters: ["joe", "john"] };
      mockHttp.delete.and.returnValue(of(false));
      console.log(voterService);

      voterService.deleteVoter(3, <ISession>session, "joe");
      expect(session.voters.length).toBe(1);
    });

  });

});



We can then adjust our constructor to include the '| any' modifier of the injected parameter:

import { Injectable, Inject } from '@angular/core';
import { ISession } from '../shared';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { catchError } from 'rxjs/operators';

@Injectable()
export class VoterService {
  constructor(@Inject(HttpClient) private http: HttpClient | any) {


  }

..

Note that we here adjust the constructor to not only accept the concrete class HttpClient but also 'any' allowing us to inject the mock object. We could alter this and introduce an interface for example instead for a more elegant approach. In case you get build errors like when running ng build stating that 'jasmine' could not be found, try out this: Inside tsconfig.json, explicitly add 'jasmine' for your 'types' like this:
{
  "compileOnSave": false,
  "compilerOptions": {
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "module": "esnext",
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "types": [ "jasmine" ],
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
  }
}

And then put the single line on top to import jasmine like this in Spied.ts:
import 'jasmine';

Wednesday 31 July 2019

Slide-in animation of forms in Angular 8

This is a cool feature in Angular 8, the support of animations. First off, we define our animation in a Typescript file like for example a sliding in translation of the X-axis. app-animation.ts
import { trigger, animate, transition, style, group, query } from '@angular/animations';

export const slideInAnimation = trigger('slideInAnimation', [
  // Transition between any two states
  transition('* <=> *', [
    // Events to apply
    // Defined style and animation function to apply
    // Config object with optional set to true to handle when element not yet added to the DOM
    query(':enter, :leave', style({ position: 'fixed', width: '100%', zIndex: 2 }), { optional: true }),
    // group block executes in parallel
    group([
      query(':enter', [
        style({ transform: 'translateX(100%)' }),
        animate('0.5s ease-out', style({ transform: 'translateX(0%)' }))
      ], { optional: true }),
      query(':leave', [
        style({ transform: 'translateX(0%)' }),
        animate('0.5s ease-out', style({ transform: 'translateX(-100%)' }))
      ], { optional: true })
    ])
  ])
]);


In our app.component.ts, we import this animation slideInAnimation like this:
import { slideInAnimation } from './app.animation';
@Component({
  selector: 'pm-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css'],
  animations: [ slideInAnimation ]
})
export class AppComponent {
  pageTitle = 'SomeAcme Product Management';


Note the association of the slideInAnimation above. And in the app component template (app.component.html) we just declare a template variable for the router outlet and set it up to trigger on route activation.
<div class="container" [@slideInAnimation]="o.isActivated ? o.activatedRoute: ''">
  <router-outlet #o="outlet"></router-outlet>
</div>
This video shows the effect. Note that this is an excerpt from Deborah Kurata's course on Angular routing from Pluralsight. Video of this technique:

Sunday 28 July 2019

Angular 8 - Datepicker and timepicker

I have been looking at the ngx-bootstrap components for Angular lately. These components are very nice and similar to the components that Bootstrap provides, for Angular. I have tested it out with Angular 8. First off, we import the datepicker and time picker of this library like this: ng add ngx-bootstrap --components datepicker ng add ngx-bootstrap --components timepicker Inside your app.module.ts, you should see the two components imported:
import { BsDatepickerModule } from "ngx-bootstrap/datepicker";
import { TimepickerModule } from 'ngx-bootstrap/timepicker';

It is also included to the imports. My sample projects app.module.ts looks like this:
import { BrowserModule } from "@angular/platform-browser";
import { FormsModule } from "@angular/forms";
import { NgModule } from "@angular/core";
import { HttpClientModule } from "@angular/common/http";
import { AppComponent } from "./app.component";
import { UserSettingsFormComponent } from "./user-settings-form/user-settings-form.component";
import { AppFocusDirective } from "./data/app-focus.directive";
import { ButtonsModule } from "ngx-bootstrap/buttons";
import { BsDatepickerModule } from "ngx-bootstrap/datepicker";
import { BsLocaleService } from 'ngx-bootstrap/datepicker';
import { BrowserAnimationsModule } from "@angular/platform-browser/animations";
import { defineLocale } from "ngx-bootstrap/chronos";
import { nbLocale } from "ngx-bootstrap/locale";
import { TimepickerModule, TimepickerConfig } from 'ngx-bootstrap/timepicker';
import { getTimepickerConfig  } from './TimepickerConfig';

defineLocale("nb", nbLocale);

@NgModule({
  declarations: [AppComponent, UserSettingsFormComponent, AppFocusDirective],
  imports: [
    BrowserModule,
    FormsModule,
    HttpClientModule,
    ButtonsModule.forRoot(),
    BsDatepickerModule.forRoot(),
    BrowserAnimationsModule,
    TimepickerModule.forRoot()
  ],
  providers: [ BsLocaleService,
   { provide: TimepickerConfig, useFactory: getTimepickerConfig }
  ],
  bootstrap: [AppComponent]
})
export class AppModule {

  constructor(private bsLocaleService: BsLocaleService) {
    this.bsLocaleService.use('nb');
  }
}

Observe the localization adjustments I have included here, for Norwegian localization of the date picker. I also include a custom factory function for setting up the time picker to not show the meridian button, i.e. show a 24 hours format of the time.
import { TimepickerConfig } from "ngx-bootstrap/timepicker";

// such override allows to keep some initial values

export function getTimepickerConfig(): TimepickerConfig {
  return Object.assign(new TimepickerConfig(), {
    hourStep: 2,
    minuteStep: 5,
    showMeridian: false,
    readonlyInput: false,
    mousewheel: true,
    showMinutes: true,
    showSeconds: false
  });
}


If you want to test out the adjustments I made, the Github repo is available here: https://github.com/toreaurstadboss/Angular8FormsNgxBootstrapTesting

Monday 22 July 2019

HttpInterceptor to cache clientside in Angular 8

I have been working with a sample project of HttpInterceptor to cache data clientside in Angular 8 hosted on Github. The repository is here: https://github.com/toreaurstadboss/AngularHttpInterceptorSample First off, the data is delivered by a small backend using Express.Js.The server.js file looks like this:
const express = require('express');
const path = require('path');
const fs = require('fs');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var towns = require('./server/routes/towns');

var app = express();

// view engine setup
app.set('views', path.join(__dirname, '/server/views'));
app.set('view engine', 'jade');

app.use(favicon(path.join(__dirname, 'dist/favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'dist')));

app.use(function (req, res, next) {
    if (!req.url.startsWith('/api')){
      req.url = '/api' + req.url;
    }
    console.log('got requst url: ' + req.url);
    console.log('Server handling request at Time:', new Date())
    next()
  })

// app.use('/', routes);
app.get('/api/towns', function(req, res) {
    let townsFileContents = fs.readFileSync(path.join(__dirname, 'server/data/townsandcities.json'), 'utf-8');
    let townsFound = JSON.parse(townsFileContents);
    console.log(townsFileContents);
    res.json(townsFound);
});

// app.get('*', function(req, res) {
//   res.sendFile(path.join(__dirname, 'dist/index.html'));
// });

// catch 404 and forward to error handler
// app.use(function(req, res, next) {
//     var err = new Error('Not Found');
//     err.status = 404;
//     next(err);
// });

// error handlers

// development error handler
// will print stacktrace
if (app.get('env') === 'development') {
    app.use(function(err, req, res, next) {
        res.status(err.status || 500);
        res.render('error', {
            message: err.message,
            error: err
        });
    });
}

// production error handler
// no stacktraces leaked to user
app.use(function(err, req, res, next) {
    res.status(err.status || 500);
    res.render('error', {
        message: err.message,
        error: {}
    });
});

var debug = require('debug')('server');

app.set('port', process.env.PORT || 3000);

app.listen(app.get('port'));

console.log('Listening on port: ' + app.get('port'));

module.exports = app;

Angular bits is written in Typescript instead of just Js. The Http Interceptor itself is a ES 6 class which is exported and caches http responses according to route urls.
import { HttpInterceptor, HttpRequest, HttpHandler, HttpEvent, HttpResponse } from '@angular/common/http';
import { Observable, of } from 'rxjs';
import { tap } from 'rxjs/operators';
import { CacheService } from './cache.service';


export class CacheInterceptor implements HttpInterceptor {

    constructor(private cacheService: CacheService) {
    }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {


        if (req.method !== 'GET') {
            this.cacheService.invalidateCache();
        }
        const cachedResponse: HttpResponse<any> = this.cacheService.get(req.url);
        if (cachedResponse !== undefined) {
         return of(cachedResponse);
        }

        return next.handle(req).pipe(
            tap(event => {
                if (event instanceof HttpResponse) {
                    this.cacheService.put(req.url, event);
                    let reponse: HttpResponse<any>;
                    reponse  = event;

                }
                return of(event);
            })
        );
    }

}

The CacheService caches responses to the client by route url as mentioned. It looks like this:
import { Injectable } from '@angular/core';
import { ICacheservice } from './icacheservice';
import { HttpResponse } from '@angular/common/http';

@Injectable({
  providedIn: 'root'
})
export class CacheService implements ICacheservice {
  private requests = { };
  put(url: string, response: HttpResponse): void {
    this.requests[url] = response;
  }
  get(url: string): HttpResponse | undefined {
    return this.requests[url] ? this.requests[url] : undefined;
  }
  invalidateCache(): void {
    this.requests = {};
  }
  expireItem(url: string, timeoutInMilliseconds: number) {
    const itemFound = this.get(url);
    if (itemFound !== undefined) {
      setTimeout(() => {
        this.requests[url] = undefined;
      }, timeoutInMilliseconds);
    }

  }

  constructor() { }

}

We inject the HttpInterceptor to the app module like this:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { CacheInterceptor } from './core/cache.interceptor';
import { HTTP_INTERCEPTORS, HttpClientModule } from '@angular/common/http';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { MatTableModule } from  '@angular/material';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule,
    MatTableModule,
    BrowserAnimationsModule
  ],
  providers: [
    { provide: HTTP_INTERCEPTORS, useClass: CacheInterceptor, multi: true }
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }

The index page then contains some markup including a Material Table using Google's Material UI. The app.component.ts handles the client side call back that triggers the retrieval and clearing of the data. If you click the load link, the data is retrieved and cached. If you open up Developer tools with F12, you will spot that the data is cached if you hit load many times. If you hit clear link, the data is cleared from cache and next click on load will fetch data again and inject into the cache. This sample shows how you can create a cache side feature of Angular 8 and support an HTTP interceptor. To check it out yourself run: git clone https://github.com/toreaurstadboss/AngularHttpInterceptorSample.git Afterwards, run: npm run start This will start both the Angular website on port 4200 and the Node.js Express.js backend on port 3000. The package.json script for npm run start looks like this: "start": "concurrently --kill-others \"ng build --watch --no-delete-output-path\" \"nodemon server.js\" \"ng serve --proxy-config proxy.conf.json \"", You can also just enter: npm start