Why does the child component mutation my variable in angular - javascript

I'm trying to find a way to share a variable between my components in Angular. My variable that I pass to my child components are interchangeable between their child components. I used service for this, but still could not solve the problem. I am sharing a demo by simplifying my operations using BehaviorSubject.
demo : https://stackblitz.com/edit/angular-ivy-6usdww?file=src%2Fapp%2Fhello.component.ts
My problem is:
i am sending variables from my app.component.ts component to hello and hello-a components. i am mutating variable in hello component. However, just before that I point the first version to a service. however, the changed status is forwarded.
hello.component
export class HelloComponent {
#Input() colDef: any[];
col: any[];
constructor(private columnDefService: ColumnDefService) {}
ngOnInit() {
let colDefFirst = Object.assign([], this.colDef);
this.columnDefService.setFirstColDef(colDefFirst);
this.setColumnDef(this.colDef);
}
setColumnDef(colDef: any[]): void {
let rC: any = Object.assign([], colDef);
rC[0].hide = true;
this.columnDefService.setColDef([].concat(rC));
this.columnDefService.colDef$.subscribe((colDef: any) => {
this.col = colDef;
});
}
}
hello-a.component
export class HelloAComponent {
#Input() colDef: any[];
firstColDef: any[] = [];
constructor(private ColumnDefService: ColumnDefService) {}
ngOnInit() {
this.ColumnDefService.firstColDef$.subscribe((firstColDef: any) => {
console.log('firstColDef', firstColDef);
this.firstColDef = firstColDef;
});
}
}
coldef.service
#Injectable({
providedIn: 'root'
})
export class ColumnDefService {
private colDefSource = new BehaviorSubject([]);
private firstColDefSource = new BehaviorSubject([]);
colDef$ = this.colDefSource.asObservable();
firstColDef$ = this.firstColDefSource.asObservable();
setColDef(colDef: any) {
this.colDefSource.next(colDef);
}
setFirstColDef(colDef: any) {
this.firstColDefSource.next(colDef);
}
}
this is what i want to do:
storing my variable in a component for future use. i want to continuously change my variable on demand in my other component.
thanks for your advice

Related

setInterval() does not set data without refreshing the page in angular component

here is my angular component -
export class UserListComponent implements OnInit, OnDestroy {
private _subscriptions: Subscription;
private _users: User[] = [];
private _clickableUser: boolean = true;
constructor(
private route: ActivatedRoute,
) { }
ngOnInit() {
const source = interval(1000);
const users = this.route.snapshot.data['users'] as IUserInterface[];
this._subscriptions = source.subscribe(() => this._users = users.map(user => new User(user)))
}
ngOnDestroy() {
this._subscriptions.unsubscribe();
}
i get the data, but the interval does not set it without refreshing.
ngOnInit() is only called once, when a component is first created. This means that your active route is being read only once, which also means that your users array is always the same one.
I'm guessing you are trying to update your component each time the route data gets updated. For that, you need to subscribe to this.route.data (notice I didn't include snapshot). It will look something like this:
this._subscriptions = this.route.data.subscribe(data => this._users = data['users'].map(user => new User(user));

httpClient.get is undefined when using dynamic function/observable references

So I asked a question a few days ago and got some headway on a solution, however now I'm stuck at another wall I'm unsure how to get over.
I have two parent components, a shared view/component with an extended base component, and a service all hooked together. The objective is to use the two parent components to drive what data is shown within the shared component. The two parent components use references to service methods passed into the shared component to get the data.
I've reached an issue where my http.get is always undefined no matter what I try. I've instantiated it like I do in my other services but I've had no luck. I suspect this is caused by how i pass in my service references. Code below:
Parent Component Code:
// PARENT COMPONENT
myData$: Observable<myType>;
searchMethod: Function;
constructor(private myService){
this.myData$ = this.myService.myData$;
this.searchMethod = this.myService.searchData;
}
// PARENT COMPONENT HTML
<app-shared-component
[myData$] = "myData$"
[searchMethod]="searchMethod">
</app-shared-component>
Shared Component Code:
export class MySharedComponent extends BaseComponent<MyType> implements OnInit {
#Input() myData$: Observable<myType>;
#Input() searchMethod: Function;
constructor() { super(); }
ngOnInit(): void {
this.data$ = this.myData$;
}
search(): void {
this.searchMethod().subscribe(//do something);
}
Base Component Code:
#Input data$: Observable<T>;
ngOnInit(): void {
this.data$.subscribe((response: T) => //do something);
super.ngOnInit();
}
Service Code:
private myDataSubject = new BehaviorSubject<MyType>(new MyType());
get myData$(): Observable<MyType> {
return this.myDataSubject.asObservable();
}
constructor(private http: HttpClient) { }
searchData(): Observable<void> {
return new Observable<void>(observer => {
this.http.get<MyType>(
'http://myuri'
).subscribe(
response => {
// do something
},
() => observer.error(),
() => observer.complete()
);
});
}
It looks like you're losing the context of your service when you set this.searchMethod = this.myService.searchData in your parent component. It should work if you change searchData() { to an arrow function: searchData = (): Observable<void> => {.

Updating a list on button click Angular

I hope i post this in a concise way. The main goal here is to update a list after a button click. I have a list of hobbies. When I click the (hobbies.component) in the list, the hobby-view component should update with a list of that hobby type. I can successfully make a call to the server. But, I'm not seeing the component hobby-view update.
Hobbies.component
<ul class="hobbyList">
<li *ngFor="let hobby of hobbies" (click)="onSelect(hobby)">
<span class="badge">{{hobby.HobbyName}}</span>
</li>
</ul>
getHobbies() creates the ul of hobbies to click. onSelect then uses a service to update a string inside the hobby-view component (which works). Then tries to update a list inside the hobby-view component.
export class HobbiesComponent implements OnInit {
selectedHobby: hobbyObj;
hobbies: hobbyObj[];
constructor(
private hobbyService: HobbyService,
private masterhobbydata: MasterHobbydata
) { }
ngOnInit() {
this.getHobbies();
}
getHobbies(): void {
this.hobbyService.getHobbies()
.subscribe(hobbies => this.hobbies = hobbies);
}
onSelect(hobby: hobbyObj): void {
this.selectedHobby = hobby;
this.masterhobbydata.changeMessage(this.selectedHobby.HobbyName);
this.masterhobbydata.getHobbiesById();
}
}
Hobby-view.component
<ul>
<li *ngFor="let hobby of hobbiesRooms">
<span class="badge">{{hobby.hname}}</span>
</li>
</ul>
<p>
Message: {{message}}
</p>
export class HobbyViewComponent implements OnInit {
hobbyRoomObj = HobbyRoomObj;
hobbiesRooms: HobbyRoomObj[];
message:string;
constructor(private data: MasterHobbydata) { }
ngOnInit() {
this.data.currentMessage.subscribe(message => this.message = message);
}
getHobbiesById(): void {
console.log("hit");
this.data.getHobbiesById()
.subscribe(hobbiesRooms => this.hobbiesRooms = hobbiesRooms);
}
}
Inside ngOnIt is where the simple string gets updated successfully from the onClick method inside hobbies.component.
Here is the service component MasterHobbyData
#Injectable({
providedIn: 'root'
})
export class MasterHobbydata {
private messageSource = new BehaviorSubject<string>("1");
currentMessage = this.messageSource.asObservable();
private hobbyListByIdUrl="http://local/?Hobby=";
constructor(private http: HttpClient){}
changeMessage(message: string) {
this.hobbyListByIdUrl += message;
this.messageSource.next(message);
}
//Returns a full list of current hobbies hosted
getHobbiesById(): Observable<HobbyRoomObj[]>{
return this.http.get<HobbyRoomObj[]>(this.hobbyListByIdUrl)
.pipe(
catchError(this.handleError('getHobbiesById', []))
);
}
}
I've been trying to manipulate this piece of code for the list of hobbies. I believe this where my problem lies.
private messageSource = new BehaviorSubject<string>("1");
currentMessage = this.messageSource.asObservable();
I think I have to make variable of BehaviorSubject
But I can't seem to find the correct syntax to this.
Am I correct in my approach? If i am, how should I implement the syntax? Any help would be much appreciated. I am new to Angular.
LIST
Karate
Apples
Cake
When I click on Apples, a request to my php script will return a list of apples. And so on and so forth.
my suggestion is to use Subject.
i.e messageSource = new Subject<string>();
and subscribe to messageSource
currentMessage = this.messageSource.asObservable(); not required remove it.
Change the currentMessage to messageSource
ngOnInit() {
this.data.messageSource .subscribe(message => this.message = message);
}
Whenever the messageSource changed it will automatically subscribed in you component.
#Injectable({
providedIn: 'root'
})
export class MasterHobbydata {
private messageSource = new BehaviorSubject<string>("1");
currentMessage = this.messageSource.asObservable();
private hobbyListByIdUrl="http://local/?Hobby=";
constructor(private http: HttpClient){}
changeMessage(message: string) {
this.messageSource.next(message);
}
//Returns a full list of current hobbies hosted
getHobbiesById(message): Observable<HobbyRoomObj[]>{
const url = this.hobbyListByIdUrl + message;
return this.http.get<HobbyRoomObj[]>(url)
.pipe(map(( response) =>{
return response;
}
)).pipe(catchError(this.handleError('getHobbiesById', []))
);
}
}
component file changes
export class HobbyViewComponent implements OnInit {
hobbyRoomObj = HobbyRoomObj;
hobbiesRooms: HobbyRoomObj[];
message:string;
constructor(private data: MasterHobbydata) { }
ngOnInit() {
this.data.currentMessage.subscribe(message => {
this.message = message
this.getHobbiesById(message);
});
}
getHobbiesById(message): void {
console.log("hit");
this.data.getHobbiesById(message)
.subscribe(hobbiesRooms => this.hobbiesRooms = hobbiesRooms);
}
}

Angular 4: How to watch an object for changes?

ETA: I know that there are various ways to watch my form for changes. That is not what I am trying to do. As the title says, I am asking how to watch for changes to an object. The app shown below is for illustration purposes only. Please answer the question that I have asked. Thanks!
I have this simple app:
import { Component, OnInit } from '#angular/core';
export class Customer {
firstName: string;
favoriteColor: string;
}
#Component({
selector: 'my-app',
template: `
<div *ngIf="customer">
<input type="text" [(ngModel)]="customer.firstName">
<input type="text" [(ngModel)]="customer.favoriteColor">
</div>
`
})
export class AppComponent implements OnInit {
private customer: Customer;
ngOnInit(): void {
this.customer = new Customer();
// TODO: how can I register a callback that will run whenever
// any property of this.customer has been changed?
}
}
Note the TODO. I need to register a callback that will run whenever any property of this.customer has been changed.
I cannot use ngChange on the inputs. I need to subscribe directly to changes on the model. The reasons pertain to my use case, and aren't worth going into here. Just trust me that this isn't an option.
Is this possible? I've done a lot of Googling, but I've come up dry.
Angular usually uses injected into constructor KeyValueDiffers class.
For your case it could look like:
import { KeyValueChanges, KeyValueDiffer, KeyValueDiffers } from '#angular/core';
export class Customer {
firstName: string;
favoriteColor: string;
}
#Component({
selector: 'my-app',
templateUrl: `./app.component.html`
})
export class AppComponent {
private customerDiffer: KeyValueDiffer<string, any>;
private customer: Customer;
constructor(private differs: KeyValueDiffers) {}
ngOnInit(): void {
this.customer = new Customer();
this.customerDiffer = this.differs.find(this.customer).create();
}
customerChanged(changes: KeyValueChanges<string, any>) {
console.log('changes');
/* If you want to see details then use
changes.forEachRemovedItem((record) => ...);
changes.forEachAddedItem((record) => ...);
changes.forEachChangedItem((record) => ...);
*/
}
ngDoCheck(): void {
const changes = this.customerDiffer.diff(this.customer);
if (changes) {
this.customerChanged(changes);
}
}
}
Stackblitz Example
One more option is using setter on properties that you want to check.
See also
http://blog.mgechev.com/2017/11/14/angular-iterablediffer-keyvaluediffer-custom-differ-track-by-fn-performance/
I need to subscribe directly to changes on the model.
Then you need to listen to model changes with ngModelChange
Template:
<input type="text" (ngModelChange)="doSomething($event)" [ngModel]="customer.firstName">
Class:
doSomething(event) {
console.log(event); // logs model value
}
DEMO
You can't watch changes in an object. Its not angular 1 there are no watchers here. Another solution will be via observables.
use form
<form #f="ngForm">
<input type="text" name="firstName" [(ngModel)]="customer.firstName">
<input type="text" name="favoriteColor" [(ngModel)]="customer.favoriteColor">
</form>
in code
#ViewChild('f') f;
ngAfterViewInit() {
this.f.form.valueChanges.subscribe((change) => {
console.log(change)
})
}
You could use custom setters to trigger your callback:
class Customer {
private _firstName: string
get firstName(): string {
return this._firstName
}
set firstName(firstName: string) {
this.valueChanged(this._firstName, firstName)
this._firstName = firstName
}
private _lastName: string
get lastName(): string {
return this._lastName
}
set lastName(lastName: string) {
this.valueChanged(this._lastName, lastName)
this._lastName = lastName
}
valueChanged: (oldVal, newVal) => void
constructor (valueChanged?: (oldVal, newVal) => void) {
// return an empty function if no callback was provided in case you don't need
// one or want to assign it later
this.valueChanged = valueChanged || (() => {})
}
}
Then just assign the callback when you create the object:
this.customer = new Customer((oldVal, newVal) => console.log(oldVal, newVal))
// or
this.customer = new Customer()
this.customer.valueChanged = (oldVal, newVal) => console.log(oldVal, newVal)
visit https://github.com/cartant/rxjs-observe. it bases on rxjs and proxy.
import { observe } from "rxjs-observe";
const instance = { name: "Alice" };
const { observables, proxy } = observe(instance);
observables.name.subscribe(value => console.log(name));
proxy.name = "Bob";
We are tasked with converting an Angular 1.x app to Angular 9. It's an application with ESRI maps, so we have some neat tools that the ESRI framework brought to the table. ESRI has watchUtils that do a whole lot more than just watch for changes.
But I missed Angular 1's simple $watch. Besides, we create Entities and Models in our application, and we may need to observe these from time to time.
I created an abstract class called MappedPropertyClass. It uses a Map<string, any> to map class properties, which allows me to easily implement toJSON and other utility functions.
The other Map this class has is _propertyChangeMap: Map<string, EventEmitter<{newvalue,oldvalue}>;
We also have a function called... $watch, which takes a string and a callback function.
This class can be extended by Entities as well as components or services
I'm happy to share, the caveat is your properties must look like this:
public get foo(): string {
return this._get("foo");
}
public set foo(value:string) {
this._set("foo", value);
}
--------------------------------------------------------------------
import { EventEmitter } from '#angular/core';
export abstract class MappedPropertyClass {
private _properties: Map<string, any>;
private _propertyChangeMap: Map<string, EventEmitter<{ newvalue, oldvalue }>>;
protected _set(propertyName: string, propertyValue: any) {
let oldValue = this._get(propertyName);
this._properties.set(propertyName, propertyValue);
this.getPropertyChangeEmitter(propertyName).emit({ newvalue:
propertyValue, oldvalue: oldValue });
}
protected _get(propertyName: string): any {
if (!this._properties.has(propertyName)) {
this._properties.set(propertyName, undefined);
}
return this._properties.get(propertyName);
}
protected get properties(): Map<string, any> {
var props = new Map<string, any>();
for (let key of this._properties.keys()) {
props.set(key, this._properties.get(key));
}
return props;
}
protected constructor() {
this._properties = new Map<string, any>();
this._propertyChangeMap = new Map<string, EventEmitter<{ newvalue: any,
oldvalue: any }>>();
}
private getPropertyChangeEmitter(propertyName: string): EventEmitter<{
newvalue, oldvalue }> {
if (!this._propertyChangeMap.has(propertyName)) {
this._propertyChangeMap.set(propertyName, new EventEmitter<{ newvalue,
oldvalue }>());
}
return this._propertyChangeMap.get(propertyName);
}
public $watch(propertyName: string, callback: (newvalue, oldvalue) => void):
any {
return this.getPropertyChangeEmitter(propertyName).subscribe((results) =>
{
callback(results.newvalue, results.oldvalue);
});
}
}

Angular: In which lifecycle hook is input data available to the Component

I have a component which receives an array of image objects as Input data.
export class ImageGalleryComponent {
#Input() images: Image[];
selectedImage: Image;
}
I would like when the component loads the selectedImage value be set to the first object of the images array. I have tried to do this in the OnInit lifecycle hook like this:
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnInit() {
this.selectedImage = this.images[0];
}
}
this gives me an error Cannot read property '0' of undefined which means the images value isn't set on this stage. I have also tried the OnChanges hook but I'm stuck because i can't get information on how to observe changes of an array. How can I achieve the expected result?
The parent component looks like this:
#Component({
selector: 'profile-detail',
templateUrl: '...',
styleUrls: [...],
directives: [ImageGalleryComponent]
})
export class ProfileDetailComponent implements OnInit {
profile: Profile;
errorMessage: string;
images: Image[];
constructor(private profileService: ProfileService, private routeParams: RouteParams){}
ngOnInit() {
this.getProfile();
}
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
}, error => this.errorMessage = <any>error
);
}
}
The parent component's template has this
...
<image-gallery [images]="images"></image-gallery>
...
Input properties are populated before ngOnInit() is called. However, this assumes the parent property that feeds the input property is already populated when the child component is created.
In your scenario, this is not the case – the images data is being populated asynchronously from a service (hence an http request). Therefore, the input property will not be populated when ngOnInit() is called.
To solve your problem, when the data is returned from the server, assign a new array to the parent property. Implement ngOnChanges() in the child. ngOnChanges() will be called when Angular change detection propagates the new array value down to the child.
You can also add a setter for your images which will be called whenever the value changes and you can set your default selected image in the setter itself:
export class ImageGalleryComponent {
private _images: Image[];
#Input()
set images(value: Image[]) {
if (value) { //null check
this._images = value;
this.selectedImage = value[0]; //setting default selected image
}
}
get images(): Image[] {
return this._images;
}
selectedImage: Image;
}
You can resolve it by simply changing few things.
export class ImageGalleryComponent implements OnInit {
#Input() images: Image[];
selectedImage: Image;
ngOnChanges() {
if(this.images) {
this.selectedImage = this.images[0];
}
}
}
And as another one solution, you can simply *ngIf all template content until you get what you need from network:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
...
And switch flag value in your fetching method:
getProfile() {
let profileId = this.routeParams.get('id');
this.profileService.getProfile(profileId).subscribe(
profile => {
this.profile = profile;
this.images = profile.images;
for (var album of profile.albums) {
this.images = this.images.concat(album.images);
}
this.imagesLoaded = true; /* <--- HERE*/
}, error => this.errorMessage = <any>error
);
}
In this way you will renderout child component only when parent will have all what child needs in static content. It's even more useful when you have some loaders/spinners that represent data fetching state:
...
<image-gallery *ngIf="imagesLoaded" [images]="images"></image-gallery>
<loader-spinner-whatever *ngIf="!imagesLoaded" [images]="images"></loader-spinner-whatever>
...
But short answer to your questions:
When inputs are available?
In OnInit hook
Why are not available to your child component?
They are, but at this particular point in time they were not loaded
What can I do with this?
Patiently wait to render child component utul you get data in asynchronous manner OR learn child component to deal with undefined input state

Categories

Resources