makinacorpus/angular-traversal

Name: angular-traversal

Owner: Makina Corpus

Description: null

Created: 2017-01-09 07:03:24.0

Updated: 2017-12-03 14:06:40.0

Pushed: 2017-11-30 16:22:55.0

Homepage: null

Size: 1359

Language: TypeScript

GitHub Committers

UserMost Recent Commit# Commits

Other Committers

UserEmailMost Recent Commit# Commits

README

Angular Traversal

When routing is not good enough

Build Status

Demo

In this demo we allow to navigate in any public Github repository. There is not specific routing implemented in the app, we just rely on the Github routing itself.

Why routing is not always good

Routing is the most common way to associate a given rendering to a given URL.

Let's think about a blog, / (the root) displays a list of the last blog posts, and each post has an URL like /2017/01/11/angular-traversal, and when the manager edits such a post, the URL is /2017/01/11/angular-traversal/edit.

Routing allows to manage that easily with 3 routes:

And Angular provides a very good routing implementation (see Angular Router).

This mechanism works perfectly for application-oriented websites (let's say an issue tracker for instance) or simple websites (like a blog). But, it does not fit the needs of complex websites.

Let's imagine a company website which can contain two types of pages: folders or simple pages:

There is no way to enumerate all the possible routes for such a website: it would be too difficult to maintain (because the site structure will probably evolve regularly).

That's why CMSes usually do not use routing.

We can imagine another example: we want to build an application to display a Git repository and we want to render differently the folders and the files.

We certainly want to use the current file or folder path as our current URL and as we cannot predict the repository tree structure, here again routing is not an option.

Note: and sometimes routing is the appropriate solution, but our backend service already implements it very well and we do not want to re-implement it another time in our frontend application.

Meet traversal

Traversal is another approach, it analyses the current URL from its end to its begining in order to identify the appropriate rendering (we name it the view) and the context resource we want to render.

We use @@ as a prefix to our view identifiers.

For instance, in /news/@@edit, edit is the view identifier, and /news is the context, so we know we want to display the /news content using the Edit form template.

Depending on the context, the same view identifier might be associated to a specific view implementation. For instance we probably have two different forms to edit a folder and to edit a page, but their respective URLs will both end with /@@edit.

The default view is view, so /about is equivalent to /about/@@view.

When do we use traversal
Angular Traversal features

Angular Traversal allows to associate a component to a view name for a given context marker, like:

erser.addView('view', 'Folder', FolderViewComponent);
erser.addView('view', 'Document', DocumentViewComponent);
erser.addView('edit', 'Folder', FolderEditComponent);
erser.addView('edit', 'Document', DocumentEditComponent);
erser.addView('history', '*', HistoryComponent);
How do we insert a view in our app?

Just like the router, we specify the view rendering location using an outlet directive:

verser-outlet></traverser-outlet>
How do we create navigation links?

We create navigation links using a traverse directive:

raverseTo="/news/2017/happy-new-year/@@edit">Edit</a>
How the traverser will get the context from the current path?

The context is obtained from a resolver which takes the context path (e.g. /news/2017/happy-new-year) as parameter and returns the corresponding context as an object.

In many cases, we will retrieve this context from a backend, so Angular Traversal provides a basic HTTP resolver which just makes a GET using the context path and convert the resulting JSON into an object. But we can provide a custom resolver implementation easily.

The traverser will give this context object to the view component so it can be rendered.

If we traverse to a view without context (like @@login), we will not re-call the resolver, we will just keep the current context and change the view.

How the traverser knows which component to use?

As mentionned earlier, the view mapping is based on a context marker (in our example, we have 'Document' and 'Folder'). It is a string computed from the context object using a custom marker class.

For instance, if we want to associate a view according the content-type, assuming the backend JSON response contains a type property, our marker class would do:

rn context.type

And '*' allows to match any context.

Note: if our marker returns an array of strings, the traverser will pick the first item of the array which matches a view definition.

Usage
Create a view component

A valid view component must have a context property, it must inject the Traverser service, and subscribe to it to get the context:

rt { Component, OnInit } from '@angular/core';
rt { Traverser } from 'angular-traversal';

ponent({
lector: 'app-folder-edit',
mplateUrl: './folder-edit.component.html',
yleUrls: ['./folder-edit.component.css']

rt class FolderEditComponent implements OnInit {

ivate context: any;

nstructor(private traverser: Traverser) { }

OnInit() {
this.traverser.target.subscribe(target => {
  this.context = target.context;
});



Warning: it must be declared in the app module in both declarations and entryComponents.

Implement a marker

A valid marker must extend the Marker class and implent a mark method returning a string.

rt { Injectable } from '@angular/core';
rt { Marker } from 'angular-traversal';

ectable()
rt class TypeMarker extends Marker {
rk(context: any): string {
  return context['@type']


Define a resolver

A valid resolver must extend the Resolver class and implement a resolve method returning an observable.

See BasicHttpResolver as example.

Register views

We register view using the Traverser's addView method. It can be done anywhere, but our main AppComponent class seems like a good place.

rt { Component } from '@angular/core';
rt { Traverser } from 'angular-traversal';
rt { EditComponent } from './edit/edit.component';
rt { FolderEditComponent } from './folder-edit/folder-edit.component';
rt { HistoryComponent } from './history/history.component';

ponent({
lector: 'app-root',
mplateUrl: './app.component.html',
yleUrls: ['./app.component.css']

rt class AppComponent {
tle = 'app works!';

nstructor(traverser:Traverser) {
  traverser.addView('edit', 'Folder', FolderEditComponent);
  traverser.addView('edit', 'Document', EditComponent);
  traverser.addView('history', '*', HistoryComponent);


Insert the outlet and the links

To insert the view rendering:

verser-outlet></traverser-outlet>

To create navigation links:

raverseTo="/news/2017/happy-new-year/@@edit">Edit</a>
Declare everything in the module

In declarations we need all our view components, and they also need to be mentionned in entryComponents.

In 'imports', we need to import the TraversalModule.

In providers, we need:

rt { BrowserModule } from '@angular/platform-browser';
rt { NgModule } from '@angular/core';
rt { FormsModule } from '@angular/forms';
rt { HttpModule } from '@angular/http';

rt { TraversalModule } from 'angular-traversal';
rt { Resolver } from 'angular-traversal';
rt { Marker } from 'angular-traversal';
rt { BasicHttpResolver, BACKEND_BASE_URL } from 'angular-traversal';

rt { AppComponent } from './app.component';
rt { EditComponent } from './edit/edit.component';
rt { HistoryComponent } from './list/list.component';
rt { FolderEditComponent } from './folder-edit/folder-edit.component';
rt { TypeMarker } from './type-marker';

odule({
clarations: [
AppComponent,
EditComponent,
HistoryComponent,
FolderEditComponent,

ports: [
BrowserModule,
FormsModule,
HttpModule,
TraversalModule,

tryComponents: [
EditComponent,
HistoryComponent,
FolderEditComponent,

oviders: [
{ provide: Resolver, useClass: BasicHttpResolver },
{ provide: BACKEND_BASE_URL, useValue: 'http://my.backend.io' },
{ provide: Marker, useClass: TypeMarker },

otstrap: [AppComponent]

rt class AppModule { }
Path normalizer

The main use case is to call a REST backend based on the current path. For instance, by calling http://localhost:4200/makinacorpus/angular-traversal/contents the traverser will call https://api.github.com/repos/makinacorpus/angular-traversal/contents and use the result as the view context.

But, if the result contains some path to other resources, in most cases they will be provided as full URL, like in our case with the GitHub API, the folder items are like that:


"name": "CHANGELOG.md",
...
"url": "https://api.github.com/repos/makinacorpus/angular-traversal/contents/CHANGELOG.md?ref=master",
...

So if we want to use this url to create a traversable link, we need to shorten it:

raverseTo="/makinacorpus/angular-traversal/contents/CHANGELOG.md?ref=master">CHANGELOG.md</a>

Of course, our resolver could support full pathes, but then the displayed location in the browser would be:

h does work, but is pretty ugly.

void that, we can implement a Normalizer:

import { Injectable } from '@angular/core'; import { Normalizer } from 'angular-traversal';

@Injectable() export class FullPathNormalizer extends Normalizer { normalize(path): string {

if (path.startsWith('https://api.github.com/repos')) {
  return path.slice(28);
} else {
  return path;
}

} }

provide it in the module:

import { Normalizer } from 'angular-traversal'; import { FullPathNormalizer } from './my-normalizer'; … { provide: Normalizer, useClass: FullPathNormalizer }, …

ther demo package

ne demo package](https://github.com/ebrehault/angular-traversal-demo)

This work is supported by the National Institutes of Health's National Center for Advancing Translational Sciences, Grant Number U24TR002306. This work is solely the responsibility of the creators and does not necessarily represent the official views of the National Institutes of Health.