Passing data between more than one component using single model and presist it? - rxjs

I have this service:
import { Injectable } from '#angular/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
#Injectable()
export class DataService<T> {
private subject: BehaviorSubject<Partial<T>> = new BehaviorSubject<Partial<T>>(null);
changeData(message: Partial<T>) {
this.subject.next(message);
}
clearData() {
this.subject.next(null);
}
getData(): Observable<Partial<T>> {
return this.subject.asObservable();
}
}
I'm using it like this.
Datepicker:
this.ds.changeData({
dateFrom: this.dateRange.start,
dateTo: this.dateRange.end
});
Main Component:
this.ds.getData().subscribe((data: FilterQuery) => {
console.log('Update data', data);
this.filterModel = data;
});
I would like to add few more components what is the best way to presist the data and build full query object? If you have any other suggestions, I will gladly appreciate it.

Related

Return EventEmitter as Observable in Nest.js

EventEmitter in Nestjs is wrapper around EventEmitter2 module. I whant that Server-Sent Events return Observable with EE.
import { Controller, Post, Body, Sse } from '#nestjs/common';
import { fromEvent } from 'rxjs';
import { EventEmitter2 } from '#nestjs/event-emitter';
import { OrdersService } from './orders.service';
import { CreateOrderDto } from './dto/create-order.dto';
#Controller('orders')
export class OrdersController {
constructor(private ordersService: OrdersService,
private eventEmitter2: EventEmitter2) {}
#Post()
createOrder(#Body() createOrderDto: CreateOrderDto) {
// save `Order` in Mongo
const newOrder = this.ordersService.save(createOrderDto);
// emit event with new order
this.eventEmitter2.emit('order.created', newOrder);
return newOrder;
}
#Sse('newOrders')
listenToTheNewOrders() {
// return Observable from EventEmitter2
return fromEvent(this.eventEmitter2, 'order.created');
}
}
But after subscribtion to this source from browser i've getting only errors
this.eventSource = new EventSource('http://localhost:3000/api/v1/orders/newOrders');
this.eventSource.addEventListener('open', (o) => {
console.log("The connection has been established.");
});
this.eventSource.addEventListener('error', (e) => {
console.log("Some erorro has happened");
console.log(e);
});
this.eventSource.addEventListener('message', (m) => {
const newOder = JSON.parse(m.data);
console.log(newOder);
});
It's quite likely that you forgot to format the event in the right way.
For SSE to work internally, each chunk needs to be a string of such format: data: <your_message>\n\n - whitespaces do matter here. See MDN reference.
With Nest.js, you don't need to create such message manually - you just need to return a JSON in the right structure.
So in your example:
#Sse('newOrders')
listenToTheNewOrders() {
// return Observable from EventEmitter2
return fromEvent(this.eventEmitter2, 'order.created');
}
would have to be adjusted to, for example:
#Sse('newOrders')
listenToTheNewOrders() {
// return Observable from EventEmitter2
return fromEvent(this.eventEmitter2, 'order.created')
.pipe(map((_) => ({ data: { newOrder } })));
}
the structure { data: { newOrder } } is key here. This will be later translated by Nest.js to earlier mentioned data: ${newOrder}\n\n

How do I pass data back to previous page using `navigateBack`?

I am using navigateTo to open a page with listview and would like to pass the results back using navigateBack but unable to achieve that. Any idea?
With Service class and Observable, you can achieve this.
notify.service.ts
import { Injectable } from '#angular/core';
import { Subject } from 'rxjs-compat/Subject';
#Injectable({
providedIn: 'root'
})
export class NotifyService {
private refreshDataForView = new Subject<any>();
refreshDataForParentViewObservable$ = this.refreshDataForView.asObservable();
public relaodDataForParentView(data: any) {
if (data) {
this.refreshDataForView.next(data);
}
}
}
Second componenet.ts
constructor(
private notifyService: NotifyService
) { }
goBack() {
this.notifyService.relaodDataForParentView({ data: 'any data you wanrt to pass here ' });
this.router.back();
}
First component.ts
reloadDataSubscription: any;
constructor(
private notifyService: NotifyService
) {}
ngOnInit() {
this.reloadDataSubscription = this.notifyService.refreshDataForParentViewObservable$
.subscribe((res) => {
console.log('======', res);
// do what you want to do with the data passed from second view
});
}

Angular UI not updating after data changes

Im new to the MEAN stack and having trouble with getting data changes to be refreshed in the UI. I know the data is getting saved properly in MongoDB, and also retrieved, because when I create a Todo item and I refresh the page, the newly added Todo item appears in the Todo List. The problem is that it isnt happening dynamically.
I've tried a number of different things including NgZone and ChangeDetectorRef to detect changes, not sure what I'm doing wrong..
Let me know if any more info is needed.. thank you!
The Todo List component:
import { Component, Input, OnInit, NgZone } from '#angular/core';
import { Todo } from '../todo.model';
import { TodoService } from '../../todo.service';
#Component({
selector: 'app-todo-list',
templateUrl: './todo-list.component.html',
styleUrls: ['./todo-list.component.scss'],
providers: [TodoService]
})
export class TodoListComponent implements OnInit {
#Input() todos: Todo[] = [];
constructor(private _todoService: TodoService, private zone: NgZone){}
getTodos() {
console.log('todo list - get todos');
this._todoService.getTodos()
.subscribe(resTodoData => {
this.zone.run(() => {
this.todos = resTodoData;
});
});
}
ngOnInit() {
console.log('todo list - init');
this.getTodos();
}
}
Service Component:
import { Injectable, NgZone } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from
'#angular/http';
import { map } from 'rxjs/operators';
import { Todo } from './todos/todo.model';
#Injectable({
providedIn: 'root'
})
export class TodoService {
// these were configured in express server
private _getUrl = "/api/todos";
private _postUrl = "/api/todo";
constructor(private _http: Http, private zone: NgZone) { }
getTodos() {
let json = this._http.get(this._getUrl)
.pipe(map((response: Response) => response.json()));
return json;
}
addTodo(todo: Todo) {
let headers = new Headers({ 'Content-Type': 'application/json'
});
let options = new RequestOptions({ headers: headers });
return this._http.post(this._postUrl, JSON.stringify(todo),
options)
.pipe(map((response: Response) => response.json()));
}
}

Angular & RXJS - [ts] Property 'map' does not exist on type 'Observable<User>'

Since creating a new angular 6 project, some previous code that I've copied over doesn't seem to be working. This primarily seems to be rxjs syntax
On the .map, it displays the error:
[ts] Property 'map' does not exist on type 'Observable'<User>'.
I seem to be getting a similar error on another file with .take
Is anyone able to point me in the right direction to resolve this please?
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router } from '#angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from 'angularfire2/auth';
import 'rxjs/add/operator/map';
import 'rxjs/add/operator/take';
import 'rxjs/add/operator/do';
#Injectable()
export class LoginGuardService implements CanActivate {
constructor(
private router: Router,
private auth: AngularFireAuth
) { }
canActivate(): Observable<boolean> {
return this.auth.authState.map(authState => {
if (authState) this.router.navigate(['/folders']);
return !authState;
}).take(1);
}
}
Second Guard
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot):
Observable<boolean> {
this.authGuardStateURL = state.url;
return this.auth.authState.pipe(
take(1)
.map(authState => !!authState)
.do(auth => !auth ? this.router.navigate(['/login']) : true)
)
}
I reckon you used Angular CLI to create your app. Angular 6 comes with RxJS 6 and since v5, RxJS has been using pipeable operators.
So your code should look like this:
import { Injectable } from '#angular/core';
import { ActivatedRouteSnapshot, RouterStateSnapshot, CanActivate, Router } from '#angular/router';
import { Observable } from 'rxjs';
import { AngularFireAuth } from 'angularfire2/auth';
import { map, take, tap } from 'rxjs/operators';
#Injectable()
export class LoginGuardService implements CanActivate {
constructor(
private router: Router,
private auth: AngularFireAuth
) { }
canActivate(): Observable<boolean> {
return this.auth.authState.pipe(
map(authState => {
if (authState) this.router.navigate(['/folders']);
return !authState;
}),
take(1)
)
}
//Second Guard
canActivate(route:ActivatedRouteSnapshot, state:RouterStateSnapshot): Observable<boolean> {
this.authGuardStateURL = state.url;
return this.auth.authState.pipe(
take(1),
map(authState => !!authState),
tap(auth => !auth ? this.router.navigate(['/login']) : true)
)
}
}
Notice how you import the operators now and how you put map and take inside pipe method.

Testing promise in Angular 2 ngOnInit

I have an Angular 2 component I am trying to put under test, but I am having trouble because the data is set in the ngOnInit function, so is not immediately available in the unit test.
user-view.component.ts:
import {Component, OnInit} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {User} from './user';
import {UserService} from './user.service';
#Component({
selector: 'user-view',
templateUrl: './components/users/view.html'
})
export class UserViewComponent implements OnInit {
public user: User;
constructor(
private _routeParams: RouteParams,
private _userService: UserService
) {}
ngOnInit() {
const id: number = parseInt(this._routeParams.get('id'));
this._userService
.getUser(id)
.then(user => {
console.info(user);
this.user = user;
});
}
}
user.service.ts:
import {Injectable} from 'angular2/core';
// mock-users is a static JS array
import {users} from './mock-users';
import {User} from './user';
#Injectable()
export class UserService {
getUsers() : Promise<User[]> {
return Promise.resolve(users);
}
getUser(id: number) : Promise<User> {
return Promise.resolve(users[id]);
}
}
user-view.component.spec.ts:
import {
beforeEachProviders,
describe,
expect,
it,
injectAsync,
TestComponentBuilder
} from 'angular2/testing';
import {provide} from 'angular2/core';
import {RouteParams} from 'angular2/router';
import {DOM} from 'angular2/src/platform/dom/dom_adapter';
import {UserViewComponent} from './user-view.component';
import {UserService} from './user.service';
export function main() {
describe('User view component', () => {
beforeEachProviders(() => [
provide(RouteParams, { useValue: new RouteParams({ id: '0' }) }),
UserService
]);
it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
return tcb.createAsync(UserViewComponent)
.then((rootTC) => {
spyOn(console, 'info');
let uvDOMEl = rootTC.nativeElement;
rootTC.detectChanges();
expect(console.info).toHaveBeenCalledWith(0);
expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);
});
}));
});
}
The route param is getting passed correctly, but the view hasn't changed before the tests are run. How do I set up a test that happens after the promise in ngOnInit is resolved?
IMO the best solution for this use case is to just make a synchronous mock service . You can't use fakeAsync for this particular case because of the XHR call for templateUrl. And personally I don't think the "hack" to make ngOnInit return a promise is very elegant. And you should not have to call ngOnInit directly, as it should be called by the framework.
You should already be using mocks anyway, as you are only unit testing the component, and don't want to be dependent on the real service working correctly.
To make a service that is synchronous, simple return the service itself from whatever methods are being called. You can then add your then and catch (subscribe if you are using Observable) methods to the mock, so it acts like a Promise. For example
class MockService {
data;
error;
getData() {
return this;
}
then(callback) {
if (!this.error) {
callback(this.data);
}
return this;
}
catch(callback) {
if (this.error) {
callback(this.error);
}
}
setData(data) {
this.data = data;
}
setError(error) {
this.error = error;
}
}
This has a few benefits. For one it gives you a lot of control over the service during execution, so you can easily customize it's behavior. And of course it's all synchronous.
Here's another example.
A common thing you will see with components is the use of ActivatedRoute and subscribing to its params. This is asynchronous, and done inside the ngOnInit. What I tend to do with this is create a mock for both the ActivatedRoute and the params property. The params property will be a mock object and have some functionality that appears to the outside world like an observable.
export class MockParams {
subscription: Subscription;
error;
constructor(private _parameters?: {[key: string]: any}) {
this.subscription = new Subscription();
spyOn(this.subscription, 'unsubscribe');
}
get params(): MockParams {
return this;
}
subscribe(next: Function, error: Function): Subscription {
if (this._parameters && !this.error) {
next(this._parameters);
}
if (this.error) {
error(this.error);
}
return this.subscription;
}
}
export class MockActivatedRoute {
constructor(public params: MockParams) {}
}
You can see we have a subscribe method that behaves like an Observable#subscribe. Another thing we do is spy on the Subscription so that we can test that it is destroyed. In most cases you will have unsubscribed inside your ngOnDestroy. To set up these mocks in your test you can just do something like
let mockParams: MockParams;
beforeEach(() => {
mockParams = new MockParams({ id: 'one' });
TestBed.configureTestingModule({
imports: [ CommonModule ],
declarations: [ TestComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
]
});
});
Now all the params are set for the route, and we have access to the mock params so we can set the error, and also check the subscription spy to make sure its been unsubscribed from.
If you look at the tests below, you will see that they are all synchronous tests. No need for async or fakeAsync, and it passes with flying colors.
Here is the complete test (using RC6)
import { Component, OnInit, OnDestroy, DebugElement } from '#angular/core';
import { CommonModule } from '#angular/common';
import { ActivatedRoute } from '#angular/router';
import { Subscription } from 'rxjs/Subscription';
import { TestBed, async } from '#angular/core/testing';
import { By } from '#angular/platform-browser';
#Component({
template: `
<span *ngIf="id">{{ id }}</span>
<span *ngIf="error">{{ error }}</span>
`
})
export class TestComponent implements OnInit, OnDestroy {
id: string;
error: string;
subscription: Subscription;
constructor(private _route: ActivatedRoute) {}
ngOnInit() {
this.subscription = this._route.params.subscribe(
(params) => {
this.id = params['id'];
},
(error) => {
this.error = error;
}
);
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
}
export class MockParams {
subscription: Subscription;
error;
constructor(private _parameters?: {[key: string]: any}) {
this.subscription = new Subscription();
spyOn(this.subscription, 'unsubscribe');
}
get params(): MockParams {
return this;
}
subscribe(next: Function, error: Function): Subscription {
if (this._parameters && !this.error) {
next(this._parameters);
}
if (this.error) {
error(this.error);
}
return this.subscription;
}
}
export class MockActivatedRoute {
constructor(public params: MockParams) {}
}
describe('component: TestComponent', () => {
let mockParams: MockParams;
beforeEach(() => {
mockParams = new MockParams({ id: 'one' });
TestBed.configureTestingModule({
imports: [ CommonModule ],
declarations: [ TestComponent ],
providers: [
{ provide: ActivatedRoute, useValue: new MockActivatedRoute(mockParams) }
]
});
});
it('should set the id on success', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
let debugEl = fixture.debugElement;
let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
expect(spanEls.length).toBe(1);
expect(spanEls[0].nativeElement.innerHTML).toBe('one');
});
it('should set the error on failure', () => {
mockParams.error = 'Something went wrong';
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
let debugEl = fixture.debugElement;
let spanEls: DebugElement[] = debugEl.queryAll(By.css('span'));
expect(spanEls.length).toBe(1);
expect(spanEls[0].nativeElement.innerHTML).toBe('Something went wrong');
});
it('should unsubscribe when component is destroyed', () => {
let fixture = TestBed.createComponent(TestComponent);
fixture.detectChanges();
fixture.destroy();
expect(mockParams.subscription.unsubscribe).toHaveBeenCalled();
});
});
Return a Promise from #ngOnInit:
ngOnInit(): Promise<any> {
const id: number = parseInt(this._routeParams.get('id'));
return this._userService
.getUser(id)
.then(user => {
console.info(user);
this.user = user;
});
}
I ran into the same issue a few days back, and found this to be the most workable solution. As far as I can tell, it doesn't impact anywhere else in the application; since #ngOnInit has no specified return type in the source's TypeScript, I doubt anything in the source code is expecting a return value from that.
Link to OnInit: https://github.com/angular/angular/blob/2.0.0-beta.6/modules/angular2/src/core/linker/interfaces.ts#L79-L122
Edit
In your test, you'd return a new Promise:
it('should have a name', injectAsync([TestComponentBuilder], (tcb: TestComponentBuilder) => {
// Create a new Promise to allow greater control over when the test finishes
//
return new Promise((resolve, reject) => {
tcb.createAsync(UserViewComponent)
.then((rootTC) => {
// Call ngOnInit manually and put your test inside the callback
//
rootTC.debugElement.componentInstance.ngOnInit().then(() => {
spyOn(console, 'info');
let uvDOMEl = rootTC.nativeElement;
rootTC.detectChanges();
expect(console.info).toHaveBeenCalledWith(0);
expect(DOM.querySelectorAll(uvDOMEl, 'h2').length).toBe(0);
// Test is done
//
resolve();
});
});
}));
}
I had the same issue, here is how I managed to fix it. I had to use fakeAsync and tick.
fakeAsync(
inject([TestComponentBuilder], (tcb: TestComponentBuilder) => {
tcb
.overrideProviders(UsersComponent, [
{ provide: UserService, useClass: MockUserService }
])
.createAsync(UsersComponent)
.then(fixture => {
fixture.autoDetectChanges(true);
let component = <UsersComponent>fixture.componentInstance;
component.ngOnInit();
flushMicrotasks();
let element = <HTMLElement>fixture.nativeElement;
let items = element.querySelectorAll('li');
console.log(items);
});
})
)

Resources