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.
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.
I try to dynamic create variables in class to store values and use it in ngModel and other placese.
I know, that I can assign value to variables in ngOnInit() like this
export class Component implements OnInit{
name: string;
ngOnInit(){
this.name = 'John Doe';
}
}
But I have some problem - I get my fields from server API and don't know what and how many items I get. I can only parsing server response and assign value to new variables after get it.
I can't do it like this (TS2540: Cannot assign to 'name' because it is a constant or a read-only property.)
export class Component implements OnInit{
ngOnInit(){
name = 'John Doe';
}
}
How can I assign new fields to my class in ngOnInit() or maybe in other place? (I think I can do it in constructor, but documentation say i shouldn't use it with Observable call to API and other difficult things)
You can use something like this to accomplish that:
ngOnInit() {
this['prop'] = 'value';
}
Here is a link to a working example: https://stackblitz.com/edit/dynamic-class-props
You can add an indexer to the class to be able to use any property name and not get a compiler error:
export class Component {
[name: string]: any;
ngOnInit(){
this["name"] = 'John Doe';
this.nameaa = "dd"
}
}
You should take care though, this means you can misspell property names and the compiler will not issue any error.
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]);
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