Angular 4 Component variable of type Item initialized in subscribe response does not have methods defined in the Item class - javascript

I'm working on an Angular project for a new internal application. While I have some experience in AngularJS I am a near complete novice when it comes to Angular 2+. An issue I've run into that I can't make sense of involves the setting of some component class variables inside of a subscribe callback in the ngOnInit block. I will be a bit abstract with my details but I hope it provides enough context to convey the issue.
I have an item type defined as such:
export class Item {
name: String;
checked: boolean = false;
// this is a simplification of the actual type and method, I'm aware this seems unnecessary
toggle(set: boolean = false): void {
this.checked = !this.checked || set;
}
}
In my component I a declared array of items using a subscribe callback to a restful service.
import ...
#Component({
templateUrl: './item.component.html',
styleUrls: [ './item.component.less',
providers: [ ItemService ]
})
export class ItemComponent implements OnInit {
items: Item[];
allChecked: boolean = false;
constructor(private itemService: ItemService) { }
ngOnInit() {
this.itemService.getItems().subscribe(
(response) => {
this.items = items;
}
);
}
checkAll(): void {
this.allChecked = !this.allChecked;
for(let item of this.items) {
item.toggle(allChecked);
}
}
In the html (as well as in the "checkAll()" function of the component above) I reference the method defined on the item type like so:
<input ... (change)="checkAll()" />
...
<tr *ngFor="let item of items; index as i">
<td>
<input ... (change)="item[i].toggle()" />
</td>
</tr>
...
I previously had the array of items being mocked inside the component ngOnInit block. When this was the case these method calls executed fine. It is only after replacing the mock data with the actual restful service call that I am having issues reaching the toggle() method. I receive an error stating that the method I'm attempting to call on my item type is undefined.
I understand that scoping is a bit wonky in javascript/typescript but since I'm using the syntactic sugar arrow format for the subscribe callback the "this" should reference the correct context, yes? Additionally, since I was at a loss I also tried using the that = this; outside the service call and then referencing "that" inside the callback to make sure I was setting the items property on the outer class context. Either way, it seems like as soon as I exit the context of the subscribe callback any notion of items comes back as undefined. Why is this? Am I missing some key point of Angular 2+?
Edit: I realize I was a bit too deep in the hole and was mistaking my IDEs lack of proper mapping (showing component data as undefined when debugging even if it wasn't) when in reality the data was there as a json without any method definitions. I've added more code to this post and altered the text/title to reflect this.

You're getting a JSON response from your service call, not instances of your
Item class. As plain data objects, they do not have ItemImpl's method definitions. So you need to map each object to a class instance.
interface Item {
id: string;
name: string;
}
class ItemImpl implements Item {
id: string;
name: string;
constructor(item: Item) {
this.id = item.id;
this.name = item.name;
}
someMethod() {
// ...
}
}
Then map the result from the HTTP call:
return this.http.get<Item[]>(this.myItemsUrl)
.pipe(
map(items => items.map(i => new ItemImpl(i)))
);
In my opinion, it would be better style to put the logic (someMethod()) into a service instead of the Item data class.

Related

Accessing Observable object in Angular

I am currently learning Angular through some tutorial and I came across a problem. I don't quite understand why is my console giving me error
ERROR in src/app/employee/employee.component.ts:17:24 - error TS2322: Type 'IEmployee' is not assignable to type 'any[]'.17 .subscribe(data => this.employees = data);
This is my code that I followed from tutorial:
export class EmployeeDetailComponent implements OnInit {
public employees = [];
constructor(private _employeeService : EmployeeService) { }
ngOnInit(): void {
this._employeeService.getEmployees()
.subscribe(data => this.employees = data);
console.log(this.employees);
}
}
Http request should return observable with type of IEmployees interface that i made, but it doesn't work unless I do this public employees:any = [].This did solve my problem, but i don't understand why, since on tutorial it works without it. My second issue is when i try to Console log my employees array after i get my data with http request, to see what type of data it is, but it seems to be empty(i tried this.employees[0].name also). Why cannot I access that array within ngOnInit() but in my html file i can do it with ngFor and access object properties.
This is IEmployees interface
export interface IEmployee {
id: number,
name: string,
age : number
}
When declaring your employees array,
do this
public employees: IEmployee = [] ;
instead of this
public employees = [];
This is because your service is retuning the data of type IEmployee while the variable you're is using is not of that type
You seem to be unsure whether you're going to receive a single Employee object or an array of Employees.
You have declared the getEmployees method in your Employee service to return an Observable<Employee>.
If your API returns an array of Employees, change the getEmployees method such that it returns an Observable<Employee[]>.
If your API returns a single Employee object, change the arrow function to the following:
data => this.employees = [data].
To additionally log the output to the console, change the arrow function to:
data => { console.log(data); // set this.employees;}
The this.employees[0] is undefined in ngOnInit because the asynchronous request finishes later, than the ngOnInit finishes. So the value of this.employees will be assigned after the ngOnInit is done. The ngFor is also able to display the values of the property after the asynchronous response arrived.

Why isn't my component object getting updated itself when anything in my globally shared service updates? Angular

I have this service:
export class RecipeService{
selectedRecipe: Recipe = 'xyz';
}
I have this component using this service:
export class RecipesComponent implements OnInit {
selectedRecipe: Recipe;
constructor(private recipeService: RecipeService) { }
ngOnInit(): void {
this.selectedRecipe = this.recipeService.selectedRecipe;
}
}
The service is defined in app.module.ts for injection, which means all components get the same instance.
My question is, whenever I update the selectedRecipe variable in one of my components, it doesn't get updated back in other components although it is referenced and hence I expect a change immediately.
What am I doing wrong?
It doesn't get updated because the new value is not "sent" to the already initiated angular components.
Instead you should use observables.
for example:
/* service */
private recipe = "xyz";
public recipeSubject: BehaviorSubject<string> = new BehaviorSubject(this.recipe);
// when changing the recipe
recipeSubject.next(this.recipe);
/* component */
this.service.recipeSubject.subscribe(res => this.recipe = res);
I googled and found out in one of the posts that its because of my object. The object (Recipe) in my service contains a primitive type, i.e string. If your object contains a primitive type, it isn't passed as a reference, hence a change in service object won't be reflected in component because they are now different.
Although I must clear that in case of array it worked perfectly fine even when my array contained objects which had primitive types. Changes were still reflected.

Why is this change emitted using Rxjs

I'm a little bit confused as why the following snipped works as expected.
The idea of this service is to have a list of strings where if you add a string, it is removed 5 seconds later. Rxjs is used here:
#Injectable()
export class ErrorService {
private errors: Array<string> = [];
private emitErrorsChanged = new Subject<any>();
public emitErrorsChanged$ = this.emitErrorsChanged.asObservable();
constructor() {
this.emitErrorsChanged$.delay(5000).subscribe(
() => {
if (this.errors.length > 0) {
this.errors.shift();
}
}
);
}
public emitErrorChange(error: string) {
this.errors.push(`${error}`);
this.emitErrorsChanged.next(this.errors);
}
}
An error component is subscribed to this service errorService.emitErrorsChanged$.subscribe(...) and shows the strings in a list. Other components/services add strings by this.errorService.emitErrorChange(error.message).
My question is: why are the removed errors (5s) emitted to the error component? The errors are just removed from the list this.errors.shift(); but the change is not emitted by this.emitErrorsChanged.next(this.errors);
The behavior occurs because you are passing reference to your object (list in this case). The changes made by this.errors.shift(); are not emitted, but I guess you can see current state of this.errors thanks to Angular's change detection. I have prepared a demo (click) so you can see that the object reference is passed in your case - what means that the list in subscription is the exactly same array list. To prevent it you can pass a copy of your list, e.g. using spread operator like in this example:
this.emitErrorsChanged.next([...this.errors]);

Angular 2: How to detect changes in an array? (#input property)

I have a parent component that retrieves an array of objects using an ajax request.
This component has two children components: One of them shows the objects in a tree structure and the other one renders its content in a table format. The parent passes the array to their children through an #input property and they display the content properly. Everything as expected.
The problem occurs when you change some field within the objects: the child components are not notified of those changes. Changes are only triggered if you manually reassign the array to its variable.
I'm used to working with Knockout JS and I need to get an effect similar to that of observableArrays.
I've read something about DoCheck but I'm not sure how it works.
OnChanges Lifecycle Hook will trigger only when input property's instance changes.
If you want to check whether an element inside the input array has been added, moved or removed, you can use IterableDiffers inside the DoCheck Lifecycle Hook as follows:
constructor(private iterableDiffers: IterableDiffers) {
this.iterableDiffer = iterableDiffers.find([]).create(null);
}
ngDoCheck() {
let changes = this.iterableDiffer.diff(this.inputArray);
if (changes) {
console.log('Changes detected!');
}
}
If you need to detect changes in objects inside an array, you will need to iterate through all elements, and apply KeyValueDiffers for each element. (You can do this in parallel with previous check).
Visit this post for more information: Detect changes in objects inside array in Angular2
You can always create a new reference to the array by merging it with an empty array:
this.yourArray = [{...}, {...}, {...}];
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [].concat(this.yourArray);
The code above will change the array reference and it will trigger the OnChanges mechanism in children components.
Read following article, don't miss mutable vs immutable objects.
Key issue is that you mutate array elements, while array reference stays the same. And Angular2 change detection checks only array reference to detect changes. After you understand concept of immutable objects you would understand why you have an issue and how to solve it.
I use redux store in one of my projects to avoid this kind of issues.
https://blog.thoughtram.io/angular/2016/02/22/angular-2-change-detection-explained.html
You can use IterableDiffers
It's used by *ngFor
constructor(private _differs: IterableDiffers) {}
ngOnChanges(changes: SimpleChanges): void {
if (!this._differ && value) {
this._differ = this._differs.find(value).create(this.ngForTrackBy);
}
}
ngDoCheck(): void {
if (this._differ) {
const changes = this._differ.diff(this.ngForOf);
if (changes) this._applyChanges(changes);
}
}
It's work for me:
#Component({
selector: 'my-component',
templateUrl: './my-component.component.html',
styleUrls: ['./my-component.component.scss']
})
export class MyComponent implements DoCheck {
#Input() changeArray: MyClassArray[]= [];
private differ: IterableDiffers;
constructor(private differs: IterableDiffers) {
this.differ = differs;
}
ngDoCheck() {
const changes = this.differ.find(this.insertedTasks);
if (changes) {
this.myMethodAfterChange();
}
}
This already appears answered. However for future problem seekers, I wanted to add something missed when I was researching and debugging a change detection problem I had. Now, my issue was a little isolated, and admittedly a stupid mistake on my end, but nonetheless relevant.
When you are updating the values in the Array or Object in reference, ensure that you are in the correct scope. I set myself into a trap by using setInterval(myService.function, 1000), where myService.function() would update the values of a public array, I used outside the service. This never actually updated the array, as the binding was off, and the correct usage should have been setInterval(myService.function.bind(this), 1000). I wasted my time trying change detection hacks, when it was a silly/simple blunder. Eliminate scope as a culprit before trying change detection solutions; it might save you some time.
Instead of triggering change detection via concat method, it might be more elegant to use ES6 destructuring operator:
this.yourArray[0].yourModifiedField = "whatever";
this.yourArray = [...this.yourArray];
You can use an impure pipe if you are directly using the array in your components template. (This example is for simple arrays that don't need deep checking)
#Pipe({
name: 'arrayChangeDetector',
pure: false
})
export class ArrayChangeDetectorPipe implements PipeTransform {
private differ: IterableDiffer<any>;
constructor(iDiff: IterableDiffers) {
this.differ = iDiff.find([]).create();
}
transform(value: any[]): any[] {
if (this.differ.diff(value)) {
return [...value];
}
return value;
}
}
<cmp [items]="arrayInput | arrayChangeDetector"></cmp>
For those time travelers among us still hitting array problems here is a reproduction of the issue along with several possible solutions.
https://stackblitz.com/edit/array-value-changes-not-detected-ang-8
Solutions include:
NgDoCheck
Using a Pipe
Using Immutable JS NPM github

Angular2 Child component destroyed unexpectedly in ngFor

So i have this Component of a from with an #Output event that trigger on submit, as follows:
#Component({
selector: 'some-component',
templateUrl: './SomeComponent.html'
})
export class SomeComponent{
#Input() data: any;
#Output() onSubmit: EventEmitter<void> = new EventEmitter<void>();
constructor(private someService: SomeService) {}
submitForm(): void{
this.someService.updateBackend(this.data, ()=>{
this.onSubmit.emit();
});
}
}
I'm using an ngFor to create multiple elements of this Component :
<template let-data ngFor [ngForOf]="dataCollection">
<some-component [data]="data" (onSubmit)="doSomthing()"></some-component>
</template>
The last missing part is the service used on submitting:
#Injectable()
export class SomeService{
constructor() {}
updateBackend(data: any, callback: () => void): void{
/*
* updating the backend
*/.then((result) => {
const { errors, data } = result;
if (data) {
callback();
}
})
}
}
At the beginning of the submitForm() function, the this.onSubmit.observers is an Array containing one observer, like it should be.
As soon as it reaches the callback method, where the this.onSubmit.emit() is invoked, the this.onSubmit.observers is an Array containing ZERO observers.
I'm experiencing two very weird behaviors:
If i remove the actual calling to update the backend in SomeService.updateBackend it works perfectly fine, and the observers still is an Array containing one observer!
If i keep the actual calling to the backend BUT not using ngFor and displaying only one <some-element> it also works perfectly fine, keeping one observer in the this.onSubmit.observers within the callback scope!
Any idea what am i doing wrong?
Thanks in advance!
Update:
Thanks to #StevenLuke's comment about logging the ngOnDestroy of SomeComponent I found out that it is being destroyed before the emit.
Actually, the first thing it is doing when the SomeService.updateBackend finishes is Destroying all the instances of this component and recreate them!
This is what makes the observers change! Why would that happen?
If you provide a trackBy function in your *ngFor to identify items in your dataCollection, it will not destroy and init. Your template would be:
<some-component *ngFor="let data of dataCollection;trackBy:trackByFunction"
[data]="data" (onSubmit)="doSomthing()"></some-component>
And the trackByFunction would look like:
trackByFunction(index, item) {
return item ? item.id : undefined;
}
So even though an item in your dataCollection is a fresh object, if its id matches an id in the previous collection, *ngFor will update [data] but not destroy and init the component.
Thanks to #GünterZöchbauer comments I found out the case was that the data the ngFor is bound to was being replaced by a new instance as I updated the backend, hence, it rerendered it's child Components causing reinitializing (destory + init) of them, which made the instance of the Component to be overwritten.
In order to solve this issue i had to place the dataCollection in a separate service, getting it for the parent component ngOnInit, saving it from causing a rerender of the ngFor, and fetch its data again only after the execution of the Child Components ended
Hope it'll be helpful to somebody!

Categories

Resources