Im trying to listen to a click Event in one Component, after the click a Function from a service gets called.
Component that listens to the click:
searchForm: FormGroup;
constructor(private search: SearchService) {
}
ngOnInit() {
this.searchForm = new FormGroup({
'search': new FormControl(null)
})
this.search.searchEvent.subscribe({ // For testing purposes
next: (searchResponse) => {
console.log(searchResponse)
}
})
}
onSearch(){
this.search.onSearch(this.searchForm)
}
The Service:
subscription: Subscription;
searchEvent: Subject<any> = new Subject<any>();
constructor(private http: HttpClient, private route: Router) {
}
onSearch(searchForm: FormGroup){
if(searchForm.controls['search'].value){
this.http.post(URL, searchForm.controls['search'].value).subscribe(searchResponse => {
this.searchEvent.next(searchResponse)
})
this.route.navigate(['/company-search'])
}
this.subscription = searchForm.controls['search'].valueChanges.subscribe(val => {
if(!val){
this.route.navigate(['/'])
this.subscription.unsubscribe();
}
})
}
Now i have another component that needs the SearchResponse from the http Request
searchResponse: object[];
subscription: Subscription;
constructor(private search: SearchService) {
this.searchResponse = [];
}
ngOnInit(){
this.subscription = this.search.searchEvent.subscribe(result => {
console.log(result)
})
}
ngOnDestroy() {
this.subscription.unsubscribe();
}
The Problem I have is that when a click happens the next function get called but only in the component that listens to the click. The Subscription in the Component that needs the SearchResponse doesnt log the message from the next event but the one in the first Component does.
PS.
I have to do it with a service because in the parent html im using a router outlet
Related
I need to stream data from my backend (in spring) to my angular frontend.
I cant get the netty socket.io implementation working.
public ConnectListener onUserConnectWithSocket = new ConnectListener() {
#Override
public void onConnect(SocketIOClient socketIOClient) {
log.info("Client connected: " + socketIOClient.getSessionId());
socketIOClient.sendEvent("getAllDashboardData", generateRandomValues());
}
};
public DataListener<String> getAllDashboardData = new DataListener<String>() {
#Override
public void onData(SocketIOClient socketIOClient, String message, AckRequest ackRequest) throws Exception {
log.info("Message received: " + message);
socketIOClient.sendEvent("getAllDashboardData", generateRandomValues().toString());
}
};
when i have something like this, the EventListener never gets called (does not log User requested data). Hence, the onConnect logs that the frontend connected.
I tried out the frontend call using express!
This simple examples works perfect:
module.exports = (io) => {
io.on('connect', (socket) => {
console.log('user connected');
socket.on('getAllDashboardData', (data) => {
//send some data to client back
socket.emit('getAllDashboardData', {data: 'data'});
});
socket.on('disconnect', () => {
console.log('user disconnected');
});
});
}
how could i write this in spring?
I also tested the backend with postman and it works fine!
The answer is:
import { Injectable } from '#angular/core';
const io = require('socket.io-client');
import {Observable} from "rxjs";
#Injectable({
providedIn: 'root'
})
export class SocketService {
socket: any;
readonly uri = 'ws://localhost:8085';
constructor() {
this.socket = io(this.uri);
}
listen(eventName: string) {
return new Observable((resolve) => {
this.socket.on(eventName, (data: any) => {
// this.socket.emit(eventName, data); maybe don't use this produces 1000 of calls
resolve.next(data);
});
});
}
emit(eventName: string, data: any) {
this.socket.emit(eventName, data);
}
}
and use socket.io-client version 2.3.0 to work with netty spring.
I have the below code in Angular component
export class ScheduleComponent implements OnInit, OnDestroy {
source:any;
connect(dateValue){
this.source = new
EventSource('http://localhost:8080/api/schedbydate?mydate='+dateValue);
this.source.addEventListener('datarec', datarec => {
let schedule: Notification;
this.schedule = JSON.parse(datarex.data);
}, false);
}
ngOnInit() {
this._erdayService.getErday().subscribe((erday) => {
this._date = erday.text();
this._erdayService.currentMessage.subscribe(message => {
this._date = message;
this.connect(this._date);}
, (error) => { console.error('SERVER ERROR: SELECTED DAY'); });}
, (error) => { console.error('SERVER ERROR:getSchedulesByDate()'); });
}
ngOnDestroy() {
this.source.removeEventListener('message', this.message, false);
//this line doesn't work because I can't access enter variable here!
console.log("Server stopped schedule");
}
}
The issue is the this._date is initially loaded erday and UI view is according to erday. Now when I change the this._date to message, the UI view gets changed.
But still the erday data is shown in UI and the UI view fluctuates between erday & message and I'm not able to stop the this.source.addEventListener().
I tried to destroy in ngOnDestroy(),but it is not working.
I even tried this.source.close();.
Can someone help to know how to stop the listener created before calling another listener on same source ?
You subscribe to 2 data sources that emits continuously :
- The first being this._erdayService.currentMessage
- The second is this.source (when you trigger this.connect())
So this._date will change continuously. So you have to decide which data source you want to keep.
Case 1: You want to keep this.source as your data provider:
export class ScheduleComponent implements OnInit, OnDestroy {
source:any;
sourceListenerSubscription$ : Observable<any>;
connect(dateValue){
this.source = new
EventSource('http://localhost:8080/api/schedbydate?mydate='+dateValue);
this.sourceSubscription$ = Observable.fromEvent(this.source, 'datarec').subscribe( datarec => {
let schedule: Notification;
this.schedule = JSON.parse(datarex.data);
}, false);
}
ngOnInit() {
this._erdayService.getErday().subscribe((erday) => {
this._date = erday.text();
// take only one erday message, then listen to your spring server
this._erdayService.currentMessage.take(1).subscribe(message => {
this._date = message;
this.connect(this._date);}
, (error) => { console.error('SERVER ERROR: SELECTED DAY'); });}
, (error) => { console.error('SERVER ERROR:getSchedulesByDate()'); });
}
ngOnDestroy() {
this.source.removeEventListener('message', this.message, false);
//this line doesn't work because I can't access enter variable here!
console.log("Server stopped schedule");
}
}
Case 2: You want to keep erday as your data provider:
export class ScheduleComponent implements OnInit, OnDestroy {
source:any;
sourceListenerSubscription$ : Observable<any>;
connect(dateValue){
this.source = new
EventSource('http://localhost:8080/api/schedbydate?mydate='+dateValue);
// take date once from spring server, and keep erday as data source
this.sourceSubscription$ = Observable.fromEvent(this.source, 'datarec').take(1).subscribe( datarec => {
let schedule: Notification;
this.schedule = JSON.parse(datarex.data);
}, false);
}
ngOnInit() {
this._erdayService.getErday().subscribe((erday) => {
this._date = erday.text();
this._erdayService.currentMessage.subscribe(message => {
this._date = message;
this.connect(this._date);}
, (error) => { console.error('SERVER ERROR: SELECTED DAY'); });}
, (error) => { console.error('SERVER ERROR:getSchedulesByDate()'); });
}
ngOnDestroy() {
this.source.removeEventListener('message', this.message, false);
//this line doesn't work because I can't access enter variable here!
console.log("Server stopped schedule");
}
}
This seems like a pretty simple case to me, but I'm obviously missing something. I have a Model to be bound to the View. I then load the Model with an Http call. Why doesn't the View update? I thought that was the whole point of one-way binding.
I have verified that I'm getting back the data I'm expecting from the http call.
Update
I added a button to the screen and databinding will actually update the screen with the http loaded data for both fields on button push, even though the button method only sets one of the values. So either there's a bug in NativeScript or I'm not doing something incorrectly.
Update 2 Just the act of clicking the button will trigger the binding to happen. I've modified the code to have an empty tap handler, and just clicking the button makes it bind.
typescript
import { Component, ChangeDetectionStrategy, OnInit } from "#angular/core";
import { Job } from "../../shared/customer/job";
import { Http, Headers, Response } from "#angular/http";
import { Observable } from "rxjs/Rx";
#Component({
selector: "my-app",
templateUrl: "pages/job-details/job-details.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class JobDetailsComponent implements OnInit {
job: Job;
salesAssociateName: string = "x";
constructor(private http: Http) {
this.job = new Job();
}
ngOnInit() {
this.getJob(1234);
}
getJob(leadId: number) {
var url = "https://url-not-for-you/job?franchiseeid=48&leadid=" + leadId;
var headers = this.createRequestHeader();
this.http.get(url, { headers: headers }).map(response => response.json())
.do(data => this.setData(data[0]))
.subscribe(
() => this.success(),
(error) => this.error()
);
}
private createRequestHeader() {
let headers = new Headers();
headers.append("AuthKey","blah");
headers.append("AuthToken", "blee");
return headers;
}
setData(job) {
this.job.FullName = job["FullName"];
this.job.SalesAssociateName = job["SalesAssociateName"];
this.salesAssociateName = this.job.SalesAssociateName;
console.log("Found job for customer: " + job["FullName"]);
}
success() {
// nothing useful
}
error() {
alert("There was a problem retrieving your customer job.");
}
changeSA() {
}
}
html
<StackLayout>
<Label [text]="job.FullName"></Label>
<Label [text]="salesAssociateName"></Label>
<Button text="Push" (tap)="changeSA()"></Button>
</StackLayout>
Your code will work as expected with the default ChangeDetectionStrategy. however, you have changed the strategy to onPush
In order to make your binding work as expected in the default changeStrategy delete the following line
changeDetection: ChangeDetectionStrategy.OnPush
or change it to
changeDetection: ChangeDetectionStrategy.Default
More about the Angular-2 ChangeDetectionStrategy here and here
If you still want to use onPush instead of the default strategy then your properties should be declared as #input() and once the change is made (in your case in setData) marked with markForCheck()
The reason your binding is working when triggered from Button tap is because
application state change can be triggered by:
Events - tap, swipe,
XHR - Fetching data from a remote server
Timers - e.g. setTimeout()
For testing purposes and if someone is interested of how to implement the scenario with onPush here is a sample code:
import { Component, ChangeDetectionStrategy, ChangeDetectorRef, OnInit, NgZone, Input } from "#angular/core";
import { Http, Headers, Response } from "#angular/http";
import { Observable as RxObservable } from "rxjs/Rx";
import "rxjs/add/operator/map";
import "rxjs/add/operator/do";
#Component({
selector: "my-app",
templateUrl: "app.component.html",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class AppComponent implements OnInit {
#Input() public job: any = { salesAssociateName: "default job" };
#Input() public salesAssociateName: string = "default name";
constructor(private http: Http, private change:ChangeDetectorRef) { }
ngOnInit() {
this.getJob();
}
getJob() {
var url = "http://httpbin.org/get";
var headers = this.createRequestHeader();
this.http.get(url, { headers: headers })
.map(response => response.json())
.do(data => {
this.setData();
}).subscribe(
() => this.success(),
(error) => this.error()
);
}
private createRequestHeader() {
let headers = new Headers();
return headers;
}
setData() {
this.job.salesAssociateName = "NEW job SalesAssociateName";
this.salesAssociateName = "NEW job FullName";
this.change.markForCheck();
}
success() {
alert("success");
}
error() {
alert("There was a problem retrieving your customer job.");
}
}
I'm going to use Angular2 to receive websocket incoming messages and update a webpage based on those received messages. Right now, I'm using a dummy echo websocket service and will replace it.
From my understanding, the function which receive websocket messages has to return an observable that is subscribed by a handler who will update the webpage. But I can't figure out how to return an observable.
Code snippet is attached below. The MonitorService creates a websocket connection and return an observable containing the received messages.
#Injectable()
export class MonitorService {
private actionUrl: string;
private headers: Headers;
private websocket: any;
private receivedMsg: any;
constructor(private http: Http, private configuration: AppConfiguration) {
this.actionUrl = configuration.BaseUrl + 'monitor/';
this.headers = new Headers();
this.headers.append('Content-Type', 'application/json');
this.headers.append('Accept', 'application/json');
}
public GetInstanceStatus = (): Observable<Response> => {
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
this.websocket.onmessage = (evt) => {
this.receivedMsg = evt;
};
return new Observable(this.receivedMsg).share();
}
}
Below is another component which subscribes to the observable returned from above and updates webpages correspondingly.
export class InstanceListComponent {
private instanceStatus: boolean
private instanceName: string
private instanceIcon: string
constructor(private monitor: MonitorService) {
this.monitor.GetInstanceStatus().subscribe((result) => {
this.setInstanceProperties(result);
});
}
setInstanceProperties(res:any) {
this.instanceName = res.Instance.toUpperCase();
this.instanceStatus = res.Status;
if (res.Status == true)
{
this.instanceIcon = "images/icon/healthy.svg#Layer_1";
} else {
this.instanceIcon = "images/icon/cancel.svg#cancel";
}
}
}
Now, I'm running into this error in the browser console
TypeError: this._subscribe is not a function
I put it on a plunker and I added a function for sending message to the Websocket endpoint. Here is the important edit:
public GetInstanceStatus(): Observable<any>{
this.websocket = new WebSocket("ws://echo.websocket.org/"); //dummy echo websocket service
this.websocket.onopen = (evt) => {
this.websocket.send("Hello World");
};
return Observable.create(observer=>{
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
})
.share();
}
Update
As you mentioned in your comment, a better alternative way is to use Observable.fromEvent()
websocket = new WebSocket("ws://echo.websocket.org/");
public GetInstanceStatus(): Observable<Event>{
return Observable.fromEvent(this.websocket,'message');
}
plunker example for Observable.fromEvent();
Also, you can do it using WebSocketSubject, although, it doesn't look like it's ready yet (as of rc.4):
constructor(){
this.websocket = WebSocketSubject.create("ws://echo.websocket.org/");
}
public sendMessage(text:string){
let msg = {msg:text};
this.websocket.next(JSON.stringify(msg));
}
plunker example
Get onMessage data from socket.
import { Injectable } from '#angular/core';
import {Observable} from 'rxjs/Rx';
#Injectable()
export class HpmaDashboardService {
private socketUrl: any = 'ws://127.0.0.0/util/test/dataserver/ws';
private websocket: any;
public GetAllInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
};
}).map(res => res.data).share();
}
**Get only single mesage from socket.**
public GetSingleInstanceStatus(objStr): Observable<any> {
this.websocket = new WebSocket(this.socketUrl);
this.websocket.onopen = (evt) => {
this.websocket.send(JSON.stringify(objStr));
};
return Observable.create(observer => {
this.websocket.onmessage = (evt) => {
observer.next(evt);
this.websocket.close();
};
}).map(res => res.data).share();
}
}
A different approach I used is with subject:
export class WebSocketClient {
private client: WebSocket | undefined;
private subject = new Subject<string>();
...
private connect() {
const client = new WebSocket(fakeUrl);
const client.onmessage = (event) => {
this.subject.next(event.data);
};
}
private watch() { return this.subject } // can be mapped
}
And using it will be in my opinion clearer:
const client = new WebSocketClient(); // can also be injected
client.connect();
client.watch().subscribe(x => ...);
Happy coding!
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);
});
})
)