Angular Server-Side Rendering with Route Resolve - javascript

I am attempting to use Server-Side Rendering in Angular (v4) to allow for better SEO.
Things work as expected until I add resolve on my route. Adding resolve causes HTML title to retain it's initial value when viewing source.
My Module:
import {
Injectable,
ModuleWithProviders,
NgModule
} from '#angular/core';
import {
ActivatedRouteSnapshot,
Resolve,
Router,
RouterModule,
RouterStateSnapshot
} from '#angular/router';
import {
Observable
} from 'rxjs/Rx';
import {
ArticleComponent
} from './article.component';
import {
Article,
ArticlesService,
UserService,
SharedModule
} from '../shared';
#Injectable()
export class ArticleResolver implements Resolve < Article > {
constructor(
private articlesService: ArticlesService,
private router: Router,
private userService: UserService
) {}
resolve(
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
): any {
return this.articlesService.get(route.params['slug'])
.catch((err) => this.router.navigateByUrl('/'));
}
}
const articleRouting: ModuleWithProviders = RouterModule.forChild([{
path: 'article/:slug',
component: ArticleComponent,
resolve: {
article: ArticleResolver
},
data: {
preload: true
}
}]);
#NgModule({
imports: [
articleRouting,
SharedModule
],
declarations: [
ArticleComponent
],
providers: [
ArticleResolver
]
}) export class ArticleModule {}
My Component:
import {
Component,
OnInit
} from '#angular/core';
import {
ActivatedRoute,
Router,
} from '#angular/router';
import {
Title,
Meta
} from '#angular/platform-browser';
import {
AppComponent
} from '../app.component';
import {
Article,
} from '../shared';
#Component({
selector: 'article-page',
templateUrl: './article.component.html'
})
export class ArticleComponent implements OnInit {
article: Article;
constructor(
private route: ActivatedRoute,
private meta: Meta,
private title: Title
) {}
ngOnInit() {
this.route.data.subscribe(
(data: {
article: Article
}) => {
this.article = data.article;
}
);
this.title.setTitle(this.article.title);
}
}
I am new to Angular SSR so any guidance is greatly appreciated.

Instead of subscribing to route data, retrieve your results from the snapshot like this:
this.route.snapshot.data['article']
You also might need to register ArticlesService in your providers for the module.
As a side note, this import:
import {
Observable
} from 'rxjs/Rx';
is an RxJS antipattern. Please use the following import instead:
import {Observable} from 'rxjs/Observable';

I found that my primary service was referencing a secondary service that was attempting to return an authentication token from window.localStorage.
Attempting to access the client storage caused Angular SSR to omit the generation of source code for my component.
Thanks #Adam_P for helping me walk through it!

Related

How to restrict loading of lazy loaded module till resolving some data from api in angular?

I have a lazy loaded module. I want to load some data before the module load. I tried app_initializer but it didn't work because i have already an app_initializer in AppModule. Is there any way to do this?
You can use an Resolver that will handle your data before the module will load and navigate to your specific route as that loaded completly
resolver
import { Injectable } from '#angular/core';
import { APIService } from './api.service';
import { Resolve } from '#angular/router';
import { ActivatedRouteSnapshot } from '#angular/router';
#Injectable()
export class APIResolver implements Resolve<any> {
constructor(private apiService: APIService) {}
resolve(route: ActivatedRouteSnapshot) {
return this.apiService.getItems(route.params.date);
}
}
in your route
{
path: 'items/:date',
component: ItemsComponent,
resolve: { items: APIResolver }
}
and to acces the resolved data in the desired component
import { ActivatedRoute } from '#angular/router';
constructor(private route: ActivatedRoute) { }
items: any[];
ngOnInit() {
this.items= this.route.snapshot.data.items;
}
using a component with a resolver in a given module will not init that module until all the data is resolved
you can check also https://angular.io/api/router/Resolve

Read route params from directly entered url in app

My question would be regarding angular 4, how to get route params, if for example a user gets on your page with, instead of the default url, like for example http://localhost:3000/, to something like http://localhost:3000/user/:id, and to be able to pick up the :id from that url (user has directly entered it in the browser, not navigating through the app).
In the example bellow same component is used, mainly because of needing to catch that id and dispatch other actions, if its present, and that would be it.
I have tried playing around with ActivatedRoute but from what I could tell so far, that only works when navigation throughout the app, from within the app, not in this case, which always returns a null value if that url is directly entered in the browser, it gets redirected to the default / route and that would be it.
Any tips or pointers are much appreciated
app.routing-module.ts
import {hookComponent} from './hook.component';
import {RouterModule, Routes} from '#angular/router';
import {NgModule} from '#angular/core';
export const routes: Routes = [
{
path: '',
component: HookComponent
},
{
path: 'user/:id',
component: HookComponent
}
];
#NgModule({
imports: [RouterModule.forRoot(routes, { enableTracing: true })],
exports: [RouterModule]
})
export class AppRoutingModule {}
hook.component
import {Component, EventEmitter, Input, OnInit, ViewChild} from '#angular/core';
import { ActivatedRoute, ParamMap} from '#angular/router';
#Component({
selector: 'hook',
templateUrl: 'hook.component.html',
styleUrls: ['hook.component.scss']
})
export class HookComponent implements OnDestroy, OnInit {
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
this.sub = this.route.params.subscribe(params => {
console.log('params are', params); //null?
});
}
}
Your way is already ok, but in your example params is an array and you can access to :id by calling params['id']:
this.sub = this.route.params.subscribe(params => {
console.log('params are', params['id']);
});
Here is an working example on stackblitz.
Access current url via Location
public constructor(location:Location) {
let url = location.prepareExternalUrl(location.path());
}
and parse out id from this.
If all you want to do is log the params.id; try using the ActivatedRouteSnapshot like this.
ngOnInit() {
console.log(this.route.snapshot.params.id);
}
If you want to check if the params.id is present, maybe do something like:
import {Component, EventEmitter, Input, OnInit, ViewChild} from '#angular/core';
import { ActivatedRoute, ParamMap} from '#angular/router';
#Component({
selector: 'hook',
templateUrl: 'hook.component.html',
styleUrls: ['hook.component.scss']
})
export class HookComponent implements OnDestroy, OnInit {
hasId: boolean = false;
constructor(private route: ActivatedRoute) {
}
ngOnInit() {
if(this.route.snapshot.params.id !== null)
{
// do magic....
}
}
}

Angular 2: Unhandled Promise rejection: No provider for AuthService! ; Zone: angular

I use an AuthService and an AuthGuard to log in/log out users and guard routes. The AuthService is used in the AuthGuard as well as in a LoginComponent. The AuthGuard is used to guard routes via CanActivate. When I try to run the app I get the following error:
zone.js:522 Unhandled Promise rejection: No provider for AuthService! ; Zone: angular ; Task: Promise.then ; Value: NoProviderError {__zone_symbol__error: Error: DI Error
at NoProviderError.ZoneAwareError
I have checked that the LoginComponent and AuthGuard both import the AuthService and inject it into the components via the constructor. I have also checked that the AuthService is imported into the AppModule file and added to the providers array so it can be used as a singleton service.
Edited to add code samples:
My App module contains the following:
#NgModule({
imports: [...],
providers: [..., AuthService, AuthGuard, ...],
declarations: [..., LoginComponent, EntryComponent ...],
bootstrap: [EntryComponent]
})
export class AppModule {
}
AuthGuard:
import { Injectable } from '#angular/core';
import { CanActivate, Router, ActivatedRouteSnapshot, RouterStateSnapshot } from '#angular/router';
import { ApiConfig } from '../Api';
import { AuthService } from './authservice';
#Injectable()
export class AuthGuard implements CanActivate {
constructor(
private authService: AuthService,
private router: Router,
private config: ApiConfig
) { }
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot) {
console.log(this.isAuthenticated());
if (this.isAuthenticated()) {
if (this.config.defined) {
return true;
} else {
this.authService.setConfig();
return true;
}
} else {
this.router.navigate(['/Login']);
return false;
}
}
// Checks if user is logged in
isAuthenticated() {
return this.authService.userLoggedIn();
}
}
LoginComponent constructor:
constructor(
private router: Router,
private notifications: Notifications,
private authService: AuthService
) {}
AuthService:
import { Injectable } from '#angular/core';
import { Http, Headers, RequestOptions } from '#angular/http';
import { Router } from '#angular/router';
import { Observable } from 'rxjs/Observable';
import { ApiConfig } from '../Api';
#Injectable()
export class AuthService {
constructor(
private http: Http,
private router: Router,
private config: ApiConfig
) {
this.apiRoot = localStorage.getItem('apiRoot');
}
...
}
In your app.component class #Component({}) decoration add a line that states:
providers: [AuthService]
This creates a singleton service for that service at that level. If you wanted to provide it at a more granular level you would provide (instantiate) it at that lower level.
Please see the first couple paragraphs in this official angular 2 documentation
Turns out I didn't capitalize my AuthService import correctly in AuthGuard.....
import { AuthService } from './authservice';
should have been
import { AuthService } from './AuthService';
Note to self: imports are case sensitive

Cookie undefined in service | cookie value exist in component | angular

api.service.ts
import { Injectable, Inject } from '#angular/core';
import { Http, Headers } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/throw';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/timeout';
import 'rxjs/add/operator/retry';
import { CacheService } from './cache.service';
import { AuthService } from '../services/auth.service';
import { CookieService } from 'angular2-cookie/core';
#Injectable()
export class ApiService {
constructor(
public _http: Http,
private _auth: AuthService,
private _cookie: CookieService,
#Inject('isBrowser') public isBrowser: boolean
) {}
get(){
console.log(this._cookie.get('Token'));//undefined
}
}
controller.component.ts
import { Component, OnInit, ChangeDetectionStrategy } from '#angular/core';
import { ActivatedRoute } from '#angular/router';
import { ApiService } from './api.service';
import { ReviewComponent } from '../shared/+review/review.component';
import { CookieService } from 'angular2-cookie/core';
// import { ModelService } from '../shared/model/model.service';
#Component({
selector: 'mall',
templateUrl: './mall.component.html',
styleUrls:['./mall.component.css'],
providers: [ ApiService, CookieService ]
})
export class MallComponent implements OnInit {
constructor(private _api: ApiService, private route: ActivatedRoute, private _cookie: CookieService) {}
ngOnInit(){
this._cookie.get('Token');// Token => value
this._api.get(); //Token => undefined
}
}
I don't understand this behavior. The cookie exist when i access in controller directly but is undefined when i access through service.
Is there any way to access cookie through services?
using https://github.com/salemdar/angular2-cookie with angular universal.
Maybe this?
ngOnInit(){
this._cookie.put('Token', WHATEVER_TOKEN_IS);// Token => value
console.log(this._api.get('Token')); //Token => undefined
}
and then
api-service
export class ApiService {
constructor(
readonly _http: Http,
private _auth: AuthService,
private _cookie: CookieService,
#Inject('isBrowser') public isBrowser: boolean
) {}
get() {
const token = this._cookie.get('Token');
console.log(token);
return token;
}
}
This might be late, but I went through the same problem.
I was not defining the base path as "/". So what was happening is that the cookie was being set for the default path where I was.
Eg. I was at site.com/auth/
Cookie would get saved at path "/auth"
If I save a cookie like
this.cookieService.set('token', token, null, "/");
then problem is solved.
Hope this helps further devs.
It was my mistake to add CookieService in component providers which initiate a new instance of service which was causing the issue.
#Component({
selector: 'mall',
templateUrl: './mall.component.html',
styleUrls:['./mall.component.css'],
providers: [ ApiService, CookieService ] //<--- here
})
CookieService should only be imported into AppComponent(root) to make a single instance available to other components.

Angular 2: Passing Data to Routes?

I am working on this angular2 project in which I am using ROUTER_DIRECTIVES to navigate from one component to other.
There are 2 components. i.e. PagesComponent & DesignerComponent.
I want to navigate from PagesComponent to DesignerComponent.
So far its routing correctly but I needed to pass page Object so designer can load that page object in itself.
I tried using RouteParams But its getting page object undefined.
below is my code:
pages.component.ts
import {Component, OnInit ,Input} from 'angular2/core';
import { GlobalObjectsService} from './../../shared/services/global/global.objects.service';
import { ROUTER_DIRECTIVES, RouteConfig } from 'angular2/router';
import { DesignerComponent } from './../../designer/designer.component';
import {RouteParams} from 'angular2/router';
#Component({
selector: 'pages',
directives:[ROUTER_DIRECTIVES,],
templateUrl: 'app/project-manager/pages/pages.component.html'
})
#RouteConfig([
{ path: '/',name: 'Designer',component: DesignerComponent }
])
export class PagesComponent implements OnInit {
#Input() pages:any;
public selectedWorkspace:any;
constructor(private globalObjectsService:GlobalObjectsService) {
this.selectedWorkspace=this.globalObjectsService.selectedWorkspace;
}
ngOnInit() { }
}
In the html, I am doing following:
<scrollable height="300" class="list-group" style="overflow-y: auto; width: auto; height: 200px;" *ngFor="#page of pages">
{{page.name}}<a [routerLink]="['Designer',{page: page}]" title="Page Designer"><i class="fa fa-edit"></i></a>
</scrollable>
In the DesignerComponent constructor I have done the following:
constructor(params: RouteParams) {
this.page = params.get('page');
console.log(this.page);//undefined
}
So far its routing correctly to designer, but when I am trying to access page Object in designer then its showing undefined.
Any solutions?
You can't pass objects using router params, only strings because it needs to be reflected in the URL. It would be probably a better approach to use a shared service to pass data around between routed components anyway.
The old router allows to pass data but the new (RC.1) router doesn't yet.
Update
data was re-introduced in RC.4 How do I pass data in Angular 2 components while using Routing?
It changes in angular 2.1.0
In something.module.ts
import { NgModule } from '#angular/core';
import { CommonModule } from '#angular/common';
import { BlogComponent } from './blog.component';
import { AddComponent } from './add/add.component';
import { EditComponent } from './edit/edit.component';
import { RouterModule } from '#angular/router';
import { MaterialModule } from '#angular/material';
import { FormsModule } from '#angular/forms';
const routes = [
{
path: '',
component: BlogComponent
},
{
path: 'add',
component: AddComponent
},
{
path: 'edit/:id',
component: EditComponent,
data: {
type: 'edit'
}
}
];
#NgModule({
imports: [
CommonModule,
RouterModule.forChild(routes),
MaterialModule.forRoot(),
FormsModule
],
declarations: [BlogComponent, EditComponent, AddComponent]
})
export class BlogModule { }
To get the data or params in edit component
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute, Params, Data } from '#angular/router';
#Component({
selector: 'app-edit',
templateUrl: './edit.component.html',
styleUrls: ['./edit.component.css']
})
export class EditComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit() {
this.route.snapshot.params['id'];
this.route.snapshot.data['type'];
}
}
You can do this:
app-routing-modules.ts:
import { NgModule } from '#angular/core';
import { RouterModule, Routes } from '#angular/router';
import { PowerBoosterComponent } from './component/power-booster.component';
export const routes: Routes = [
{ path: 'pipeexamples',component: PowerBoosterComponent,
data:{ name:'shubham' } },
];
#NgModule({
imports: [ RouterModule.forRoot(routes) ],
exports: [ RouterModule ]
})
export class AppRoutingModule {}
In this above route, I want to send data via a pipeexamples path to PowerBoosterComponent.So now I can receive this data in PowerBoosterComponent like this:
power-booster-component.ts
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute, Params, Data } from '#angular/router';
#Component({
selector: 'power-booster',
template: `
<h2>Power Booster</h2>`
})
export class PowerBoosterComponent implements OnInit {
constructor(
private route: ActivatedRoute,
private router: Router
) { }
ngOnInit() {
//this.route.snapshot.data['name']
console.log("Data via params: ",this.route.snapshot.data['name']);
}
}
So you can get the data by this.route.snapshot.data['name'].
1. Set up your routes to accept data
{
path: 'some-route',
loadChildren:
() => import(
'./some-component/some-component.module'
).then(
m => m.SomeComponentModule
),
data: {
key: 'value',
...
},
}
2. Navigate to route:
From HTML:
<a [routerLink]=['/some-component', { key: 'value', ... }> ... </a>
Or from Typescript:
import {Router} from '#angular/router';
...
this.router.navigate(
[
'/some-component',
{
key: 'value',
...
}
]
);
3. Get data from route
import {ActivatedRoute} from '#angular/router';
...
this.value = this.route.snapshot.params['key'];

Categories

Resources