I have two forms in one view, each outputting changes. I have a global directive with two selectors to get both streams.
I can easily handle one form but when I have two, and place each in its own pipe or directive, they mess up each others save function, so to speak, so I need both in the same pipe. (Or another solution ;)
Form component
<form name="shop_info"
autocomplete="off"
#f1="ngForm"
(SubmitShop)="save($event)"
[SaveShop]="f1"
*ngIf="merchant">
...
</form>
...
<form name="region"
autocomplete="off"
#f2="ngForm"
(SubmitRegion)="save($event)"
[SaveRegion]="f2">
...
</form>
Directive with one stream:
#Directive({
selector: '[SaveShop], [SaveRegion]'
})
export class SaveMerchantDirective implements OnInit {
#Input() SaveShop: any;
#Input() SaveRegion: any;
#Output() SubmitShop: EventEmitter<any> = new EventEmitter<any>();
#Output() SubmitRegion: EventEmitter<any> = new EventEmitter<any>();
#Input() debounce = 350;
constructor(
private saveService: SaveService,
private $transitions: TransitionService
) { }
ngOnInit() {
this.saveRegion.form.valueChanges
.pipe(
debounceTime(this.debounce),
switchMap((data: Object) => {
if (!sessionStorage.getItem('dataRegion') || sessionStorage.getItem('dataRegion') === '{}') {
sessionStorage.setItem('dataRegion', JSON.stringify(data));
}
this.$transitions.onBefore({}, () => {
if (this.saveRegion.valid && !this.saveRegion.pristine) {
if (confirm(this.translate.instant(
'You are about to discard your REGION changes. Click "OK" to discard and navigate away! Click "Cancel" to stay.'
))) {
this.discard();
return true;
} else {
return false;
}
}
});
if (this.saveRegion.valid && !this.saveRegion.pristine) {
window.dispatchEvent(new CustomEvent('showHeader', { detail: true }));
return this.saveService.currentStatus$.pipe(
map(status => {
if (status === 'save') {
this.save(data);
} else if (status === 'discard') {
this.discard();
}
})
);
} else {
return NEVER;
}
})
)
.subscribe();
this.$transitions.onExit({}, () => {
sessionStorage.removeItem('dataRegion');
status = 'false';
this.saveService.changeStatus('false');
});
How do I handle this?
Can I simply do:
combineLatest(this.saveRegion.form.valueChanges, this.saveShop.form.valueChanges)
.pipe(
debounceTime(this.debounce),
switchMap((data: Array<Object>) => {...
Edit:
Doing
Observable.combineLatest(
this.saveShop.form.valueChanges,
this.saveRegion.form.valueChanges
)
.subscribe(...
gives Property 'combineLatest' does not exist on type 'typeof Observable'.
Doing
combineLatest(
this.saveShop.form.valueChanges,
this.saveRegion.form.valueChanges
)
.subscribe(...
gives me several problems where one is that 'form' cannot read undefined (since the two streams do not come in simultaneously one is undefined).
And if I add startWith(null) I get
this.saveShop.form.valueChanges.startWith is not a function
And if I add startWith(null) I get this.saveShop.form.valueChanges.startWith is not a function
That at least is easily solved: this use of operators is deprecated (possibly would still work, if you imported the "right" startWith from the right file), instead use .pipe (as you are doing with other operators: debounceTime, switchMap), so
this.saveRegion.form.valueChanges.pipe(startWith(null)) //...
(Alternatively, concat(of(null), this.saveRegion.form.valueChanges) would do).
Another thing: you can filter out any undefined/null values and just use "the good stuff", like this
combineLatest(
this.saveShop.form.valueChanges.pipe(filter(value => Boolean(value)),
this.saveRegion.form.valueChanges.pipe(filter(value => Boolean(value)),
//...
Related
I would like to execute toggleButtonOnClick$() function from the service, only when I click on a button. This function will save current state, service will later receive this change and update usersObs$.
Whenever the above mentioned happens, the toggleButtonOnClick$ gets executed a few more times, even if I didn't click on the button again.
How can I prevent this and make this function only execute when I do .next() on the clickSubject and not when userObs$ changes?
I will write an example of the whole situation
ButtonComponent
private userClick = new Subject<null>();
private obs1$: Observable<string>();
private obs2$: Observable<string>();
private obs3$: Observable<boolean>();
ngOnInit(): void {
this.obs2$ = this.obs1$.pipe(
switchMap((value) => this.someService.getSomePropBasedOnValue$(value))
);
this.obs3$ = this.obs2$.pipe(
switchMap((value) => this.someService.checksAndReturnsBoolean$(value))
this.subscriptions.add(
this.someService.toggleButtonOnClick$(this.obs2$, this.userClick).subscribe()
)
}
ngOnDestroy(): void {
this.subscriptions.unsuscribe();
}
onClick(): void {
// emit a value when click on button to start observable chain
this.userClick.next();
}
HTML
<div
[attr.tooltip]="(obs3$ | async) ?
('text_translation_1' | transloco)
: ('text_translation_2' | transloco)"
>
<span
*ngIf="(obs3$ | async) && !isHovering"
>
Something here
</span>
<span
*ngIf="(obs3$ | async) && isHovering"
>
Something here
</span>
<span
*ngIf="!(obs3$ | async)"
>
Something here
</span>
</div>
SomeService
public checksAndReturnsBoolean$(id): Observable<boolean> {
return this.userObs$.pipe(
map((users) => { users.some(((user) => user.id === id)) }
);
}
public getSomePropBasedOnValue$(id): Observable<SomeObject | null> {
return this.userObs$.pipe(
map((users) => { users.find(((user) => user.id === id)) ?? null }
);
}
public toggleButtonOnClick$(obs2$, userClick): Observable<void> {
return userClick.pipe(
switchMap(() => obs3$),
switchMap((id) => combineLatest([this.getSomeDataById$(id), of(id)]))
).pipe(
map(([data, id]) => {
// some code block that gets executed everytime an observable emits new value
})
);
Once everything finishes, I try to store the users decision after a click is made. So the userObs$ gets updated, once that happens, the block within toggleButtonOnClick$ is executed again, and not once, but 2 sometimes 3 or 4.
Btw, in the component, the obs2$ im using it on the DOM with Async pipe to show/hide stuff. Maybe that is also triggering the calls after the service observable changes.
I've tried several things already without luck.
Any tip or help or guiding would be appreciated.
Thanks.
What I was needing was to use shareReplay(1) and take(1) on different functions on my service to make it work as expected without repeating unnecessary calls.
It would end up looking like this:
public checksAndReturnsBoolean$(id): Observable<boolean> {
return this.userObs$.pipe(
map((users) => { users.some((user) => user.id === id) }),
take(1)
);
}
public getSomePropBasedOnValue$(id): Observable<SomeObject | null> {
return this.userObs$.pipe(
map((users) => { users.find((user) => user.id === id) ?? null }),
shareReplay(1)
);
}
I am trying to share data between components using the rxjs subject and i've used that data in component
Component.html
<div class="spinner-container" *ngIf="loading">
<div class="spinner-item">
<nx-spinner nxSize="large"></nx-spinner>
</div>
</div>
component.ts
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
})
},100)
}
Here is the error
ExpressionChangedAfterItHasBeenCheckedError: Expression has changed
after it was checked. Previous value: 'ngIf: false'. Current value:
'ngIf: true'.
I found a workaround using changedetectref but I don't think its good practice is ther any other way to solve this issue
You can manually trigger change detection using the detectChanges() method of the ChangeDetectorRef
Try like this:
import { ChangeDetectorRef} from '#angular/core';
constructor(private cdr: ChangeDetectorRef) { }
ngOnInit(){
setTimeout(()=>{
this.commonService.spinnerTrigger.subscribe((trigger)=>{
this.loading = trigger;
if (this.cdr && !(this.cdr as ViewRef).destroyed) {
this.cdr.detectChanges();
}
})
},100)
}
Making the next callback async worked for me once:
this.commonService.spinnerTrigger.subscribe(async (trigger) => {
this.loading = await trigger;
});
Or adding a zero delay:
this.commonService.spinnerTrigger.pipe(delay(0)).subscribe((trigger) => {
this.loading = trigger;
});
This is an open issue in Github,
Github issue => https://github.com/angular/angular/issues/15634
And they provided a workaround using setTimeout() for now and still there aren't any updates regarding this issue.
And also you can try changeDetector that may solve your issue.
import { ChangeDetectorRef } from '#angular/core';
constructor(private cdRef:ChangeDetectorRef) {}
ngAfterViewChecked()
{
this.cdRef.detectChanges();
}
I don't see any need here to mess around with change detection / setTimeout (which triggers change detection).
Stackblitz
Use a spinner service which parent and child can use.
spinner.service.ts
#Injectable()
export class SpinnerService {
private loading = new BehaviorSubject<boolean>(true)
loading$: Observable<boolean> = this.loading.asObservable()
setSpinner(bool: boolean) {
this.loading.next(bool)
}
}
Example - Component setting spinner
ngOnInit() {
this.service.getChildData().pipe(
// handle any errors
catchError(err => {
console.log('Error caught: ', err)
this.data = err
return throwError(err)
}),
// no matter what set spinner false
finalize(() => {
this.spinnerService.setSpinner(false)
}),
// subscription clean up
takeUntil(this.destroyed$)
).subscribe(data => this.data = data)
}
Example - parent / container displaying spinner
ngOnInit() {
this.loading$ = this.spinnerService.loading$
this.spinnerService.setSpinner(true) // if needed
}
<div *ngIf="loading$ | async">
I am a spinner
</div>
In AngularJS, we can listen variable change using $watch, $digest... which is no longer possible with the new versions of Angular (5, 6).
In Angular, this behaviour is now part of the component lifecycle.
I checked on the official documention, articles and especially on Angular 5 change detection on mutable objects, to find out how to listen to a variable (class property) change in a TypeScript class / Angular
What is proposed today is :
import { OnChanges, SimpleChanges, DoCheck } from '#angular/core';
#Component({
selector: 'my-comp',
templateUrl: 'my-comp.html',
styleUrls: ['my-comp.css'],
inputs:['input1', 'input2']
})
export class MyClass implements OnChanges, DoCheck, OnInit{
//I can track changes for this properties
#Input() input1:string;
#Input() input2:string;
//Properties what I want to track !
myProperty_1: boolean
myProperty_2: ['A', 'B', 'C'];
myProperty_3: MysObject;
constructor() { }
ngOnInit() { }
//Solution 1 - fired when Angular detects changes to the #Input properties
ngOnChanges(changes: SimpleChanges) {
//Action for change
}
//Solution 2 - Where Angular fails to detect the changes to the input property
//the DoCheck allows us to implement our custom change detection
ngDoCheck() {
//Action for change
}
}
This is only true for #Input() property !
If I want to track changes of my component's own properties (myProperty_1, myProperty_2 or myProperty_3), this will not work.
Can someone help me to solve this problematic ? Preferably a solution that is compatible with Angular 5
You can still check component's field members value change by KeyValueDiffers via DoCheck lifehook.
import { DoCheck, KeyValueDiffers, KeyValueDiffer } from '#angular/core';
differ: KeyValueDiffer<string, any>;
constructor(private differs: KeyValueDiffers) {
this.differ = this.differs.find({}).create();
}
ngDoCheck() {
const change = this.differ.diff(this);
if (change) {
change.forEachChangedItem(item => {
console.log('item changed', item);
});
}
}
see demo.
I think the nicest solution to your issue is to use a decorator that replaces the original field with a property automatically, then on the setter you can create a SimpleChanges object similar to the one created by angular in order to use the same notification callback as for angular (alternatively you could create a different interface for these notifications, but the same principle applies)
import { OnChanges, SimpleChanges, DoCheck, SimpleChange } from '#angular/core';
function Watch() : PropertyDecorator & MethodDecorator{
function isOnChanges(val: OnChanges): val is OnChanges{
return !!(val as OnChanges).ngOnChanges
}
return (target : any, key: string | symbol, propDesc?: PropertyDescriptor) => {
let privateKey = "_" + key.toString();
let isNotFirstChangePrivateKey = "_" + key.toString() + 'IsNotFirstChange';
propDesc = propDesc || {
configurable: true,
enumerable: true,
};
propDesc.get = propDesc.get || (function (this: any) { return this[privateKey] });
const originalSetter = propDesc.set || (function (this: any, val: any) { this[privateKey] = val });
propDesc.set = function (this: any, val: any) {
let oldValue = this[key];
if(val != oldValue) {
originalSetter.call(this, val);
let isNotFirstChange = this[isNotFirstChangePrivateKey];
this[isNotFirstChangePrivateKey] = true;
if(isOnChanges(this)) {
var changes: SimpleChanges = {
[key]: new SimpleChange(oldValue, val, !isNotFirstChange)
}
this.ngOnChanges(changes);
}
}
}
return propDesc;
}
}
// Usage
export class MyClass implements OnChanges {
//Properties what I want to track !
#Watch()
myProperty_1: boolean = true
#Watch()
myProperty_2 = ['A', 'B', 'C'];
#Watch()
myProperty_3 = {};
constructor() { }
ngOnChanges(changes: SimpleChanges) {
console.log(changes);
}
}
var myInatsnce = new MyClass(); // outputs original field setting with firstChange == true
myInatsnce.myProperty_2 = ["F"]; // will be notified on subsequent changes with firstChange == false
as said you can use
public set myProperty_2(value: type): void {
if(value) {
//doMyCheck
}
this._myProperty_2 = value;
}
and then if you need to retrieve it
public get myProperty_2(): type {
return this._myProperty_2;
}
in that way you can do all the checks that you want while setting/ getting your variables such this methods will fire every time you set/get the myProperty_2 property.
small demo: https://stackblitz.com/edit/angular-n72qlu
I think I came into the way to listen to DOM changes that you can get any changes that do to your element, I really hope these hints and tips will help you to fix your problem, following the following simple step:
First, you need to reference your element like this:
in HTML:
<section id="homepage-elements" #someElement>
....
</section>
And in your TS file of that component:
#ViewChild('someElement')
public someElement: ElementRef;
Second, you need to create an observer to listen to that element changes, you need to make your component ts file to implements AfterViewInit, OnDestroy, then implement that ngAfterViewInit() there (OnDestroy has a job later):
private changes: MutationObserver;
ngAfterViewInit(): void {
console.debug(this.someElement.nativeElement);
// This is just to demo
setInterval(() => {
// Note: Renderer2 service you to inject with constructor, but this is just for demo so it is not really part of the answer
this.renderer.setAttribute(this.someElement.nativeElement, 'my_custom', 'secondNow_' + (new Date().getSeconds()));
}, 5000);
// Here is the Mutation Observer for that element works
this.changes = new MutationObserver((mutations: MutationRecord[]) => {
mutations.forEach((mutation: MutationRecord) => {
console.debug('Mutation record fired', mutation);
console.debug(`Attribute '${mutation.attributeName}' changed to value `, mutation.target.attributes[mutation.attributeName].value);
});
}
);
// Here we start observer to work with that element
this.changes.observe(this.someElement.nativeElement, {
attributes: true,
childList: true,
characterData: true
});
}
You will see the console will work with any changes on that element:
This is another example here that you will see 2 mutation records fired and for the class that changed:
// This is just to demo
setTimeout(() => {
// Note: Renderer2 service you to inject with constructor, but this is just for demo so it is not really part of the answer
this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds()));
this.renderer.addClass(this.someElement.nativeElement, 'newClass' + (new Date().getSeconds() + 1));
}, 5000);
// Here is the Mutation Observer for that element works
this.changes = new MutationObserver((mutations: MutationRecord[]) => {
mutations.forEach((mutation: MutationRecord) => {
console.debug('Mutation record fired', mutation);
if (mutation.attributeName == 'class') {
console.debug(`Class changed, current class list`, mutation.target.classList);
}
});
}
);
Console log:
And just housekeeping stuff, OnDestroy:
ngOnDestroy(): void {
this.changes.disconnect();
}
Finally, you can look into this Reference: Listening to DOM Changes Using MutationObserver in Angular
You can import ChangeDetectorRef
constructor(private cd: ChangeDetectorRef) {
// detect changes on the current component
// this.cd is an injected ChangeDetector instance
this.cd.detectChanges();
// or run change detection for the all app
// this.appRef is an ApplicationRef instance
this.appRef.tick();
}
I've got this EventsStorage typescript class that is responsible for storing and retrieving Event objects in ionic-storage (wrapper for sqlite and indexedDB). It uses my Event class throughout.
I would like to reuse a lot of this logic for something other than an Event, like a Widget.
I come from a ruby background where it would be relatively simple to extract all the storage logic, set a ruby var that is literally the class Event and use that var wherever I use Event. Can I do something similar in typescript? Is there another mechanic I can use to reuse the bulk of this class for something else, like Widget?
Ideally, my EventsStorage class becomes really lightweight, and I'm not just wrapping calls to this.some_storage_module.get_ids() or this.some_storage_module.insert_new_objs() -- which would have to be copy/pasted to every other instance I needed this.
Something like this:
export class EventsStorage { // extends BaseStorage (maybe??)
constructor(){
super(Event, 'events'); // or some small set of magical args
}
}
Here's the existing class:
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
import { Event } from '../classes/event';
// EventsStorage < EntityStorage
// - tracks local storage info
// - a key to an array of saved objects
// - a query() method that returns saved objects
#Injectable()
export class EventsStorage {
base_key: string;
ids_key: string;
constructor(
private storage: Storage
){
this.base_key = 'event';
this.ids_key = [this.base_key, 'ids'].join('_');
}
get_ids(): Promise<any>{
return this.storage.ready().then(() => {
return this.storage.get(this.ids_key).then((val) => {
if(val === null){
return [];
} else {
return val;
}
});
});
}
insert_new_objs(new_objs: any): Promise<any>{
return new_objs.reduce((prev: Promise<string>, cur: any): Promise<any> => {
return prev.then(() => {
return this.storage.set(cur._id, cur.event);
});
}, Promise.resolve()).then(() => {
console.log('saving event_ids');
return this.storage.set(this.ids_key, new_objs.map(obj => obj._id));
});
}
update(events: Event[]): Promise<any> {
let new_objs = events.map((event) => {
return {
_id: [this.base_key, event.id].join('_'),
event: event
};
});
return this.insert_new_objs(new_objs);
}
query(): Promise<Event[]>{
let events = [];
return this.get_ids().then((ids) => {
return ids.reduce((prev: Promise<string>, cur: string): Promise<any> => {
return prev.then(() => {
return this.get_id(cur).then((raw_event) => {
events = events.concat([raw_event as Event]);
return events;
});
});
}, Promise.resolve());
});
}
get_id(id: string): Promise<Event>{
return this.storage.get(id).then((raw_event) => {
return raw_event;
});
}
}
It looks to me like you want to use generics. You basically define some basic interface between all the things you'll want to store, and your code should depend on that interface. In your code as far as I can tell you only use the id property.
So it would look kinda like this
import { Event } from '...';
import { Widget } from '...';
interface HasId{
id: string;
}
class ItemsStorage<T extends HasId> {
....
get_id(id: string): Promise<T>{
...
}
}
const EventStorage = new ItemsStorage<Events>(storage);
const WidgetStorage = new ItemsStorage<Widget>(storage);
const ev = EventStorage.get_id('abc'); //type is Promise<Event>
const wd = WidgetStorage.get_id('def'); //type is Promise<Widget>
You can read more about generics here.
Edit:
1 - about subclassing - It's usually less preferable. If your ItemsStorage class need different behavior when dealing with Events vs Widgets, than subclassing is your solution. But if you have the same behavior for every class, one might call your code generic, and using generics is better.
I am working on an Angular2 application and one of the #Components has a button that when clicked will send a post request to my server which will either respond with an Ok(string) or a BadRequest(string).
I am having trouble updating an #Input field of one of my #Components after getting the answer from the server.
Below are simplified version of some of my classes.
My Component class
#Component({
moduleId: module.id,
selector: 'model-comp',
templateUrl: './model.component.html',
styleUrls: ['./model.component.css']
})
export class MyComponent{
#Input() model: Model;
#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
this.emitter.emit(this.model);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
My Service class
export class MyService{
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Template of MyComponent
<button (click)="toggle()"
[ngClass]="{'class1': true, 'class2': model.flag}">Text</button>
Template of Parent Component
<div *ngFor="let model of getList()">
<model-comp [model]="model" (emitter)="onEmit($event)"></model-comp>
</div>
The onEmit Function
onEmit(evt: any): void{
if(evt instanceof Model){
var evtModel = evt as Model;
this.list.find(search => search.id == evtModel.id)
.isFav = evtModel.isFav;
}
}
The problem is that even though I post my data and receive the response, The property flag of my model does not change.
I think that the click event reloads the component thus removing the observers of the EventEmitter.
So is there any way to cancel the reload, not lose the observers of the EventEmitter or any other way to update the root object or the element class?
update (see comments below the question)
If getList() (what *ngFor binds to) returns a new list every time it is called, *ngFor will be permanently busy rerendering the items because change detection will cause getList() being called again and again.
Binding to a function that returns a new object or array every time it's called directly will cause serious issues like exceptions and dramatic performance degredation.
Using method/function calls in the view is strongly discouraged in general. Rather assign the list to a field and bind to that field instead of the method.
ngOnInit() is fine for initializing the list but also any event handler for initializing or updating the list.
original
If you modify the model value that you got passed in from the parent, then the parent also sees the change. Emitting the value as an event is probably redundant.
I guess you are modifying list (from <div *ngFor="let model of list">) in onEmit() which then causes *ngFor to rerender the list.
I don't think you should change #input property from within the component.
it suppose to listen and act to changes from the parent component.
MyComponent.ts
export class MyComponent{
#Input() model: Model;
//#Output() emitter: EventEmitter<Model> = new EventEmitter<Model>();
public constructor(private service: MyService){}
public toggle(): void {
this.service.send(model.id, model.name){
.subscribe(
result => this.onSuccess(result)),
error => this.onError(error),
() => this.onComplete());
}
public onSuccess(result: string): void {
if(result.inculdes("Some Text")) this.model.flag = true;
else this.model.flag = false;
//this.emitter.emit(this.model);
this.service.emitter.next(false);
}
public onError(error: any): void {
//notification using bootstrap-notify
}
public onComplete(): void {
//currently empty
}
}
Service
#Injectable // important
export class MyService{
public emitter: Subject<any> = new Subject();
public send(id: string, name: string){
return <Observable<string>>this.http
.post('url', new Dto(id, name))
.map(result => this.getData<string>(result))
.catch(this.catchBadResponse);
}
private getData<E>(result: Response): E {
//checking if result.status is ok
var body = result.json ? res.json(): null;
return <E>(body || {});
}
private catchBadRespomse: (error: any) => Observable<any> = (error: any) => {
var response = <Response>error;
var json = response.json();
var msg = json.Message;
var errormsg = json?
(json.error ? json.error: JSON.stringify(msg?msg:json)) :
(response.statusText || 'Error?');
return Obserable.of(errormsg);
}
}
Now you can listen to Service.emitter anywhere in app