Angular 2 share Service between components in different routes - javascript

I have a behaviour in Angular 2 project that i don't know how to solve. I'm using webpack with Angular 2 2.3 (if this helps).
I have a complex project with structure like this:
- index.ts
- app.module.ts
- app.component.ts
- app.routes.ts
- services
- login.service.ts
- +innerapp
- inner.routes.ts
- inner.module.ts
- inner.component.ts
- inner.component.html
- services
-inner.service.ts
- insideinner
- insideinner.component.ts
- insideinner.component.html
- header
- header.component.ts
- header.component.html
- form
- form.component.ts
- form.component.html
When you execute shows login and then route to +innerapp. Inner.component.ts loads inner.services.ts and do a http call for data. A lot of data is moved from server and a let of BehaivorSubjects are initialized inside inner.service.ts.
All works fine, but in a moment user clicks button and loads form.component.ts with a big form. User fills form and click submit, in this moment inner.service is called to add data form. My surprise is inner.service haven't data, it's just initialised.
Code below.
//inner.routes.ts
export const routes = [
{ path: '', children: [
{ path: '', component: InnerComponent },
{ path: 'form', component: FormComponent },
]},
];
inner.module.ts
import { routes } from './inner.routes';
import { InnerComponent } from './inner.component';
import { FormComponent } from './insideinner/form/form.component';
// Services
import { InnerService } from './service/inner.service';
#NgModule({
declarations: [
// Components / Directives/ Pipes
InnerComponent,
FormComponent
],
imports: [
RouterModule.forChild(routes),
],
providers: [
InnerService
]
})
export class InnerModule {
public static routes = routes;
}
inner.component.ts:
#Component({
selector: 'inner',
templateUrl: './inner.component.html'
})
export class InnerComponent implements OnInit {
constructor ( private innerService: innerService ) {
this.innerService.fetchData()
.subscribe(
(response) => {
this.innerService.addData(response.json());
},
(error) => {
alert(error);
}
);
}
services/inner.services.ts
import { Injectable } from '#angular/core';
import { Observable, BehaviorSubject } from 'rxjs';
import { Headers, RequestOptions, Http, Response } from '#angular/http';
#Injectable()
export class InnerService {
// Observable string streams
public readonly data: Observable<string>;
// Observable string sources
private _data: BehaviorSubject<string> = new BehaviorSubject(null);
// private properties
private options: RequestOptions;
constructor( public http: Http ) {
this.data = this._user.asObservable();
// http standard values
let token = localStorage.getItem('token');
let cabs = new Headers({ Authorization: 'Bearer ' + token });
this.options = new RequestOptions({ headers: cabs });
}
// Service message commands
public addData (t: string) {
this._data.next(t);
}
public saveData(t: string) {
return this.http.post(blabla,
{
data: t
},
this.options
).map((res: Response) => {
this.addData(t);
return true;
}).catch(this.handleError);
}
private handleError (error: any) {
//code
}
public fetchData(): Observable<any> {
return this.http.get(blabla, this.options)
.map((res) => { return res.body })
.catch(this.handleError);
}
}
insideinner/form/form.component.ts
import { Component, OnInit, ViewEncapsulation } from '#angular/core';
// services & others
import { InnerService } from '../../services/inner.service';
#Component({
selector: 'add-members',
templateUrl: './form.component.html',
encapsulation: ViewEncapsulation.None
})
export class FormComponent implements OnInit {
constructor(
private fb: FormBuilder,
private innerService: InnerService
) {}
public ngOnInit() {
this.showForm = false;
}
public onSubmit(value: string) {
this.innerService.saveData(value); //Fail here, inner service are without data
}
public showAdd() {
this.showForm = true;
}
}
I read a lot of docs and read here similar problems, but solutions aren't working for me.
EDIT 2017-05-31
I think that is dupe question. I see that problem is related with lazyload in routes. I try this solution:
Angular 2 lazy loaded module - service not singleton
and this solution:
Angular 2 How to make singleton service available to lazy loaded modules
But no one work for me. I want to say that this project is in garbage and I began again with Angular 1.6 but I'm really interested in solve this problem to make future projects.

Related

Conditionally instantiate service class as a provider in NestJS

I have a controller called User and two service classes: UserAdminService and UserSuperAdminService.
When a user makes a request to any endpoint of the User controller, I want to check if the user making the request is an Admin or a Super Admin (based on the roles in the token) and instantiate the correct service (UserAdminService or UserSuperAdminService). Note that the two services implement the same UserService interface (just the internals of the methods that change a bit). How can I make this with NestJS?
What I tried:
user.module.ts
providers: [
{
provide: "UserService",
inject: [REQUEST],
useFactory: (request: Request) => UserServiceFactory(request)
}
],
user-service.factory.ts
export function UserServiceFactory(request: Request) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return UserSuperAdminService;
else
return UserAdminService;
}
user.controller.ts
constructor(
#Inject("UserService") private readonly userService: UserServiceInterface
) {}
One of the reasons my code is not working is because I am returning the classes and not the instantiated objects from the factory, but I want NestJS to resolve the services dependencies. Any ideas?
Rather than passing back the class to instantiate, which Nest doesn't handle, you could add the UserSuperAdminService and UserAdminService to the inject array, and pass back the instance that Nest then would create per request.
providers: [
{
provide: "UserService",
inject: [REQUEST, UserSuperAdminService, UserAdminService],
useFactory: (request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) => UserServiceFactory(request, superAdminService, adminService)
}
...
]
export function UserServiceFactory(request: Request, superAdminService: UserSuperAdminService, adminService: UserAdminService) {
const { realm_access } = JwtService.parseJwt(
request.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin))
return superAdminService;
else
return adminService;
}
Instead of trying to conditionally instantiate a service class you could create a global middleware to redirect the request to the appropriate controller e.g.
import { Injectable, NestMiddleware } from '#nestjs/common';
#Injectable()
export class AdminUserMiddleware implements NestMiddleware {
use(req: any, res: any, next: () => void) {
const { realm_access } = JwtService.parseJwt(
req.headers["authorization"].split(' ')[1]
);
if (realm_access["roles"].includes(RolesEnum.SuperAdmin)) {
req.url = req.url.replace(/^\/, '/super-admin/');
}
next();
}
}
Then you can apply it to all routes in your app.module.ts
#Module({
imports: [HttpModule],
controllers: [UserAdminController, UserSuperAdminController]
providers: [UserSuperAdminService, UserAdminService]
})
export class AppModule implements NestModule {
configure(consumer: MiddlewareConsumer) {
consumer
.apply(AdminUserMiddleware)
.forRoutes('/');
}
}
and have the following controlers:
#Controller('/')
export class UserAdminController {
private readonly logger: Logger = new Logger(UserAdminController.name);
constructor(private readonly userAdminService: UserAdminService) {}
#Controller('/super-admin')
export class UserSuperAdminController {
private readonly logger: Logger = new Logger(UserSuperAdminController.name);
constructor(private readonly userSuperAdminService: UserSuperAdminService) {}
}
See the NestJS docs and this post for further details

Routing child to parent is not working when navigates in Angular

I'm using angular 5.1.0, and I have an issue with the routing system, let me explain:
In my app-routing module I have an url /api that lazy loads another module, in that lazy loaded module I have the next routing implementation:
api-routing.module.ts
const routes: Routes = [
{
path: '',
component: ApisComponent,
data: {
breadcrumbs: 'APIs',
},
children: [
{
path: '',
component: ApiListComponent,
},
{
path: ':id',
component: ApiDetailComponent,
resolve: {
api: ApiResolverService
},
data: {
breadcrumbs: '{{ api.title }}',
},
},
],
},
];
The important thing here is the data param that the Router receives.
In my app I have a generic error behaviour that when an exception is throwed I have a errorHandler class that catch the error and redirects to another url: /error, this is the handler code:
import { ErrorHandler, Injectable, Injector } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class AppErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: any): void {
const routerService = this.injector.get(Router);
routerService.navigateByUrl('/error', { skipLocationChange: true });
}
}
The problem is, when an exception is throwed inside /api and handleError is executed, I see my generic error page rendered with the breadcrumb loaded in the last route: /api by data param.
Is there any way to set the Router to reset data when is loaded? or maybe I'm doing something wrong?
UPDATE
At this point I thought the problem was due to data param, but now I see that it's not the problem. Let me show my error.component that is rendered when Router loads /error:
import { Component, OnInit } from '#angular/core';
import { Router, ActivatedRoute } from '#angular/router';
#Component({
selector: 'app-error',
templateUrl: './error.component.html',
styleUrls: ['./error.component.scss']
})
export class ErrorComponent implements OnInit {
constructor(private router: Router, private route: ActivatedRoute) { }
ngOnInit() {
console.log('snapshot trace');
console.log(this.route.snapshot);
}
}
I have included in the onInit component method, a trace of ActivatedRoute snapshot to see what it has, and the thing is that trace is not showing when errorHandler navigates from /api to /error.
But if I load directly /error the trace is showed, so for any reason the error component is not instanciated correctly in the first scenario (navigate from /api to /error)
UPDATE
I have upgraded to angular 5.2.9 and the problem still happens.
I have solved the problem using NgZone, I think the "timing" routing problem that angular has involve the render error component out of angular zone, so, the AppErrorHandler class looks like this:
import { ErrorHandler, Injectable, Injector, NgZone } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class AppErrorHandler implements ErrorHandler {
constructor(private injector: Injector) { }
handleError(error: any): void {
const routerService = this.injector.get(Router);
const ngZone = this.injector.get(NgZone);
ngZone.run(() => {
routerService.navigate(['/error'], { skipLocationChange: true });
});
}
}
Here a github issue related to my problem

Angular 2 Interface throwing error of non existing property

I have an Angular 2 interface books.ts
export interface Books {
artists: Object;
tracks: Object;
}
This is the my service file where I am using it with http request searchService.ts
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import { Books } from 'app/pages/search-results/books';
import 'rxjs/add/operator/map'
#Injectable()
export class SearchService {
constructor(private _http:Http) { }
getBook(keyword): Observable<Books[]>{
return this._http.get('https://api.spotify.com/v1/search?q=' + keyword + '&type=track,artist')
.map((response: Response) => <Books[]> response.json());
}
}
And this is my component where I am using interface searchResults.ts
import { Component, OnInit } from '#angular/core';
import { ActivatedRoute, Router } from '#angular/router';
import { SearchService } from 'app/shared/search/search.service';
import { Books } from 'app/pages/search-results/books';
#Component({
selector: 'app-search-results',
templateUrl: './search-results.component.html',
styleUrls: ['./search-results.component.css'],
providers: [SearchService]
})
export class SearchResultsComponent implements OnInit {
keyword: any;
sub: any;
books: Books[];
errMessage: string;
arists: Object;
constructor(private _route: ActivatedRoute, private _router: Router, private _search: SearchService) { }
ngOnInit() {
this.sub = this._route
.queryParams
.subscribe(params => {
// Defaults to 0 if no query param provided.
this.keyword = params['keyword'] || 0;
this.getBooks(this.keyword);
});
//
}
getBooks(value) {
this._search.getBook(value)
.subscribe(
res => {
this.books = res;
console.log(res.artists);
},
error => { this.errMessage = <any>error }
);
}
}
The error comes when I try to console the res.artists. The error says Property 'artists' does not exist on type 'Books[]'. I am new to Angular 2 and doesn't know how to fix that.
The response is looks like
{artists:{limit: 20, item:[]}, tracks:{limit: 20, item:[]}}
I'm not sure but I think you try to get res.artist from collection of books. You can check it by for or e.g res[0].artist to get concrete artist.
getBook function in class SearchService return an array of Books object (Books[])
so, the res in getBooks function in SearchResultsComponent will be an Array of Books.
You can console.log(res) to see detail, if you want access to artists please try with res[0].artists if the res is not an empty array
The problem is that I am getting Object in response and I am assigning it to an Array which is causing the error. I have simply changes the both types to object and it solved my problem.
From this
books: Books[];
To this
books: Books;

Angular http service loop

Today i'm facing a new problem with services.
I'm trying to make an http service but when I try to store, in my service, the Observable object returned by http.get.map - my app crashs.
I wanted to achieve a "system" where the service loops to update datas and the components which subscribed to the observable update its data according to the service's data.
Here is the code :
afficheur.component.ts :
import { Component } from '#angular/core';
import {HardwareService} from "../../services/hardware.service";
#Component({
selector: 'afficheur',
templateUrl: 'app/components/hardware/afficheur.component.html'
})
export class AfficheurComponent{
public state: Boolean;
constructor(private service: HardwareService){
this.service
.getHardware()
.subscribe(data => (console.log(data), this.state = data.afficheur),
error => console.log(error),
() => console.log('Get etat afficheur complete'))
}
}
hardware.service.ts :
import { Injectable, OnInit } from '#angular/core';
import { Headers, Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/operator/catch';
import 'rxjs/add/operator/map';
#Injectable()
export class HardwareService implements OnInit{
private apiUrl = 'http://10.72.23.11:5000'; // URL to web API
private ressources: Observable<any>;
constructor (private http: Http) {}
ngOnInit() {
this.loopupdate()
}
loopupdate(): void {
setInterval(() => {
this.update();
}, 5000);
}
update(): void {
this.ressources = this.http.get(this.apiUrl)
.map(this.extractData)
.catch(this.handleError);
}
getHardware(){
return this.ressources;
}
private extractData(res: Response) {
let body = res.json();
return body || { };
}
private handleError (error: Response | any) {
// In a real world app, you might use a remote logging infrastructure
let errMsg: string;
if (error instanceof Response) {
const body = error.json() || '';
const err = body.error || JSON.stringify(body);
errMsg = `${error.status} - ${error.statusText || ''} ${err}`;
} else {
errMsg = error.message ? error.message : error.toString();
}
console.error(errMsg);
return Observable.throw(errMsg);
}
}
app.module.ts :
import { FormsModule } from '#angular/forms';
import { NgModule } from '#angular/core';
import { BrowserModule } from '#angular/platform-browser';
import { HttpModule } from '#angular/http';
import { AppComponent } from './app.component';
import { ROUTING } from './app.routes';
import {HardwareService} from "./services/hardware.service";
import {AfficheurComponent} from "./components/hardware/afficheur.component";
import {HardwareListComponent} from "./views/hardwarelist/hardwarelist.component";
#NgModule({
imports: [ BrowserModule, ROUTING, HttpModule, FormsModule, HttpModule],
declarations: [
AppComponent,
AfficheurComponent,
HardwareListComponent
],
bootstrap: [ AppComponent ],
providers: [ HardwareService ]
})
export class AppModule { }
Thanks again for being here :D
EDIT :
I got an error when i try to launch my app :
ERROR TypeError: Cannot read property 'subscribe' of undefined
I think it's related to the this.ressources initialization, any idea ?
EDIT 2 :
In my service :
initializePolling(){
return IntervalObservable.create(5000)
.flatMap(() => {
return this.getHardware()
});
}
getHardware(): Observable<any> {
return this.http.get(this.apiUrl)
.map(this.extractData)
.catch(this.handleError);
}
How can i subscribe to this with my component ? I don't know what method i should call in my component to fetch datas without make multiple calls if i have multiple components.
The problem is that ngOnInit(), like the one in your Injectable class, is a Lifecycle hook which only works with Directives and Components. You should try calling this.loopUpdate() from within the Injectable class' constructor. You can know more about this on another thread/question.
If you want to set an interval in fetching the data, do that in the component class, not in the service. In the service you should just have methods that return Observables (in your case) from calling http.get().... In that way you wouldn't have an undefined object returned and a more reusable service.
Also, here's another SO link for you to have look at.

How to map from one model to another in Angular 2?

I have this function in my Angular 2 component, which calls Web Api:
getNextConjunctionApi(): Observable<any> {
return this._http.get(this.uri + '/GetNextConjunction')
.map((res: Response) => res.json());
}
Web Api returns a complex object, which I would like to map to an Angular 2 model called ClientModel:
export class ClientModel {
prop1: string;
prop2: string;
...
}
Can this mapping be done by rewriting the map functionality, or need I do it in some other way?
.map((res: Response) => res.json());
I accomplished this with a slightly different approach. I had my component call a service that would return an observable. My component could then use a specific type that I created. I will show you what I have done for a blog.
posts.component.ts
import { Component, OnInit } from '#angular/core';
import { PostsService } from './posts.service';
import { PostComponent } from '../post/post.component'; // --> This is my custom type
import { Observable } from 'rxjs/Rx';
#Component({
selector: 'app-posts',
templateUrl: './posts.component.html',
providers: [PostsService]
})
export class PostsComponent implements OnInit {
posts: Observable<PostComponent[]>; // --> I use the type here
constructor( private _postsService: PostsService ) { }
ngOnInit() {
this._postsService.getAllPosts()
.subscribe(
posts => { this.posts = posts }, // --> I add the return values here
error => { console.log(error) }
);
}
}
The above has three key pieces. I import the custom type, PostComponent, set posts to an Observable of type PostComponent array, and as the Observable comes back, I add the values to the posts array.
posts.service.ts
import { Injectable } from '#angular/core';
import { Http, Response } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/catch';
#Injectable()
export class PostsService {
constructor( private _http: Http ) {}
getAllPosts(){
return this._http.get('[INSERT API CALL]')
.map((response: Response) => response.json())
.catch(msg => Observable.throw(msg));
}
}
In my service, I only map the response to response.json. This gives me more information than I need. I 'filter' it in my post.component
post.component.ts
import { Component, Input } from '#angular/core';
#Component({
selector: 'post',
templateUrl: './post.component.html'
})
export class PostComponent{
#Input() curPost: {
'id': number,
'title': string,
'author': string,
'date': string,
'body' : string,
};
constructor() { }
}

Categories

Resources