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.");
}
}
Related
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
});
}
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()));
}
}
I build an app in Angular2 and Spring-MVC, and when I try to make a POST request to my server I don't get any sign of success or fail, but the request doesn't happening because I can't see the new data. When I do the request from Postman - the request is successful and I can see the new data.
The Angular2 code:
import { Injectable } from '#angular/core';
import { Http, Response, Headers, RequestOptions } from '#angular/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/operator/map';
#Injectable()
export class MainContentService {
constructor(
private http: Http) {}
addNewCategory(categoryName: string) {
let body = JSON.stringify({ name: categoryName });
let headers = new Headers({ 'Content-Type': 'application/json' });
let options = new RequestOptions({ headers: headers });
console.log(body);
console.log(options);
return this.http.post('http://localhost:8080/api/v1/categories', body, options)
.map(this.extractData)
.catch(this.handleError);
}
private extractData(res: Response) {
let body = res.json();
console.log(body);
return body.data || { };
}
private handleError (error: any) {
console.error(error);
return Observable.throw(error.json().error || 'Server error');
}
}
I can see the console.log(body) and console.log(option) printed in the dev-tools console, but nothing more then that:
The postman request:
My component:
import { Component } from '#angular/core';
import { MainContentService } from '../shared/main-content.service';
#Component({
moduleId: module.id,
selector: 'bfy-add-category',
templateUrl: 'add-category.component.html',
styleUrls: ['add-category.component.css'],
providers: [MainContentService]
})
export class AddCategoryComponent {
editName: string;
constructor(
private mainContentService: MainContentService
) { }
cancel() {
console.log('cencel');
}
save() {
let categoryName = this.editName;
this.mainContentService.addNewCategory(categoryName);
}
}
My component HTML code:
<div class="col-sm-12 col-md-8 col-lg-9 thumbnail pull-right">
<label>Category: </label>
<input [(ngModel)]="editName" placeholder="Category name .."/>
<div>
<button (click)="cancel()">Cancel</button>
<button (click)="save()">Save</button>
</div>
</div>
The http.get/post/... methods wait as long as someone subscribes to the Observable. It won't make a request before that happens. This is called a cold observable. Subscribing works like that:
http.get("http://yoururl.com").subscribe(data => { ... });
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);
});
})
)
I am using Angular v2 (2.0.0-beta-1) and displaying a simple chart using Google Charts.
import {Component, View} from "angular2/core";
import {Http, HTTP_PROVIDERS} from "angular2/http";
import {OnInit, OnDestroy} from 'angular2/core';
declare let io: any;
declare let google: any;
#Component({
selector:'default',
viewProviders: [HTTP_PROVIDERS]
})
#View({
templateUrl: 'app/default/default.html'
})
export class DefaultPage implements OnInit, OnDestroy {
charttitle: string;
data: any;
options: any;
timerToken: any;
chart: any;
socket: any;
constructor(http: Http) {
}
ngOnInit() {
console.log("onInit");
this.charttitle = "Sample Graph using live data";
this.options = {
title: "My Daily Activities",
is3D: true
};
this.socket = io();
this.socket.on("data_updated", (msg) => {
this.data = new google.visualization.DataTable();
this.data.addColumn('string', 'Task');
this.data.addColumn('number', 'Hours per Day');
this.data.addRows(5);
let data = JSON.parse(msg).activityData;
for (let i = 0; i < data.length; i++) {
let act = data[i];
this.data.setCell(i, 0, act.act);
this.data.setCell(i, 1, act.value);
}
this.chart.draw(this.data, this.options);
});
this.chart = new google.visualization.PieChart(document.getElementById('chart_div'));
google.visualization.events.addListener(this.chart, 'select', this.mySelectHandler);
}
mySelectHandler() {
console.trace();
console.log("Chart: " + this);
//let selectedItem = this.chart.getSelection()[0];
/*if (selectedItem) {
let value = this.data.getValue(selectedItem.row, 0);
console.log("The user selected: " + value);
}*/
}
ngOnDestroy() {
console.log("onDestroy");
this.socket.disconnect();
}
}
The problem I have is the following line.
google.visualization.events.addListener(this.chart, 'select', this.mySelectHandler);
The event is registered is when an element on the pie chart is selected the actual event handler is fired. But all the Angular JS 2 scope variables referenced by this aren't in scope. It's as if the Google Chart visualization library is running in its own scope.
I know that Angular JS has the Angular-Charts directive but we cannot use that as the company wants to use Angular v2 only.
Is there a way I can get the Google Charts API to 'bubble' an event to the event handler running on the scope of the Angular component.
If you want that your mySelectHandler takes part within the Angular2 context / change detection, you could leverage NgZone, as described below. This way, the changes you make in this function will update the view accordingly.
import {NgZone} from 'angular2/core';
export class DefaultPage implements OnInit, OnDestroy {
constructor(private ngZone:NgZone) {
}
ngOnInit()
this.chart = new google.visualization.PieChart(
document.getElementById('chart_div'));
google.visualization.events.addListener(
this.chart, 'select', () => {
this.ngZone.run(() => {
this.mySelectHandler();
});
}
);
}
}
Hope that I correctly understood your question.
Thierry