Base solution for your next web application
Open Closed

A minimalist ASPNETZERO Angular client #11164


User avatar
0
alexanderpilhar created

Hey everybody!

A few years back I tried to create a separate Angular client that was able to consume ASPNETZERO's public API's (non-authorized): [Reduced Angular Client #4596](https://support.aspnetzero.com/QA/Questions/4596/Reduced-Angular-Client)

While this approach did the job back then I never was too happy with it as a lot of not needed resources remained.

A few days ago I decided to try another approach: starting with a new Angular application and only add what is necessary to make it work with ASPNETZERO's Core Services.

Today, I want to share with you my results (see next comment)!

I hope it is useful to some of you!

Cheers!


6 Answer(s)
  • User Avatar
    0
    alexanderpilhar created

    Minimalist ASPNETZERO Angular client

    This project was generated with Angular CLI version 14.0.4.

    This project is a minimalist ASPNETZERO Angular client. It implements the very minimal requirements to make use of the ASPNETZERO Core Services (ASPNETZERO 11.2.1).

    Features

    • Access to ASPNETZERO Core Service (public/non-authorized API only)
    • NgxSpinner (package ngx-spinner)
    • Localization
    • SignalR (public/non-authorized endpoint only)

    Access to ASPNETZERO Core Services

    This documentation shows how to create a minimalist ASPNETZERO Angular client that can consume ASPNETZERO Core Services (public/non-authorized API only).

    Add package 'nswag'

    Add package:

    • npm install --save-dev nswag

    Copy ./nswag/* from original ASPNETZERO Angular client.

    In ./nswag/service.config.nswag update typeScriptVersion and rxJsVersion according to ./project.json.

    Go to ./nswag and run ./resfresh.bat (ASPNETZERO Core Services must be up and running).

    This creates ./src/shared/service-proxies/service-proxies.ts.

    Copy ./src/shared/service-proxies/service-proxy.module.ts from original ASPNETZERO Angular client.
    Remove everything irrelevant:

    import { HTTP_INTERCEPTORS } from '@angular/common/http';
    import { NgModule } from '@angular/core';
    import { AbpHttpInterceptor } from 'abp-ng2-module';
    import * as ApiServiceProxies from './service-proxies';
    
    @NgModule({
        providers: [
            
            { provide: HTTP_INTERCEPTORS, useClass: AbpHttpInterceptor, multi: true },
    
            ApiServiceProxies.MyServiceProxy,
        ],
    })
    export class ServiceProxyModule {}
    

    As we can see ServiceProxyModule requires package abp-ng2-module.

    Add package 'abp-ng2-module'

    Add packages:

    • npm install --save luxon
    • npm install --save-dev @types/luxon
    • npm install --save --legacy-peer-deps abp-web-resources
    • npm install --save --legacy-peer-deps abp-ng2-module

    Adding @types to ./tsconfig.*.json:
    see: angular.io / guide / typescript-configuration / installable-typings-files

    In ./tsconfig.app.json add "luxon" to compilerOptions.types:

    "compilerOptions": {
        // ...,
        "types": [
            "luxon"
        ]
    }
    

    In ./tsconfig.spec.json add "luxon" to compilerOptions.types:

    "compilerOptions": {
        // ...,
        "types": [
            // ...,
            "luxon"
        ]
    }
    

    Adding "skipLibCheck": true to ./tsconfig.compilerOptions:
    This is necessary to prevent build errors caused by abp-ng2-module not being able to find references to abp-web-resources.

    {
        "compilerOptions": {
            // ...,
            "skipLibCheck": true
        }
    }
    

    Add ./src/typings.d.ts with the following content:
    see: typescriptlang.org / docs / handbook / declaration-files / publishing / red-flags

    ///<reference path="../node_modules/abp-web-resources/Abp/Framework/scripts/abp.d.ts"/>
    

    Adding abp-web-resources to ./src/assets/:
    Copy ./src/assets/abp-web-resources/abp.js from original ASPNETZERO Angular client.

    Adding "src/assets/abp-web-resources/abp.js" to ./angular.json to build.options and test.options:

    "scripts": [
        "src/assets/abp-web-resources/abp.js"
    ]
    

    Additional Resources and AppModule

    Copy the following files from original ASPNETZERO Angular client:

    • ./src/app-pre-bootstrap.ts
    • ./src/assets/appconfig.json
    • ./src/assets/appconfig.production.json
    • ./src/shared/app-consts.ts (original name: AppConsts.ts)
    • ./src/shared/helpers/xml-http-request-helper.ts (original name: XmlHttpRequestHelper.ts)

    The content of ./src/app-pre-bootstrap.ts is the following:

    import { environment } from './environments/environment';
    import { AppConsts } from './shared/app-consts';
    import { XmlHttpRequestHelper } from './shared/helpers/xml-http-request-helper';
    
    export class AppPreBootstrap {
    
        static run(appRootUrl: string, callback: () => void, resolve: any, reject: any): void {
    
            AppPreBootstrap.getApplicationConfig(appRootUrl, () => {
    
                callback();
            });
        }
        
        private static getApplicationConfig(appRootUrl: string, callback: () => void) {
    
            let type = 'GET';
            let url = appRootUrl + 'assets/' + environment.appConfig;
            let customHeaders = [{
                name: 'Abp.TenantId',
                value: abp.multiTenancy.getTenantIdCookie() + ''
            }];
    
            XmlHttpRequestHelper.ajax(type, url, customHeaders, null, (result: any) => {
    
                AppConsts.localeMappings = result.localeMappings;
    
                AppConsts.appBaseUrlFormat = result.appBaseUrl;
                AppConsts.remoteServiceBaseUrlFormat = result.remoteServiceBaseUrl;
    
                AppConsts.appBaseUrl = result.appBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
                AppConsts.remoteServiceBaseUrl = result.remoteServiceBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
    
                callback();
            });
        }
    }
    

    Modify the following files:

    • ./src/environments/environment.prod.ts
    export const environment = {
      production: true,
      appConfig: 'appconfig.production.json'
    };
    
    • ./src/environments/environment.ts
    export const environment = {
      production: false,
      appConfig: 'appconfig.json'
    };
    

    Modify ./src/app/app.module.ts:

    import { PlatformLocation } from '@angular/common';
    import { HttpClientModule } from '@angular/common/http';
    import { APP_INITIALIZER, NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { AppPreBootstrap } from 'src/app-pre-bootstrap';
    import { AppConsts } from 'src/shared/app-consts';
    import { API_BASE_URL } from 'src/shared/service-proxies/service-proxies';
    import { ServiceProxyModule } from 'src/shared/service-proxies/service-proxy.module';
    
    import { AppRoutingModule } from './app-routing.module';
    import { AppComponent } from './app.component';
    
    export function appInitializerFactory(platformLocation: PlatformLocation) {
    
      return () => {
    
        return new Promise<boolean>((resolve, reject) => {
    
          AppConsts.appBaseHref = getBaseHref(platformLocation);
          let appBaseUrl = `${getDocumentOrigin()}${AppConsts.appBaseHref}`;
    
          AppPreBootstrap.run(
            appBaseUrl,
            () => { resolve(true); },
            resolve,
            reject
          );
    
        });
    
      };
    
    }
    
    function getDocumentOrigin() {
    
      const docloc = document.location;
    
      return docloc.origin ?? `${docloc.protocol}//${docloc.hostname}${docloc.port ? ':' + docloc.port : ''}`
    }
    
    export function getRemoteServiceBaseUrl(): string {
    
      return AppConsts.remoteServiceBaseUrl;
    }
    
    export function getBaseHref(platformLocation: PlatformLocation) {
    
      return platformLocation.getBaseHrefFromDOM() ?? '/';
    }
    
    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        AppRoutingModule,
        ServiceProxyModule,
        HttpClientModule,
      ],
      providers: [
        { provide: API_BASE_URL, useFactory: getRemoteServiceBaseUrl },
        {
          provide: APP_INITIALIZER,
          useFactory: appInitializerFactory,
          deps: [PlatformLocation],
          multi: true
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    
    
  • User Avatar
    0
    alexanderpilhar created

    NgxSpinner

    This documentation shows how to make use of package ngx-spinner.

    Add package:

    • npm install --save --legacy-peer-deps ngx-spinner

    Adapt @NgModule decorator in ./src/app/app.module.ts as follows:

    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        AppRoutingModule,
        ServiceProxyModule,
        HttpClientModule,
        NgxSpinnerModule
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        { provide: API_BASE_URL, useFactory: getRemoteServiceBaseUrl },
        {
          provide: APP_INITIALIZER,
          useFactory: appInitializerFactory,
          deps: [Injector, PlatformLocation],
          multi: true
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    Adapt appInitializerFactory() in ./src/app/app.module.ts as follows:

    export function appInitializerFactory(injector: Injector, platformLocation: PlatformLocation) {
    
      return () => {
    
        const spinnerService = injector.get(NgxSpinnerService);
    
        spinnerService.show();
    
        return new Promise<boolean>((resolve, reject) => {
    
          AppConsts.appBaseHref = getBaseHref(platformLocation);
          let appBaseUrl = `${getDocumentOrigin()}${AppConsts.appBaseHref}`;
    
          AppPreBootstrap.run(
            appBaseUrl,
            () => {
              spinnerService.hide();
              resolve(true);
            },
            resolve,
            reject
          );
    
        });
    
      };
    
    }
    

    Add stylings to ./angular.json:
    Note: Which css file to import depends on the type of animation you want to use.

    "styles": [
      "node_modules/ngx-spinner/animations/ball-clip-rotate.css"
    ],
    

    DONE

    That's it! Now we can make use of NgxSpinner in our minimalist ASPNETZERO Angular client!

  • User Avatar
    0
    alexanderpilhar created

    Localization

    This documentation shows how to make use of ASPNETZERO's localization capabilities.

    Add packages:

    • npm install --save --legacy-peer-deps lodash-es
    • npm install --save-dev --legacy-peer-deps @types/lodash-es

    Edit ./tsconfig.json to contain the following compilerOptions:

        "strict": false,
        "paths": {
          "@shared/*": ["./src/shared/*"]
        }
    

    Copy ./src/shared/services/locale-mappings.services.ts from original ASPNETZERO Angular client (original location is ./src/shared/).

    Copy ./src/shared/pipes/localize.pipe.ts from original ASPNETZERO Angular client (original location is ./src/shared/common/pipes/).

    Add ./src/shared/pipes/pipes.module.ts with the following content:

    import { NgModule } from '@angular/core';
    import { LocalizePipe } from "./localize.pipe";
    
    @NgModule({
        declarations: [
            LocalizePipe,
        ],
        exports: [
            LocalizePipe,
        ]
    })
    export class PipesModule {}
    

    AbpModule

    Add the following functions to ./src/app/app.module.ts:

    export function convertAbpLocaleToAngularLocale(locale: string): string {
    
      return new LocaleMappingService().map('angular', locale);
    }
    
    export function getCurrentLanguage(): string {
    
      return convertAbpLocaleToAngularLocale(abp.localization.currentLanguage.name);
    }
    
    function registerLocales(
      resolve: (value?: boolean | Promise<boolean>) => void,
      reject: any,
      spinnerService: NgxSpinnerService
    ) {
    
      if (abp.localization.currentLanguage.name && abp.localization.currentLanguage.name !== 'en-US') {
    
        let angularLocale = convertAbpLocaleToAngularLocale(abp.localization.currentLanguage.name);
    
        import(`/node_modules/@angular/common/locales/${angularLocale}.mjs`).then((module) => {
    
          registerLocaleData(module.default);
    
          spinnerService.hide();
          resolve(true);
    
        }, reject);
      }
      else {
    
        spinnerService.hide();
        resolve(true);
      }
    }
    

    Adapt appInitializerFactory() in ./src/app/app.module.ts as follows:

    export function appInitializerFactory(injector: Injector, platformLocation: PlatformLocation) {
    
      return () => {
    
        const spinnerService = injector.get(NgxSpinnerService);
    
        spinnerService.show();
    
        return new Promise<boolean>((resolve, reject) => {
    
          AppConsts.appBaseHref = getBaseHref(platformLocation);
          let appBaseUrl = `${getDocumentOrigin()}${AppConsts.appBaseHref}`;
    
          AppPreBootstrap.run(
            appBaseUrl,
            () => {
              registerLocales(resolve, reject, spinnerService);
              resolve(true);
            },
            resolve,
            reject
          );
    
        });
    
      };
    
    }
    

    Adapt @NgModule decorator in ./src/app/app.module.ts as follows:

    @NgModule({
      declarations: [
        AppComponent
      ],
      imports: [
        BrowserModule,
        BrowserAnimationsModule,
        AppRoutingModule,
        ServiceProxyModule,
        HttpClientModule,
        NgxSpinnerModule,
        PipesModule,
      ],
      schemas: [CUSTOM_ELEMENTS_SCHEMA],
      providers: [
        { provide: API_BASE_URL, useFactory: getRemoteServiceBaseUrl },
        {
          provide: APP_INITIALIZER,
          useFactory: appInitializerFactory,
          deps: [Injector, PlatformLocation],
          multi: true
        },
        {
          provide: LOCALE_ID,
          useFactory: getCurrentLanguage
        }
      ],
      bootstrap: [AppComponent]
    })
    export class AppModule { }
    

    Adapt ./src/app-pre-bootstrap.ts as follows:

    import { environment } from './environments/environment';
    import { AppConsts } from './shared/app-consts';
    import { XmlHttpRequestHelper } from './shared/helpers/xml-http-request-helper';
    import { merge as _merge } from 'lodash-es';
    import { LocaleMappingService } from '@shared/services/locale-mapping.service';
    import { DateTime, Settings } from 'luxon';
    
    export class AppPreBootstrap {
    
        static run(appRootUrl: string, callback: () => void, resolve: any, reject: any): void {
    
            AppPreBootstrap.getApplicationConfig(appRootUrl, () => {
    
                AppPreBootstrap.getUserConfiguration(callback);
            });
        }
        
        private static getApplicationConfig(appRootUrl: string, callback: () => void) {
    
            let type = 'GET';
            let url = appRootUrl + 'assets/' + environment.appConfig;
            let customHeaders = [{
                name: 'Abp.TenantId',
                value: abp.multiTenancy.getTenantIdCookie() + ''
            }];
    
            XmlHttpRequestHelper.ajax(type, url, customHeaders, null, (result: any) => {
    
                AppConsts.localeMappings = result.localeMappings;
    
                AppConsts.appBaseUrlFormat = result.appBaseUrl;
                AppConsts.remoteServiceBaseUrlFormat = result.remoteServiceBaseUrl;
    
                AppConsts.appBaseUrl = result.appBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
                AppConsts.remoteServiceBaseUrl = result.remoteServiceBaseUrl.replace(AppConsts.tenancyNamePlaceHolderInUrl + '.', '');
    
                callback();
            });
        }
        
        private static getCurrentClockProvider(currentProviderName: string): abp.timing.IClockProvider {
    
            if (currentProviderName === 'unspecifiedClockProvider') {
                return abp.timing.unspecifiedClockProvider;
            }
    
            if (currentProviderName === 'utcClockProvider') {
                return abp.timing.utcClockProvider;
            }
    
            return abp.timing.localClockProvider;
        }
    
        private static getRequestHeadersWithDefaultValues() {
    
            const cookieLangValue = abp.utils.getCookieValue('Abp.Localization.CultureName');
    
            let requestHeaders = {
                '.AspNetCore.Culture': 'c=' + cookieLangValue + '|uic=' + cookieLangValue,
                [abp.multiTenancy.tenantIdCookieName]: abp.multiTenancy.getTenantIdCookie(),
            };
    
            if (!cookieLangValue) {
                delete requestHeaders['.AspNetCore.Culture'];
            }
    
            return requestHeaders;
        }
    
        private static getUserConfiguration(callback: () => void): any {
    
            const token = abp.auth.getToken();
    
            let requestHeaders = AppPreBootstrap.getRequestHeadersWithDefaultValues();
    
            if (token) {
                requestHeaders['Authorization'] = 'Bearer ' + token;
            }
    
            return XmlHttpRequestHelper.ajax(
                'GET',
                AppConsts.remoteServiceBaseUrl + '/AbpUserConfiguration/GetAll',
                requestHeaders,
                null,
                (response) => {
                    
                    let result = response.result;
    
                    _merge(abp, result);
    
                    abp.clock.provider = this.getCurrentClockProvider(result.clock.provider);
    
                    AppPreBootstrap.configureLuxon();
    
                    abp.event.trigger('abp.dynamicScriptsInitialized');
    
                    AppConsts.recaptchaSiteKey = abp.setting.get('Recaptcha.SiteKey');
                    AppConsts.subscriptionExpireNootifyDayCount = parseInt(
                        abp.setting.get('App.TenantManagement.SubscriptionExpireNotifyDayCount')
                    );
    
                    callback();
                }
            );
        }
    
        private static configureLuxon() {
    
            let luxonLocale = new LocaleMappingService().map('luxon', abp.localization.currentLanguage.name);
    
            DateTime.local().setLocale(luxonLocale);
            DateTime.utc().setLocale(luxonLocale);
            Settings.defaultLocale = luxonLocale;
    
            if (abp.clock.provider.supportsMultipleTimezone) {
                Settings.defaultZone = abp.timing.timeZoneInfo.iana.timeZoneId;
            }
    
            Date.prototype.toISOString = function () {
                return DateTime.fromJSDate(this)
                    .setLocale('en')
                    .setZone(abp.timing.timeZoneInfo.iana.timeZoneId)
                    .toString();
            };
    
            Date.prototype.toString = function () {
                return DateTime.fromJSDate(this)
                    .setLocale('en')
                    .setZone(abp.timing.timeZoneInfo.iana.timeZoneId)
                    .toString();
            };
    
            DateTime.prototype.toString = function () {
                let date = this.setLocale('en').setZone(abp.timing.timeZoneInfo.iana.timeZoneId) as DateTime;
                return date.toISO();
            };
        }
    }
    

    DONE

    That's it! Now we can make use of ASPNETZERO's localization capabilities in our minimalist ASPNETZERO Angular client!

  • User Avatar
    0
    alexanderpilhar created

    SignalR

    This documentation shows how to make use of ASPNETZERO's SignalR capabilities (public/non-authorized endpoint only).

    Add package:

    • npm install --save --legacy-peer-deps @microsoft/signalr

    Add assets and scripts to ./angular.json:

    "assets": [
      "src/favicon.ico",
      "src/assets",
      {
        "glob": "abp.signalr-client.js",
        "input": "node_modules/abp-web-resources/Abp/Framework/scripts/libs",
        "output": "/assets/abp"
      }
    ],
    "scripts": [
      "src/assets/abp-web-resources/abp.js",
      "node_modules/@microsoft/signalr/dist/browser/signalr.min.js"
    ]
    

    Adapt ./src/typings.d.ts as follows:

    ///<reference path="../node_modules/abp-web-resources/Abp/Framework/scripts/abp.d.ts"/>
    ///<reference path="../node_modules/abp-web-resources/Abp/Framework/scripts/libs/abp.signalr.d.ts"/>
    

    Copy ./src/shared/helpers/signalr-helper.ts from original ASPNETZERO Angular client (original name is SignalRHelper.ts).
    Adapt the file as follows:

    import { AppConsts } from '@shared/app-consts';
    
    export class SignalRHelper {
    
        static initSignalR(callback: () => void): void {
    
            abp.signalr = {
                autoConnect: false,
                connect: undefined,
                hubs: undefined,
                qs: undefined,
                remoteServiceBaseUrl: AppConsts.remoteServiceBaseUrl,
                startConnection: undefined,
                url: '/signalr-public',
            };
    
            let script = document.createElement('script');
            script.src = `${AppConsts.appBaseUrl}/assets/abp/abp.signalr-client.js`;
            script.onload = () => {
                callback();
            };
    
            document.head.appendChild(script);
        }
    }
    
    

    DONE

    That's it! Now we can make use of ASPNETZERO's SignalR capabilities in our minimalist ASPNETZERO Angular client!

  • User Avatar
    0
    alexanderpilhar created

    That's it for now ; )

  • User Avatar
    0
    ismcagdas created
    Support Team

    Hi @alexanderpilhar

    Thank you very much for sharing this :), it is really appriciated. If you have time in the future and if you can create a single markdown file with the content above, we can also publish it on https://blog.aspnetzero.com/