In this part of the blog I'll show you how to create a DNN module with Angular 2 und higher versions. I got a lot of requests to do this blog but I needed some time to get all the things together. But now I think everything fits and I'm happy to share this with you! Special cudos go to Thomas Chailland who made the first steps and kicks me on the road and to David Poindexter whose theming session on DNNConnect 2018 inspires me to use mklink to shadow the generated scripts directory into the DNN folder which allows to separate the Angular development from the DNN module development.
The DNN module
Fortunately nearly nothing changed in the DNN module. The only difference to the module from Part 1 - 6 is the content of the script folder and the index.html which bootstraps the Angular Application. To be exact: We need two versions of index.html, one for development and one for release. That's needed because while development we have another set of javascript libs to be loaded than in the release version where everything is minified and compressed. You find both versions of this file as index.debug.html and index.release.html in the download at the bottom of this blog and you only have to rename the appropriate file to index.html dependent on the phase of development you are in (maybe automate this in the MSBuild script?)
The Angular 4+ part
Learning to code Angular 4+ could not be part of this blog so I assume that you are familiar with the Angular code. I will only have a look at the interesting steps to get your Angular code to communicate with DNN. So if you want to follow please install the module attached to this blog into a DNN Installation so that your backend is prepared and we can start coding Angular against it.
Setting up the Angular project
First you have to install some software on your machine. These programs are needed:
- Nodejs (I'm using the LTS version)
- Visual studio code
After installation of node you can install the angular command line interface by entering the following on the command prompt:
npm install -g @angular/cli
Now we are ready to create the Angular project. Select a development directory and open a dos prompt there. To create a new project:
ng new angular5-items
cd angular5-items
code .
Now open a terminal window in code (in Germany this is Ctrl-ö) and type
ng serve
Open up the browser and go to http://localhost:4200, you should see now the following:
Now lets see if we can run this in DNN. First we have to take a few steps:
- Open up the angular-cli.json file and change the outDir parameter from dist to script (In Angular 6 this file is named angular.json and the structure differs but there should be something similar)
- Enter ng build in the terminal window of Code. This should create a script folder in your project containing the Angular files.
- Now open a dos prompt in your ~/DesktopModules/Angular5Module and enter the following command
mklink /J script [FolderOfAngularProject]/script
This creates a hard link from the script folder of your Angular Project to the script folder of the DNN installation (like a shadow copy). You should see now the Angular files inside your DNN scripts folder! Thanks again David Poindexter for the tip 😉 !
- As a last step rename index.debug.html to index.html and add the module to a page. If you see this, everything went fine so far:
Adding the needed methods to talk to the DNN WebApi
When talking to the DNN WebApi we need to add some headers to every http request. This is done by the following:
- Create a new folder http under the src folder and add a new file named DNNInterceptor.ts and paste the following code inside:
import { Injectable } from '@angular/core';
import { HttpRequest, HttpHandler, HttpEvent, HttpInterceptor} from '@angular/common/http';
import { Observable } from 'rxjs/Observable';
import { DnnContextService } from '../service/dnncontext.service';
@Injectable()
export class DnnInterceptor implements HttpInterceptor {
private context;
constructor(private ctx: DnnContextService) {
this.context = this.ctx.getServiceFramework();
console.log(this.context);
}
intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
request = request.clone({
setHeaders: {
ModuleId: this.context.moduleId.toString(),
TabId: this.context.tabId.toString(),
RequestVerificationToken: this.context.antiForgeryToken,
'X-Debugging-Hint': 'bootstrapped by bbAngular',
}
});
return next.handle(request);
}
}
- Next add a new folder named service under the src folder and add a new file dnncontext.service.ts:
import { Injectable } from '@angular/core';
declare const window: any;
@Injectable()
export class DnnContextService {
// https://medium.com/@ryanchenkie_40935/angular-authentication-using-the-http-client-and-http-interceptors-2f9d1540eb8
private _moduleId = -1;
private _tabId = -1;
private _antiForgeryToken = '';
private _properties: any = {};
constructor() {
const MODULE = 'Angular5Module';
if (window && window[MODULE]) {
this._properties = window[MODULE];
console.log('this.properties:', this._properties);
}
}
getServiceFramework() {
this._moduleId = this._properties.ModuleId;
if (this._antiForgeryToken !== '') {
return this.context;
} else {
// Check if DNN Services framework exists.
if (window.$ && window.$.ServicesFramework) {
const sf = window.$.ServicesFramework();
// Check if sf is initialized.
if (sf.getAntiForgeryValue() && sf.getTabId() !== -1) {
this._tabId = sf.getTabId();
this._antiForgeryToken = sf.getAntiForgeryValue();
return this.context;
} else {
return null;
}
} else {
return null;
}
}
}
get context() {
return { 'tabId': this._tabId, 'antiForgeryToken': this._antiForgeryToken, 'moduleId': this._moduleId };
}
get properties() {
return this._properties;
}
get resources() {
return this._properties.Resources;
}
}
- Now open src/app/app.module.ts and add your two new files in the providers part to make them known to angular (ItemService contains the http requests for the demo module, you can look this up in the download):
providers: [
ItemService,
{
provide: HTTP_INTERCEPTORS,
useClass: DnnInterceptor,
multi: true
},
DnnContextService
],
How the communication between DNN and the Angular App works
When you have a look at the index.html you see the following code there:
<script>window["Angular5Module"] = [ModuleProperties:All]</script>
The DNN token engine replaces [ModuleProperties:All] with a json string containing all the information like moduleid, tabid, resource keys for multilanguage purposes etc. (see ~\DesktopModules\Angular5Module\Controller\ModulePropertiesPropertyAccess.cs for more information) and adds it to the windows object of the browser. the dnncontext.service.ts grabs this information and makes it available to Angular. Easy peasy !
Talking to DNN
Its a good practise to encapsulate http request into a service so in this demo case add a new file in your service folder named item.service.ts:
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { HttpClient } from '@angular/common/http';
import { Item } from '../models/item.interface';
import { DnnContextService } from './dnncontext.service';
@Injectable()
export class ItemService {
private _urlBase: string;
constructor(private http: HttpClient, private ctx: DnnContextService) {
this._urlBase = this.ctx.properties.ModuleDirectory + 'API/item/';
}
getItems(): Observable<Item[]> {
return this.http.get<Item[]>(this._urlBase + 'list');
}
newItem(item): Observable<Item> {
return this.http.post<Item>(this._urlBase + 'new', item);
}
updateItem(item): Observable<Item> {
return this.http.post<Item>(this._urlBase + 'edit', item);
}
deleteItem(item): Observable<string> {
return this.http.post<string>(this._urlBase + 'delete', item);
}
reorderItems(sortItems): Observable<Item[]> {
return this.http.post<Item[]>(this._urlBase + 'reorder', sortItems);
}
getUsers(): Observable<any> {
return this.http.get<any>(this._urlBase + 'users');
}
}
As you can see: the DnnContextService is injected as this.ctx and you can access every information of the DNN backend - in this case the module install directory.
Debugging
While developing your module it is exhausting and time consuming to do ng build every time you have changed something. But there is a trick: If you fire the following command:
ng build --watch
Angular does an automatic rebuild of your App every time you save a file. And it only rebuilds the part you have changed, so it's much faster than building the whole project! It's not as comfortable as ng serve because in this case there is no automatic refresh of the browser - you have to refresh DNN by yourself! And be aware of using Ctrl-F5 to do a complete refresh and avoid caching!
When you're done with development use the following command to create a minified version of your code:
ng build --prod --output-hashing none
Do not forget to use the index.release.html as index.html now ! And after testing with your release version you are ready for packaging! If you followed the steps of the other blog parts the build system does it for you only by changing the compile mode in Visual Studio to release. Yeah!
Downloads
- Angularmodule_01.00.00_Source.zip Source version of the DNN module
- Angularmodule_01.00.00_Install.zip Installable version of the DNN module with Angular script code inside (ready to run!)
- Angular5_Items.Source.zip Angular project angular5-items (do not forget to call npm install after unzipping to download node modules!)
Conclusion
As you see: It's not so hard to integrate Angular 2+ Apps into DNN. The only thing I did not solve by now is that it is not possible to have more than one Angular module on a page. Maybe someone has an idea how to solve this ?