Skip to main content
Home  ›  Blog

How to Correctly Connect DNN to Angular 4 & 5 (Reactive JS)

Connecting a DNN module to Angular is very difficult, because the Angular App must know the ModuleId and more. These things which are delayed on page-load, leading to async problems. We found a very elegant solution for this, using reactive observables.

Why Angular Needs DNN Data

An Angular-App on a DNN page will usually need data from the DNN server. This is what the flow looks like:

In our case the data is usually 2sxc content-items, but the problem is the same even if you have your own endpoints: Angular will have to send AJAX requests to the server (requiring an endpoint-url), and the server will want to know a few things to validate the request, including:

  1. Security Token (to prevent API-attacks)
  2. TabId
  3. ModuleID (to check things like read/write permissions on this specific instance)
  4. Root path for API calls - which change depending on DNN version and other factors

This is what it looks like. To simplify a bit, I left out TabId, which behaves exactly like the security token:

Every XHR-request must include this data in the headers. When using DNN, there is a JavaScript object called the ServicesFramework which gives you the security-token, but it also needs to know your ModuleId. Like this:

So your Code must run this sequence:

  1. Find the ModuleId of your code (this is very hard when your code is inside an external JavaScript file) - but we can auto-detect it if we have a DOM Html-element
  2. Wait a while (because the ServicesFramework is f***ed up if you use it to quickly)
  3. Get the ServicesFramework
  4. …ask the ServicesFramework for more information
  5. Give it to the Angular App

To do this, these are the interdependencies - note the circular dependency when we want the ModuleId:

Angular Is Faster Than DNN JavaScript

Now Angular 4+ is very fast. It load so quickly, that DNN is simply not ready when it starts, so you are left with these options:

  • delay starting the app just because DNN has a slow JavaScript system
    analysis 1: Sucks, not a good idea
    analysis 2: Would also cause circular dependency (see image below)
  • try to give the initialization data to the app after it started
    analysis: Very hard to do, as many parts inside the App would have to know about this delay and be informed when it can continue. We would have to add a lot of hacky stuff to delay requests. And this gets very messy very quickly. 

Insight: It's an Async Event Problem

Taking a step back, you'll notice that the problem is a set of events which depend on each other, but happen out of sync. This is the ideal scenario for reactive programming.

Reactive Programming Saves The Day

Assuming we don't delay app-start, which we would only do because we would wait for data not needed for the app to build itself, then this is the dependencies we really have:

 

Now we must simply tell each part to observe other parts, and provide information whenever our observations are complete:

Requirement: Application Parts Shouldn't Know About This

Whenever we develop a solution for the DNN community, we're really concerned about providing a best-practice solution. Because we know that people will copy our implementation - often without understanding how/why it works. If our solution would affect the application architecture, this would cause a lot of trouble. So we wanted to develop a solution which

  1. simply works
  2. which doesn't require any of the inner parts to know that this was added

Solution: Intercept Http Requests

Since all http requests should run through HttpClient (Http is deprecated since Angular 4.3), all we must do is ensure that the HttpClient is aware of its dependencies. In our first solution, we injected a different HttpClient, but since Angular 4.3 allows interceptors, we're actually leaving HttpClient untouched, but adding an observable interceptor to the pipeline. Here's a bit of code which does the "wait for all values to be ready":

Full Context Relies On Many Partial Streams

all$ = Observable.combineLatest(
        this.moduleId$,             // wait for module id
        this.tabId$,                // wait for tabId
        this.contentBlockId$,       // wait for content-block id
        this.sxc$,                  // wait for sxc instance
        this.antiForgeryToken$)     // wait for security token
        .map(res => <ContextInfo>{  // then merge streams
            moduleId: res[0],
            tabId: res[1],
            contentBlockId: res[2],
            sxc: res[3],
            antiForgeryToken: res[4]
        });

 

This results in an observable ContextInfo object, which provides a combined data set with all necessary parameters. This Observable will be cold (meaning empty) until all values are ready.

Our interceptor must now combine this observable full-context, and connect it to all http-requests. This again results in a new observable http-request, which is again cold, because it can't start till everything is ready:

Angular Http Client Interceptor waiting for Context

intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    return this.context.all$.take(1)
      .flatMap(ctx => {

        // Clone the request and update the url with dnn & 2sxc params.
        const newReq = req.clone({
          url: ctx.sxc.resolveServiceUrl(req.url),
          setHeaders: {
            ModuleId: ctx.moduleId.toString(),
            TabId: ctx.tabId.toString(),
            ContentBlockId: ctx.contentBlockId.toString(),
            RequestVerificationToken: ctx.antiForgeryToken,
            'X-Debugging-Hint': 'bootstrapped by Sxc4Angular',
          },
        });

        return next.handle(newReq);
      });
  }

 

This may feel strange, so let me explain what's happening: 

  1. The Angular code is calling HttpClient...(...) and is expecting an Observable, which will then deliver the data.
  2. This continues to work, but in reality the http-observable is now wrapped inside another observable, which is chained to the Context, which may itself be delayed. 
  3. Note that it will only take(1) context - otherwise any future (unexpected) context change would re-trigger the request.
  4. Once the chain is complete and the Context delivers data, we'll modify the http-headers and allow the work to continue
  5. The code which initialized this XHR/AJAX/JSON request will never know this happened - it can work using the common Angular coding style

Try our Full Implementation

We created a demo-app which does exactly this - you can download it from the app-catalog or browse the code on Github. Just install the app in 2sxc, and you're good to go. As you can see, the application code itself is very clean - the only places where the application must "know" about DNN/2sxc is in the main.ts and in the app.component.ts. Everything else works is exactly like any normal Angular application.

Using the Solution

Basically it's classing npm install `@2sic.com/dnn-sxc-angular` and you're good to go. Best read the detailed instructions on NPM


Love from Switzerland, 
iJungleboy


Daniel Mettler grew up in the jungles of Indonesia and is founder and CEO of 2sic internet solutions in Switzerland and Liechtenstein, an 20-head web specialist with over 800 DNN projects since 1999. He is also chief architect of 2sxc (see github), an open source module for creating attractive content and DNN Apps.

Read more posts by Daniel Mettler