I'm passing a value from a service to a component using BehaviorSubject -
In serviceFile.service.ts:
taskComplete = new BehaviorSubject<{ complete: Boolean; error: any }>(null);
...
this.taskComplete.next({ complete: false, error: error });
...
In componentFile.component.ts:
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
this.error = data.error
? data.error.error.message
: null;
console.log(this.error);
}
);
}
The problem is that the value of property this.error is changed and printed in console.log(), but this change is not reflected in the component template. In other words, angular does not check this change and re-render.
You are initializing your taskComplete BehaviorSubject with null, so that's the first value emitted. However, in your component you are trying to access data.error when data is null for the first value emitted. The following should work:
this.error = data && data.error && data.error.error
? data.error.error.message
: null;
I created this working example: https://stackblitz.com/edit/angular-fh6cfg?file=src%2Fapp%2Fapp.component.ts
If this.myService.taskComplete is an asynchronous action you'll need to manually trigger change detection.
constructor(private cdr: ChangeDetectorRef) { }
...
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
this.error = ...;
this.cdr.markForCheck();
}
);
}
I'd suggest two changes.
If the default value of the BehaviourSubject is null and if you're forced to check if the value is null in each of it's subscription, you're better off using a ReplaySubject with buffer 1 instead. It'll buffer/hold the last value similar to BehaviorSubject but doesn't require a default value.
If the object's underlying reference hasn't changed, the Angular change detection may detect any changes to re-render the template. In that case try to make a hard-copy using JSON.parse(JSON.stringify()).
Service
taskComplete = new ReplaySubject<{ complete: Boolean; error: any }>(1);
Component
ngOnInit() {
this.taskCompleteSub = this.myService.taskComplete.subscribe(
(data) => {
// `null` check not required here now
this.error = JSON.parse(JSON.stringify(data.error.error.message));
console.log(this.error);
}
);
}
Related
I'm trying to get the current value of a BevhaviorSubject in Angular. I printed each, the whole thing and the value only, to the console to check its content by using these two lines:
console.log(this._isNumeric)
console.log(this._isNumeric.getValue())
...but what I am receiving is this:
closed: false
hasError: false
isStopped: false
observers: []
thrownError: null
_isScalar: false
_value: true
value: true
__proto__: Subject
for the subject (note that the value parameter is set to true) and
false
if I am just printing the value. Maybe I am making an obvious mistake, but does someone has a clue how to get the actual value of the BehaviorSubject? Using .value instead of .getValue() does not change the outcome. Thank you! :)
In your service you can create and expose the BehaviorSubject like this:
private _isNumeric$ = new BehaviorSubject<boolean>(false); // first value is false
/** Protects the _isNumeric$ BehaviorSubject from outside access */
public get IsNumeric$(): Observable<boolean> {
return this._isNumeric$.asObservable();
}
// NOTE: This is how you can access the value within the service
private get IsNumeric(): boolean {
return this._isNumeric$.getValue();
}
To clarify the use of the '$' at the end of the variable name. It does not do anything, it is just sometimes used by convention to indicate that a variable holds an Observable or that a function will return an Observable.
If you wanted to emit a new value you can do so using '.next()' on the BehaviorSubject
this._isNumeric$.next(true); // Emit a value of true
If you wanted to access the data from a component you can retrieve the data by subscribing to it like so. Remember to unsubscribe from the BehaviorSubject as well.
this.yourService.IsNumeric$
.pipe(takeUntil(this.onDestroy$)) // This is just a subject used to unsubscribe later
.subscribe((value: boolean) => {
// Use the result from the BehaviorSubject
});
Every Behavior Subject will require 3 things: a) initial value, b) a source that can be turned into an observable c) a public variable that can be subscribed. You can use Behavior Subject like this:
// In your service file, do this
#Injectable({ provideIn: 'root' })
export class YourServiceName {
// Set the initialvalues in this format ==>
// initialValuesForBehaviorSubject: YourModel = initialValue;
// In your case:
initialValue: boolean = false;
// Make a source using Behavior Subject as a type and above initial
// values as the initial value as a private variable
// This source is required to SET the data
// Your case:
private behaviorSubjectSource: BehaviorSubject<boolean> =
new BehaviorSubject<boolean>(this.initialValue);
// Create a public observable to GET the data
// Your case:
observableForBehaviorSubject: Observable<boolean> =
this.behaviorSubjectSource.asObservable();
// Create a method to set the data from anywhere in your application
setData(data: boolean) {
this.behaviorSubjectSource.next(data);
}
// Create a method to get data from anywhere in your application
getData(): Observable<boolean> {
return this.observableForBehaviorSubject;
}
}
///// In your component-a.ts file, set the value like this:
ngOnInit() {
this.yourService.setData(true);
}
//// In your component-b.ts, get the value like this:
ngOnInit() {
localVariableToStoreObservableData: boolean;
this.yourService.getData().subscribe(data => {
if(data) {
this.localVariableToStoreObservableData = data;
}
})
}
Assume data has already been cached in sessionStorage. I have hydrateStateWithSessionStorage in an external CacheService.js file. I import this file. When I try to pass this.setState to this function, I get this error:
Uncaught TypeError: Cannot read property 'updater' of undefined
How can I solve this? I could possibly use a the React hook useState and pass the setter function, but what if I want to use a class component instead of functional component? Or am I simply unable to pass setState because it implicitly uses the 'this' keyword in its implementation?
hydrateStateWithSessionStorage(state, setState) {
// for all items in state
for (let key in state) {
// if the key exists in localStorage
if (sessionStorage.hasOwnProperty(key)) {
// get the key's value from localStorage
let value = sessionStorage.getItem(key);
// console.log(value)
// parse the localStorage string and setState
try {
value = JSON.parse(value);
console.log('before')
setState({ [key]: value });
console.log('after')
} catch (e) {
// handle empty string
setState({ [key]: value });
}
}
}
}
//in the component consuming CacheService
//this.Cache = new CacheService(); //in the constructor
componentDidMount() {
this.Cache.hydrateStateWithLocalStorage(this.state, this.setState);
this.Auth.fetch('api/upcomingbill/').then((data) => {
this.setState({ list: data })
});
}
I would treat this function more of a check return sessionStorage object if nothing return undefined. Send this.state into the fn() then check response and return the response.
componentDidMount() {
const newState = hydrateState(this.state):
!!newState && this.setState(newState)
}
Just a brain dump..
How about this?
componentDidMount() {
this.Cache.hydrateStateWithLocalStorage(this);
this.Auth.fetch('api/upcomingbill/').then((data) => {
this.setState({ list: data })
});
}
And then using setState like so...
hydrateStateWithSessionStorage(ReactComp) {
for (let key in ReactComp.state) {
...
ReactComp.setState({ [key]: value });
....
}
}
FYI, this is not a React specific issue. The reason you are getting that error is, you are passing just a method that internally uses "this" (of that particular class) to the hydrateStateWithSessionStorage function. By the time, that method is called in the function, the scope has changed and "this" is not defined or not the same anymore.
I'm trying to learn Angular 2 and am rebuilding an Angular 1 app I've made with Angular 2 using the Angular CLI. I've setup a HTTP GET request, which fires successfully, and setup a subscriber to interpret the result, and console logging in the subscriber function shows the data I expect. However, no data is being updated on the template.
I tried setting the data to an initial value, to a value in the ngOnInit, and in the subscriber function, and the initial and ngOnInit update the template accordingly. For the life of me, I can't figure out why the template won't update on the subscribe.
events: any[] = ['asdf'];
constructor(private http: Http) {
}
ngOnInit() {
this.events = ['house'];
this.getEvents().subscribe(this.processEvents);
}
getEvents(): Observable<Event[]> {
let params: URLSearchParams = new URLSearchParams();
params.set('types', this.filters.types.join(','));
params.set('dates', this.filters.dates.join(','));
return this.http
.get('//api.dexcon.local/getEvents.php', { search: params })
.map((response: Response) => {
return response.json().events;
});
}
processEvents(data: Event[]) {
this.events = ['car','bike'];
console.log(this.events);
}
The data is being displayed via an ngFor, but car and bike never show. Where have I gone wrong?
You have gone wrong with not respecting the this context of TypeScript, if you do stuff like this:
.subscribe(this.processEvents);
the context get lost onto the processEvents function.
You have to either bind it:
.subscribe(this.processEvents.bind(this));
Use an anonymous function:
.subscribe((data: Events) => {this.processEvents(data)});
Or set your method to a class property:
processEvents: Function = (data: Event[]) => {
this.events = ['car','bike'];
console.log(this.events);
}
Pick your favourite, but I like the last option, because when you use eventListeners you can easily detach them with this method.
Not really sure with what's going on with that processEvents. If you want to subscribe to your response just do:
this.getEvents()
.subscribe(data => {
this.events = data;
});
I have issue, with passing async data to child component. I trying to write dynamic form generator. Issue starts when I try to call json via Observable and pass it into child component.
service:
generateSearchFields2(): Observable<any> {
return this.http
.get(this.API + 'searchFields')
.map((res:Response) => {
res.json().data as any;
for (var i = 0; i < res.json().data.length; i++) {
var searchField = res.json().data[i];
switch (searchField.component) {
case "TextboxQuestion":
let TXQ: TextboxQuestion = new TextboxQuestion({
key: searchField.key,
label: searchField.label,
value: searchField.value,
required: searchField.required,
order: searchField.order
});
this.searchFieldModels.push(TXQ);
console.log("TXQ: ", TXQ, this.searchFieldModels);
break;
case "DropdownQuestion":
let DDQ: DropdownQuestion = new DropdownQuestion({
key: searchField.key,
label: searchField.label,
required: searchField.required,
options: searchField.options,
order: searchField.order
});
this.searchFieldModels.push(DDQ);
console.log("TXQ: ", DDQ, this.searchFieldModels);
break;
default:
alert("DEFAULT");
break;
}
}
return this.searchFieldModels.sort((a, b) => a.order - b.order);
})
.catch(this.handleError);
}
Component Parent:
generateSearchFields2() {
this.service.generateSearchFields2()
.subscribe(res => this.searchFields = res)
}
Iam passing variable via INPUT directive in parent template to child: [searchFields]="searchFields"
Issue is in child component, where searchField has undefined value. In this child I pass value to another service, to create formContros, but I got undefined there also. Data missing starts here, in child:
#Input() searchFields: SearchBase<any>[] = [];
ngOnInit() {
this.form = this.qcs.toFormGroup(this.searchFields);
console.log("ONINIT DYNAMIC FORM COMPONENT: ", this.searchFields);
}
Please for hint how I can pass async variable, to not loose data meantime
You can make #Input() searchFields a setter
private _searchFields: SearchBase<any>[] = [];
#Input() set searchFields(value SearchBase<any>[]) {
if(value != null) {
this.form = this.qcs.toFormGroup(this.searchFields);
console.log("ONINIT DYNAMIC FORM COMPONENT: ", this.searchFields);
}
}
get searchFields() : SearchBase<any>[] {
return this.searchFields;
}
You can also use ngOnChanges() which is called every time an input is updated, but a setter is usually more convenient except perhaps when the executed code depends on multiple inputs being set.
In the ngOnInit event the data which comes from the parent is not bound yet. So your searchFields is undefined yet. You can use it in NgAfterViewInit component lifecycle event.
#Input() searchFields: SearchBase<any>[] = [];
ngAfterViewInit() {
this.form = this.qcs.toFormGroup(this.searchFields);
console.log("ONINIT DYNAMIC FORM COMPONENT: ", this.searchFields);
}
For other cases you can see Angular2 Component Lifecycle events
I am facing a weird issue in assigning response to a class's global variable from inside a observable. So my program logic is as follows:
Get latest playlists ID's from elastic search (i use elastic search from a type definition file). This returns me a PromiseLike to which i hook a then operator.
Inside the promise resolution, i make another http get call (i.e an observable)
In Observable subscription, i assign my global array with the response from the server.
Code is working correctly, I am getting responses as they should be but i cant assign the variable to the global one.
Here is my code:
import {Component, OnInit} from '#angular/core';
import {PlaylistService} from '../api/services'
#Component({
selector: 'app-playlists',
templateUrl: './playlists.component.html',
styleUrls: ['./playlists.component.css']
})
export class PlaylistsComponent implements OnInit {
public playlists: any[] = [];
constructor(private playlistService: PlaylistService) {
}
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
});
}
}
The listIds function is as follows:
listIds() {
return this.api.listing('playlist').then((body) => {
let hits = body.hits.hits;
return _.keys(_.groupBy(hits, '_id'));
});
}
and here is my api.listing function (elastic search client)
listing(type: string) {
let es = this.prepareES();
return es.search({
index: 'test',
_source: ["_id"],
type: type
});
}
The return type of es.search is
search(params: SearchParams): PromiseLike>;
Any ideas why i am not being able to assign value to global variable?
It looks like the promise returned by this.playlistservice.listIds() doesn't run inside Angulars zone. This is why Angular2 doesn't run change detection and doesn't recognize the change.
You can invoke change detection explicitly after the change:
constructor(private playlistService: PlaylistService, private cdRef:ChangeDetectorRef) {
...
ngOnInit() {
let that = this;
this.playlistService.listIds().then((val) => { // <-- promise resolution
return this.playlistService.getByIds(val).toPromise(); // <-- http get call which i then convert to promise for simplicity
}).then((res) => { // <-- resolution of the http get call
console.log(this.playlists); <-- in this log, i get my desired results
// here is my problem, this assignment doesn't happens
this.playlists = res.data;
this.cdRef.detectChanges();
});
}
Can you try passing
this.playlistService.listIds()
call inside your
return this.playlistService.getByIds(val)
replace val with first service call and see if your view gets updated. Just for testing purpose like
return this.playlistService.getByIds(this.playlistService.listIds())
.then((results)=>{/*rest of logic here*/});