ngOnChange only triggers after clicked somewhere on the screen after subscribe - subscribe

I have the following components.
Parent:
#Component({
selector: 'test'
})
onAccountChange(account: AccountItem): void {
this.appService.callApi.subscribe((data) => {
this.availableInstrument = data
})
}
And my Child component is as follows:
#Component({
selector: 'child'
})
export class Child implements OnInit, OnChanges {
#Input('availableInstrument') availableInstrument!: any[]
ngOnChanges() :void {
console.log("Change")
this.initializeComponent()
}
My question is at I notice that the ngOnChange function only gets called after I click somewhere else on the screen, even though the callback function (onAccountChange) has completed and availableInstrument has been populated from the api call. I would want the ngOnChange function be called as soon as the input data is changed from the api call rather than having to click somewhere else on the screen. If I hard code the input values in my call back function, I see ngOnChange is called immediately in the child component, but not when it is within the subscribe block.
Thank you so much!

You need to set the change detection strategy to onPush.
#Component({
selector: "child",
changeDetection: ChangeDetectionStrategy.OnPush
})

Related

Events provider is deprecating. Using Redux or Observables for state in ionic apps

I've been using events in my ionic application, where i subscribe in one page, and publish the event in the other page. Now I see a warning that Events are going to be changed with Observables and Redux state and effect.
I was using Events mainly to call for component function changes outside it, so I had a components for example:
Component1.ts
this.events.subscribe('event:addValue1', (data: any) => {
this.valueName = 'VALUE1';
});
this.events.subscribe('event:addValue2', (data: any) => {
this.valueName = 'VALUE2';
});
and than outside this component I was calling the publish methods from any page, like:
Page1.ts
this.events.publish('event:addValue1');
Page2.ts
this.events.publish('event:addValue2');
By this i was able to change the data (this.valueName) outside the Component1.ts from any other page, simply by publishing the desired event.
I know that this might not sound or be right approach, but It was the only way I was doing changes to my Component1.ts outside it from any page.
I have now changed this and just put separate functions and than i access them via ViewChild component name like
#ViewChild('component') component: any;
....
this.component.functionAddValue1().
and additionally I send additional params via Angular NavigationExtras if i need to calculate and call some function from the Component1.ts, lets say if I navigate to some route.
Before this I was just calling the events.publish and I was able to make the changes to the Component1.ts on the fly.
Create event service.
In the EventService.ts:
export class EventService {
private dataObserved = new BehaviorSubject<any>('');
currentEvent = this.dataObserved.asObservable();
constructo(){}
publish(param):void {
this.dataObserved.next(param);
}
}
For publishing the event from example page1:
constructor(public eventService:EventService){}
updatePost(value){
this.eventService.publish({name:'post:updated',params:value});
}
In page 2:
constructor(public eventService:EventService){
eventService.currentEvent.subscribe(value=>{
if(value.name=='post:updated'){
//get value.name
}else if(value.name=='another:event'){
//get value or update view or trigger function or method...
}
// here you can get the value or do whatever you want
});
}

Wrapping a FormControl in Angular (2+)

I'm trying to create a custom form control in Angular (v5). The custom control is essentially a wrapper around an Angular Material component, but with some extra stuff going on.
I've read various tutorials on implementing ControlValueAccessor, but I can't find anything that accounts for writing a component to wrap an existing component.
Ideally, I want a custom component that displays the Angular Material component (with some extra bindings and stuff going on), but to be able to pass in validation from the parent form (e.g. required) and have the Angular Material components handle that.
Example:
Outer component, containing a form and using custom component
<form [formGroup]="myForm">
<div formArrayName="things">
<div *ngFor="let thing of things; let i = index;">
<app-my-custom-control [formControlName]="i"></app-my-custom-control>
</div>
</div>
</form>
Custom component template
Essentially my custom form component just wraps an Angular Material drop-down with autocomplete. I could do this without creating a custom component, but it seems to make sense to do it this way as all the code for handling filtering etc. can live within that component class, rather than being in the container class (which doesn't need to care about the implementation of this).
<mat-form-field>
<input matInput placeholder="Thing" aria-label="Thing" [matAutocomplete]="thingInput">
<mat-autocomplete #thingInput="matAutocomplete">
<mat-option *ngFor="let option of filteredOptions | async" [value]="option">
{{ option }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
So, on the input changing, that value should be used as the form value.
Things I've tried
I've tried a few ways of doing this, all with their own pitfalls:
Simple event binding
Bind to keyup and blur events on the input, and then notify the parent of the change (i.e. call the function that Angular passes into registerOnChange as part of implementing ControlValueAccessor).
That sort of works, but on selecting a value from the dropdown it seems the change events don't fire and you end up in an inconsistent state.
It also doesn't account for validation (e.g. if it's "required", when a value isn;t set the form control will correctly be invalid, but the Angular Material component won't show as such).
Nested form
This is a bit closer. I've created a new form within the custom component class, which has a single control. In the component template, I pass in that form control to the Angular Material component. In the class, I subscribe to valueChanges of that and then propagate the changes back to the parent (via the function passed into registerOnChange).
This sort of works, but feels messy and like there should be a better way.
It also means that any validation applied to my custom form control (by the container component) is ignored, as I've created a new "inner form" that lacks the original validation.
Don't use ControlValueAccessor at all, and instead just pass in the form
As the title says... I tried not doing this the "proper" way, and instead added a binding to the parent form. I then create a form control within the custom component as part of that parent form.
This works for handling value updates, and to an extent validation (but it has to be created as part of the component, not the parent form), but this just feels wrong.
Summary
What's the proper way of handling this? It feels like I'm just stumbling through different anti-patterns, but I can't find anything in the docs to suggest that this is even supported.
Edit:
I've added a helper for doing just this an angular utilities library I've started: s-ng-utils. Using that you can extend WrappedFormControlSuperclass and write:
#Component({
selector: 'my-wrapper',
template: '<input [formControl]="formControl">',
providers: [provideValueAccessor(MyWrapper)],
})
export class MyWrapper extends WrappedFormControlSuperclass<string> {
// ...
}
See some more documentation here.
One solution is to get the #ViewChild() corresponding to the inner form components ControlValueAccessor, and delegating to it in your own component. For example:
#Component({
selector: 'my-wrapper',
template: '<input ngDefaultControl>',
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => NumberInputComponent),
multi: true,
},
],
})
export class MyWrapper implements ControlValueAccessor {
#ViewChild(DefaultValueAccessor) private valueAccessor: DefaultValueAccessor;
writeValue(obj: any) {
this.valueAccessor.writeValue(obj);
}
registerOnChange(fn: any) {
this.valueAccessor.registerOnChange(fn);
}
registerOnTouched(fn: any) {
this.valueAccessor.registerOnTouched(fn);
}
setDisabledState(isDisabled: boolean) {
this.valueAccessor.setDisabledState(isDisabled);
}
}
The ngDefaultControl in the template above is to manually trigger angular to attach its normal DefaultValueAccessor to the input. This happens automatically if you use <input ngModel>, but we don't want the ngModel here, just the value accessor. You'll need to change DefaultValueAccessor above to whatever the value accessor is for the material dropdown - I'm not familiar with Material myself.
I'm a bit late to the party but here is what I did with wrapping a component which might accept formControlName, formControl, or ngModel
#Component({
selector: 'app-input',
template: '<input [formControl]="control">',
styleUrls: ['./app-input.component.scss']
})
export class AppInputComponent implements OnInit, ControlValueAccessor {
constructor(#Optional() #Self() public ngControl: NgControl) {
if (this.ngControl != null) {
// Setting the value accessor directly (instead of using the providers) to avoid running into a circular import.
this.ngControl.valueAccessor = this;
}
}
control: FormControl;
// These are just to make Angular happy. Not needed since the control is passed to the child input
writeValue(obj: any): void { }
registerOnChange(fn: (_: any) => void): void { }
registerOnTouched(fn: any): void { }
ngOnInit() {
if (this.ngControl instanceof FormControlName) {
const formGroupDirective = this.ngControl.formDirective as FormGroupDirective;
if (formGroupDirective) {
this.control = formGroupDirective.form.controls[this.ngControl.name] as FormControl;
}
} else if (this.ngControl instanceof FormControlDirective) {
this.control = this.ngControl.control;
} else if (this.ngControl instanceof NgModel) {
this.control = this.ngControl.control;
this.control.valueChanges.subscribe(x => this.ngControl.viewToModelUpdate(this.control.value));
} else if (!this.ngControl) {
this.control = new FormControl();
}
}
}
Obviously, don't forget to unsubscribe from this.control.valueChanges
I have actually been wrapping my head around this for a while and I figured out a good solution that is very similar (or the same) as Eric's.
The thing he forgot to account for, is that you can't use the #ViewChild valueAccessor until the view has actually loaded (See #ViewChild docs)
Here is the solution: (I am giving you my example which is wrapping a core angular select directive with NgModel, since you are using a custom formControl, you will need to target that formControl's valueAccessor class)
#Component({
selector: 'my-country-select',
templateUrl: './country-select.component.html',
styleUrls: ['./country-select.component.scss'],
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: CountrySelectComponent,
multi: true
}]
})
export class CountrySelectComponent implements ControlValueAccessor, OnInit, AfterViewInit, OnChanges {
#ViewChild(SelectControlValueAccessor) private valueAccessor: SelectControlValueAccessor;
private country: number;
private formControlChanged: any;
private formControlTouched: any;
public ngAfterViewInit(): void {
this.valueAccessor.registerOnChange(this.formControlChanged);
this.valueAccessor.registerOnTouched(this.formControlTouched);
}
public registerOnChange(fn: any): void {
this.formControlChanged = fn;
}
public registerOnTouched(fn: any): void {
this.formControlTouched = fn;
}
public writeValue(newCountryId: number): void {
this.country = newCountryId;
}
public setDisabledState(isDisabled: boolean): void {
this.valueAccessor.setDisabledState(isDisabled);
}
}
NgForm is providing an easy way to manage your forms without injecting any data in a HTML form. Input data must be injected at the component level not in a classic html tag.
<form #myForm="ngForm" (ngSubmit)="onSubmit(myForm)>...</form>
Other way is to create a form component where all the data model is binded using ngModel ;)

Angular 2 + ngrx(redux) + forms

How do you handle Angular 2 forms in unidirectional data flow? Especially with validation between several parent/child components?
I am using ngrx/store and model driven forms with form builder.. Is it possible to do something similar like form reducer in React and make it as a part of Store?
Do you have some articles about it?
I have created a library called ngrx-forms that does exactly what you want. You can get it on npm via:
npm install ngrx-forms --save
I recommend checking out the full README on the github page, but below you can find some examples of what you need to do to get the library up and running once installed.
Import the module:
import { StoreModule } from '#ngrx/store';
import { NgrxFormsModule } from 'ngrx-forms';
import { reducers } from './reducer';
#NgModule({
declarations: [
AppComponent,
],
imports: [
NgrxFormsModule,
StoreModule.forRoot(reducers),
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }
Add a group state somewhere in your state tree via createFormGroupState and call the formGroupReducer inside your reducer:
import { Action } from '#ngrx/store';
import { FormGroupState, createFormGroupState, formGroupReducer } from 'ngrx-forms';
export interface MyFormValue {
someTextInput: string;
someCheckbox: boolean;
nested: {
someNumber: number;
};
}
const FORM_ID = 'some globally unique string';
const initialFormState = createFormGroupState<MyFormValue>(FORM_ID, {
someTextInput: '',
someCheckbox: false,
nested: {
someNumber: 0,
},
});
export interface AppState {
someOtherField: string;
myForm: FormGroupState<MyFormValue>;
}
const initialState: AppState = {
someOtherField: '',
myForm: initialFormState,
};
export function appReducer(state = initialState, action: Action): AppState {
const myForm = formGroupReducer(state.myForm, action);
if (myForm !== state.myForm) {
state = { ...state, myForm };
}
switch (action.type) {
case 'some action type':
// modify state
return state;
default: {
return state;
}
}
}
Expose the form state inside your component:
import { Component } from '#angular/core';
import { Store } from '#ngrx/store';
import { FormGroupState } from 'ngrx-forms';
import { Observable } from 'rxjs/Observable';
import { MyFormValue } from './reducer';
#Component({
selector: 'my-component',
templateUrl: './my-component.html',
})
export class MyComponent {
formState$: Observable<FormGroupState<MyFormValue>>;
constructor(private store: Store<AppState>) {
this.formState$ = store.select(s => s.myForm);
}
}
Set the control states in your template:
<form novalidate [ngrxFormState]="(formState$ | async)">
<input type="text"
[ngrxFormControlState]="(formState$ | async).controls.someTextInput">
<input type="checkbox"
[ngrxFormControlState]="(formState$ | async).controls.someCheckbox">
<input type="number"
[ngrxFormControlState]="(formState$ | async).controls.nested.controls.someNumber">
</form>
This is a fairly old question, but I couldn't find a great solution in my own quest for working with ngrx + reactive forms in Angular. As a result, I'll post my research here with hope that it may help someone else. My solution can be broken down into two parts, and I pray you (oh weathered soul) find it applicable to your problem:
1) Monitor the form element/s (for example, "keyup" event for a typical text input), and update the State from that event. This strategy comes straight out of the book search component in the ngrx example app. We can now successfully populate the State as our form changes. Awesome! 50% done!
2) The angular reactive forms guide demonstrates creating the form group in the constructor. I have seen some other people do it inside ngOnInit, but this is too late in the lifecycle for our needs (I tried, I failed). Now that we have our form group established, setup ngOnChanges to capture any changes pushed from the state, and then update the form group using patchValue. For example:
ngOnChanges(changes: SimpleChanges) {
if (changes.valueICareAbout1) {
this.myForm.patchValue({
valueICareAbout1: changes.valueICareAbout1.currentValue
});
}
if (changes.valueICareAbout2) {
this.myForm.patchValue({
valueICareAbout2: changes.valueICareAbout2.currentValue
});
}
}
In the applications I built with Angular 2, the following guideline seemed to work well:
Parent components pass data down to children via data binding. Child components request data changes by emitting output events to parent components. It is the parent components responsibility to act accordingly.
In a hierarchical component structure, data changes are handled by the lowest component that depends on the data. If there's another component higher up or a sibling that depends on the same data item, pass changes up by emitting events and leave the handling to a higher component.
This scheme works well because, for any data that is relevant to more than one component, there is a single component responsible for performing changes. Changes bubble down automatically. Components are reusable, and changes in the component tree can be easily adapted.
With regard to validation, any component in the ladder between the lowest component emitting a data change request up to the highest component that finally handles the change, any component can effectively cancel the change by not passing it higher up. In most applications, I'd opt for validating data changes at the origin of the change though.
Naturally, child components can still have internal state and need not communicate changes - unless changes are relevant to the parent component.
Form data is inherently a very local state, especially for Angular since ngModel binds to local component variables. The top devs that I know recommend keeping the data for the form localized to that component (ie just use ngModel with local variables). This is because un-submitted form data is almost never shared by various components across your whole application. When the user submits the form then you can dispatch an action with a payload containing the form data to a parent component, to the store, or even to an ngrx/effect to be posted to a server.

Model update not reflected in UI only on second NavController page

I have a bizarre problem that when I change a value in the model, it does not update the view. My demo is a simple page which displays a timer whose value is updated in the model which I want reflected in the UI:
import { Component } from '#angular/core';
import { Observable } from 'rxjs/Rx';
#Component({
template: '<ion-content>Ticks (every second) : {{ticks}}</ion-content>',
})
export class ProgramOverviewPage {
ticks = 0;
ngOnInit() {
let timer = Observable.timer(0, 1000);
timer.subscribe(t => { this.ticks = t; console.log(t);});
}
}
If I set this page as my root page, it works fine. However, if I set a different page as my root page, and then immediately navigate to the timer page:
ngOnInit() {
this.nav.push(ProgramOverviewPage, {
});
}
then the page renders, but the tick value does not update the UI. I can't think of anything other than that the NavController is messing with the ChangeDetector, but I don't know why that would be. Anything I can add to debug this is much appreciated.
"ionic-angular": "2.0.0-beta.10"
Ionic 2 seems to be automatically setting Change Detection to OnPush for each of the Content objects (generated from <ion-content> I believe). This can be verified by using Augury and clicking on the Content object.
Because of this, it's necessary to explicitly tell the change detection system whenever you make any change which should be pushed to the UI using the ChangeDetectorRef.detectChanges() method. See the thoughtram blog for details.
import { Component, ChangeDetectorRef } from '#angular/core';
import { Observable } from 'rxjs/Rx';
#Component({
template: '<ion-content>Ticks (every second) : {{ticks}}</ion-content>',
})
export class ProgramOverviewPage {
ticks = 0;
ngOnInit() {
let timer = Observable.timer(0, 1000);
timer.subscribe(t => {
this.ticks = t;
console.log(t);
this.cd.detectChanges(); // Invoke the change detector
});
}
}

Angular2 Undefined Object attribute when using a callback function as Component #Input()

Im trying to bind a callback function to a directive, when the event is fired the attribute of the parent component are undefined
app.ts
import {Component} from 'angular2/core';
import {bootstrap} from 'angular2/platform/browser';
import {MyComponent} from './my-component';
#Component({
selector: 'my-app',
template: `
<button (click)="appOnClick('CLICK FROM APP')">BUTTOM OUTSIDE COMPONENT</button>
<br><br>
<my-component [onClick]="appOnClick"></my-component>`,
directives: [MyComponent]
})
export class MyApp {
public theBoundCallback: Function;
test:string = "THIS SHOULD HAVE A VALUE";
public ngOnInit(){
this.theBoundCallback = this.appOnClick.bind(this);
}
appOnClick(someText){
console.log(someText);
console.log(this.test);
}
}
bootstrap(MyApp);
my-component.ts
import {Component, Input} from 'angular2/core';
#Component({
selector: 'my-component',
template: `<button (click)="onClick('CLICK FROM COMPONENT')">BUTTOM INSIDE COMPONENT</button>`
})
export class MyComponent{
#Input() onClick: Function;
}
That will render two buttons:
BUTTOM OUTSIDE COMPONENT, this calls the appOnClick function direct from the app, when clicked the console shows:
- CLICK FROM APP
- THIS SHOULD HAVE A VALUE
BUTTOM INSIDE COMPONENT, this calls the appOnClick function via the #Input function in the component, when clicked the console shows:
- CLICK FROM APP
- undefined
I've created the example on Plunker
Is that a way to assign this correctly so I can work with my object attributes when the callback is trigger?
Updated plunkr
In order to pass appOnClick around this way, you need to declare it as a property like so:
export class MyApp {
...
appOnClick = (someText) => {
console.log(someText);
console.log(this.test);
}
}
instead of:
export class MyApp {
...
appOnClick(someText){
console.log(someText);
console.log(this.test);
}
}
I think that you forgot "(...)" when using the appOnClick method and use "[...]" instead of "(...)" when configuring the event handler:
<my-component (onClick)="appOnClick($event)"></my-component>`,
Moreover within your sub component you need to define a custom event with "#Output":
#Component({
selector: 'my-component',
template: `<button (click)="handleClick('CLICK FROM COMPONENT')">BUTTOM INSIDE COMPONENT</button>`
})
export class MyComponent{
#Output()
onClick:EventEmitter<string> = new EventEmitter();
handleClick(txt:string) {
this.onClick.emit(txt);
}
}