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
User | Most Recent Commit | # Commits |
---|
Other Committers
User | Most Recent Commit | # Commits |
---|
When routing is not good enough
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.
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:
/
=> use the home page template,/:year/:month/:postid
=> use the post template./:year/:month/:postid/edit
=> use the post edition template.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:
/about
is a page,/news
is a folder,/news/welcome-our-new-president
is a page,/news/archives
is a folder.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.
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
.
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);
Just like the router, we specify the view rendering location using an outlet directive:
verser-outlet></traverser-outlet>
We create navigation links using a traverse directive:
raverseTo="/news/2017/happy-new-year/@@edit">Edit</a>
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.
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.
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
.
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']
A valid resolver must extend the Resolver
class and implement a resolve
method returning an observable.
See BasicHttpResolver as example.
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);
To insert the view rendering:
verser-outlet></traverser-outlet>
To create navigation links:
raverseTo="/news/2017/happy-new-year/@@edit">Edit</a>
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:
BasicHttpResolver
, we also need the BACKEND_BASE_URL
value),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 { }
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)