Angular 4: How to watch an object for changes? - javascript

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);
});
}
}

Related

Why does the child component mutation my variable in angular

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

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);
}
}

Proper way to propagate computed value on Angular2

I want to use computed value by #Input properties
But propagating initial value is not work well.
https://plnkr.co/edit/1MMpOYOKIouwnNc3uIuy
I create App (root component with template driven form) and NumComponent (child component that just saving typed value) component.
When i pass attribute to NumComponent like [useThree]="true" then i want set default value '3' to NumComponent
But i can't find way without using setTimeout
Is there way to propagate initial value without setTimeout?
Edited at 5/5
App component
#Component({
selector: 'my-app',
template: `
<div>
<form novalidate #form="ngForm">
<app-num name="num" ngModel [useThree]="true"></app-num>
</form>
<pre>{{form.value | json}}</pre>
</div>
`
})
export class App {}
NumComponent
export const NumValueAccessor = {
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumComponent),
multi: true
};
#Component({
selector: 'app-num',
template: `<input [(ngModel)]="num" type="text" (ngModelChange)="updateValue()" />`,
providers: [NumValueAccessor]
})
export class NumComponent implements ControlValueAccessor {
num = 0;
// I want set literal number 3 to `num` property
// when `useThree` is true.
#Input() useThree = false;
onChange = (_: any) => {};
updateValue(num = this.num) {
this.onChange(String(num));
}
writeValue(value: string): void {
if (this.useThree) {
/**********
* ISSUE
**********/
// this code is not work. after code ran, `NumComponent` has
// value 3 but AppComponent's internal FormComponent value
// is '' (empty string)
// this.num = 3;
// this.updateValue(3);
// ran code with `setTimeout` solve this problem. but
// I don't want using setTimeout for this feature.
// setTimeout(() => {
// this.num = 3;
// this.updateValue(3);
// }, 0);
// Is there any way to propagate computed initial value?
this.num = 3;
this.updateValue(3);
/**********
* ISSUE
**********/
this.useThree = false;
return;
}
this.num = Number(value);
}
registerOnChange(fn: any): void {
this.onChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState(isDisabled: boolean): void {}
}
It seems that parent component don't realize propagated value at initialization lifecycle time.
I am not sure I have understood the problem fully, but one possible solution to what you are looking for could be
private _useThree = false;
#Input() set useThree(value: boolean) {
this._useThree = value;
if (this._useThree) {
this.num = 3;
}
}
This way, any time you want to set the value of the input useThree property from the parent component, you actually execute the code of the setter method defined above.

Property change subscription with Aurelia

I have a property on my viewmodel which I want to listen to and trigger events based on its value, like this:
class viewModel {
constructor() {
this.value = '0';
let val = 2;
subscribe(this.value, callbackForValue);
subscribe(val, callbackForVal);
}
}
Is this a feature of Aurelia? If so, how would I go about setting up such a subscription?
In some plugins I've been using DI to get the ObserverLocator instance from the container:
import {inject} from 'aurelia-dependency-injection'; // or from 'aurelia-framework'
import {ObserverLocator} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(ObserverLocator)
export class Foo {
constructor(observerLocator) {
this.observerLocator = observerLocator;
}
...
}
You can then do something like this:
var subscription = this.observerLocator
.getObserver(myObj, 'myPropertyName')
.subscribe(myCallback);
When you're ready to dispose of the subscription, invoke it:
subscription();
I think this is all subject to change but it's something you could use right now if you needed to.
More info here
October 2015 update
The ObserverLocator is Aurelia's internal "bare metal" API. There's now a public API for the binding engine that could be used:
import {inject} from 'aurelia-dependency-injection'; // or from 'aurelia-framework'
import {BindingEngine} from 'aurelia-binding'; // or from 'aurelia-framework'
#inject(BindingEngine)
export class ViewModel {
constructor(bindingEngine) {
this.obj = { foo: 'bar' };
// subscribe
let subscription = bindingEngine.propertyObserver(this.obj, 'foo')
.subscribe((newValue, oldValue) => console.log(newValue));
// unsubscribe
subscription.dispose();
}
}
The observable attribute has less of an overhead to binding according to I kill nerds.
import {observable} from "aurelia-framework";
export class Example {
#observable
public description: string;
private descriptionChanged(newValue: string, oldValue: string): void {
}
}
listen to and trigger events based on its value
A snippet from code using TypeScript, hopefully that will get you an idea:
import {bindingMode} from "aurelia-binding";
export class Example{
#bindable
public description: string;
private descriptionChanged(newValue: string, oldValue: string): void {
console.log(newValue, oldValue);
}
}
Method name should follow convention `${propertyName}Changed`
EDIT: That's exactly what Decade Moon suggested in the comment above: Property change subscription with Aurelia
The #observable decorator works fine for this scenario.
You could use the BindingEngine to watch a collection or control when to subscribe/unsubscribe

Categories

Resources