I have a custom form control component (it is a glorified input). The reason for it being a custom component is for ease of UI changes - i.e. if we change the way we style our input controls fundamentally it will be easy to propagate change across the whole application.
Currently we are using Material Design in Angular https://material.angular.io
which styles controls very nicely when they are invalid.
We have implemented ControlValueAccessor in order to allow us to pass a formControlName to our custom component, which works perfectly; the form is valid/invalid when the custom control is valid/invalid and the application functions as expected.
However, the issue is that we need to style the UI inside the custom component based on whether it is invalid or not, which we don't seem to be able to do - the input that actually needs to be styled is never validated, it simply passes data to and from the parent component.
COMPONENT.ts
import { Component, forwardRef, Input, OnInit } from '#angular/core';
import {
AbstractControl,
ControlValueAccessor,
NG_VALIDATORS,
NG_VALUE_ACCESSOR,
ValidationErrors,
Validator,
} from '#angular/forms';
#Component({
selector: 'app-input',
templateUrl: './input.component.html',
styleUrls: ['./input.component.css'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => InputComponent),
multi: true
}
]
})
export class InputComponent implements OnInit, ControlValueAccessor {
writeValue(obj: any): void {
this._value = obj;
}
registerOnChange(fn: any): void {
this.onChanged = fn;
}
registerOnTouched(fn: any): void {
this.onTouched = fn;
}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
get value() {
return this._value;
}
set value(value: any) {
if (this._value !== value) {
this._value = value;
this.onChanged(value);
}
}
#Input() type: string;
onBlur() {
this.onTouched();
}
private onTouched = () => {};
private onChanged = (_: any) => {};
disabled: boolean;
private _value: any;
constructor() { }
ngOnInit() {
}
}
COMPONENT.html
<ng-container [ngSwitch]="type">
<md-input-container class="full-width" *ngSwitchCase="'text'">
<span mdPrefix><md-icon>lock_outline</md-icon> </span>
<input mdInput placeholder="Password" type="text" [(ngModel)]="value" (blur)="onBlur()" />
</md-input-container>
</ng-container>
example use on page:
HTML:
<app-input type="text" formControlName="foo"></app-input>
TS:
this.form = this.fb.group({
foo: [null, Validators.required]
});
You can get access of the NgControl through DI. NgControl has all the information about validation status. To retrieve NgControl you should not provide your component through NG_VALUE_ACCESSOR instead you should set the accessor in the constructor.
#Component({
selector: 'custom-form-comp',
templateUrl: '..',
styleUrls: ...
})
export class CustomComponent implements ControlValueAccessor {
constructor(#Self() #Optional() private control: NgControl) {
this.control.valueAccessor = this;
}
// ControlValueAccessor methods and others
public get invalid(): boolean {
return this.control ? this.control.invalid : false;
}
public get showError(): boolean {
if (!this.control) {
return false;
}
const { dirty, touched } = this.control;
return this.invalid ? (dirty || touched) : false;
}
}
Please go through this article to know the complete information.
Answer found here:
Get access to FormControl from the custom form component in Angular
Not sure this is the best way to do it, and I'd love someone to find a prettier way, but binding the child input to the form control obtained in this manner solved our issues
In addition: Might be considered dirty, but it does the trick for me:
let your component implement the Validator interface.
2 In the validate function you use the controlcontainer to get to the outer formcontrol of your component.
Track the status of the parent form control (VALID/INVALID) by using a variable.
check for touched. and perform validation actions on your fields only when touched is true and the status has changed.
Related
I am using ag-grid enterprise with angular 6. I have a master/detail setup with a custom detailCellRenderer.
The issue I am having is that the detail closes if any data changes in the master row. I cannot find any documentation on stopping that or even detecting that it is happening.
here is my grid definition:
ag-grid-angular(
style="height: 100%;width: 100%",
class="ag-theme-balham",
[gridOptions]='gridOptions',
[enableSorting]="true",
[enableColResize]='true',
[unSortIcom]='true',
[enableFilter]='true',
[rowSelection]="'single'",
[suppressRowDrag]='true',
[animateRows]='true',
[sideBar]='sideBar',
[statusBar] = 'statusBar',
[enableRangeSelection] = 'true',
[floatingFilter] = 'true',
[suppressDragLeaveHidesColumns]='true',
(gridReady)="onGridReady($event)",
(firstDataRendered)="onFirstDataRendered($event)",
[rowData]="alarms",
[columnDefs]="columnDefs",
[pagination]="true",
[paginationAutoPageSize]='true',
[frameworkComponents]='frameworkComponents',
[masterDetail]="true",
detailCellRenderer = "alarmInstanceSubtableRenderer",
[getContextMenuItems]="getContextMenuItems"
)
here is my renderer:
import { Component, OnInit } from '#angular/core';
import {ICellRendererAngularComp} from 'ag-grid-angular';
import {AlarmInstance} from "../../../../../../../lib/models/alarm-instance/alarm-instance";
#Component({
selector: 'vfms-alarm-instance-subtable-renderer',
templateUrl: './alarm-instance-subtable-renderer.component.html',
styleUrls: ['./alarm-instance-subtable-renderer.component.styl']
})
export class AlarmInstanceSubtableRendererComponent implements OnInit,ICellRendererAngularComp {
alarmInstance: AlarmInstance;
constructor() { }
ngOnInit() {
}
refresh(params: any): boolean {
return false;
}
agInit(params: any): void {
this.alarmInstance = params.data
}
}
refresh() does not make a difference whether true or false. In fact it is never called.
Add the following to your grid definition:
[rememberGroupStateWhenNewData] = true
https://www.ag-grid.com/javascript-grid-grouping/#keeping-group-state
I am having much difficulties implementing RadListView in my application. According to documentation the ListView accepts items as an ObservableArray, but regardless of whether that or plain array is used, items are not being displayed. I prepared a sample app in the Playground
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=11
Component is like this:
import { Component, ViewChild, OnInit, ChangeDetectorRef, ElementRef } from "#angular/core";
import { GestureTypes, PanGestureEventData, TouchGestureEventData } from "tns-core-modules/ui/gestures";
import { ObservableArray } from "tns-core-modules/data/observable-array";
#Component({
selector: "ns-app",
moduleId: module.id,
templateUrl: "./app.component.html",
})
export class AppComponent implements OnInit {
public gridData = []; //new ObservableArray<any>();
constructor(
private cd: ChangeDetectorRef
) {
this.feedTestData();
}
ngOnInit() {
}
ngAfterViewInit() {
}
public detectChanges() {
this.cd.detectChanges();
}
private feedTestData() {
let data = [
{
description: 'line 1',
},
{
description: 'line 2',
},
];
this.gridData.splice(0, 0, data);
}
}
and template like this:
<GridLayout tkExampleTitle tkToggleNavButton>
<RadListView [items]="gridData">
<ng-template tkListItemTemplate let-item="item">
<StackLayout orientation="vertical">
<Label [text]="description"></Label>
</StackLayout>
</ng-template>
</RadListView>
</GridLayout>
What's perplexing me even more is that in my real application RadListView displays something, but does it on its own, completely ignoring my template. I can literally leave nothing inside RadListView, but it would still display items
I have updated your playground and it is working now.
https://play.nativescript.org/?template=play-ng&id=39xE2X&v=12
You were trying to access the description in ng-template while it should be item.description
<Label [text]="item.description"></Label>
Also for the testing purpose, I am creating an Onservable array from your data. this.gridData = new ObservableArray(data); in your feedTestData function.
I want to create a custom form input component in ionic2, by extending BaseInput. But it doesn't rendered, and I can't find it on the DOM.
import { Component, ElementRef, OnDestroy, Optional, Renderer,
ViewEncapsulation } from "#angular/core";
import { Config, Form, Item } from "ionic-angular";
import { BaseInput } from "ionic-angular/util/base-input";
import { NG_VALUE_ACCESSOR } from "#angular/forms";
#Component({
selector: 'my-checkbox',
template:
'<p>aaaaa</p>',
host: {
'[class.checkbox-disabled]': '_disabled'
},
providers: [ { provide: NG_VALUE_ACCESSOR, useExisting: MyCheckboxComponent, multi: true } ],
encapsulation: ViewEncapsulation.None,
})
export class MyCheckboxComponent extends BaseInput<any> implements OnDestroy {
constructor(form: Form, config: Config, elementRef: ElementRef, renderer: Renderer, #Optional() item: Item) {
super(config, elementRef, renderer, 'my-checkbox', [], form, item, null);
}
}
The code is copy from src/component/checkbox/checkbox.ts and make a little changes.
I had the same problem. My component did not get render within <ion-item> parent element. I fixed it by adding item-content directive
<ion-item>
<ion-label>Label</ion-label>
<my-checkbox item-content></my-checkbox>
</ion-item>
I don't seem to be able to set focus on a input field in dynamically added FormGroup:
addNewRow(){
(<FormArray>this.modalForm.get('group1')).push(this.makeNewRow());
// here I would like to set a focus to the first input field
// say, it is named 'textField'
// but <FormControl> nor [<AbstractControl>][1] dont seem to provide
// either a method to set focus or to access the native element
// to act upon
}
How do I set focus to angular2 FormControl or AbstractControl?
I made this post back in December 2016, Angular has progressed significantly since then, so I'd make sure from other sources that this is still a legitimate way of doing things
You cannot set to a FormControl or AbstractControl, since they aren't DOM elements. What you'd need to do is have an element reference to them, somehow, and call .focus() on that. You can achieve this through ViewChildren (of which the API docs are non-existent currently, 2016-12-16).
In your component class:
import { ElementRef, ViewChildren } from '#angular/core';
// ...imports and such
class MyComponent {
// other variables
#ViewChildren('formRow') rows: ElementRef;
// ...other code
addNewRow() {
// other stuff for adding a row
this.rows.first().nativeElement.focus();
}
}
If you wanted to focus on the last child...this.rows.last().nativeElement.focus()
And in your template something like:
<div #formRow *ngFor="let row in rows">
<!-- form row stuff -->
</div>
EDIT:
I actually found a CodePen of someone doing what you're looking for https://codepen.io/souldreamer/pen/QydMNG
For Angular 5, combining all of the above answers as follows:
Component relevant code:
import { AfterViewInit, QueryList, ViewChildren, OnDestroy } from '#angular/core';
import { Subscription } from 'rxjs/Subscription';
// .. other imports
export class MyComp implements AfterViewInit, OnDestroy {
#ViewChildren('input') rows: QueryList<any>;
private sub1:Subscription = new Subscription();
//other variables ..
// changes to rows only happen after this lifecycle event so you need
// to subscribe to the changes made in rows.
// This subscription is to avoid memory leaks
ngAfterViewInit() {
this.sub1 = this.rows.changes.subscribe(resp => {
if (this.rows.length > 1){
this.rows.last.nativeElement.focus();
}
});
}
//memory leak avoidance
ngOnDestroy(){
this.sub1.unsubscribe();
}
//add a new input to the page
addInput() {
const formArray = this.form.get('inputs') as FormArray;
formArray.push(
new FormGroup(
{input: new FormControl(null, [Validators.required])}
));
return true;
}
// need for dynamic adds of elements to re
//focus may not be needed by others
trackByFn(index:any, item:any){
return index;
}
The Template logic Looks like this:
<div formArrayName="inputs" class="col-md-6 col-12"
*ngFor="let inputCtrl of form.get('phones').controls;
let i=index; trackBy:trackByFn">
<div [formGroupName]="i">
<input #input type="text" class="phone"
(blur)="addRecord()"
formControlName="input" />
</div>
</div>
In my template I add a record on blur, but you can just as easily set up a button to dynamically add the next input field. The important part is that with this code, the new element gets the focus as desired.
Let me know what you think
This is the safe method recommend by angular
#Component({
selector: 'my-comp',
template: `
<input #myInput type="text" />
<div> Some other content </div>
`
})
export class MyComp implements AfterViewInit {
#ViewChild('myInput') input: ElementRef;
constructor(private renderer: Renderer) {}
ngAfterViewInit() {
this.renderer.invokeElementMethod(this.input.nativeElement,
'focus');
}
}
With angular 13, I did it this way:
import { Component, OnInit, Input } from '#angular/core';
import { FormGroup, Validators, FormControl, FormControlDirective, FormControlName } from '#angular/forms';
// This setting is required
const originFormControlNgOnChanges = FormControlDirective.prototype.ngOnChanges;
FormControlDirective.prototype.ngOnChanges = function ()
{
this.form.nativeElement = this.valueAccessor._elementRef.nativeElement;
return originFormControlNgOnChanges.apply(this, arguments);
};
const originFormControlNameNgOnChanges = FormControlName.prototype.ngOnChanges;
FormControlName.prototype.ngOnChanges = function ()
{
const result = originFormControlNameNgOnChanges.apply(this, arguments);
this.control.nativeElement = this.valueAccessor._elementRef.nativeElement;
return result;
};
#Component({
selector: 'app-prog-fields',
templateUrl: './prog-fields.component.html',
styleUrls: ['./prog-fields.component.scss']
})
export class ProgFieldsComponent implements OnInit
{
...
generateControls()
{
let ctrlsForm = {};
this.fields.forEach(elem =>
{
ctrlsForm[elem.key] = new FormControl(this.getDefaultValue(elem), this.getValidators(elem));
});
this.formGroup = new FormGroup(ctrlsForm);
}
...
validateAndFocus()
{
if (formGroup.Invalid)
{
let stopLoop = false;
Object.keys(formGroup.controls).map(KEY =>
{
if (!stopLoop && formGroup.controls[KEY].invalid)
{
(<any>formGroup.get(KEY)).nativeElement.focus();
stopLoop = true;
}
});
alert("Warn", "Form invalid");
return;
}
}
}
Reference:
https://stackblitz.com/edit/focus-using-formcontrolname-as-selector?file=src%2Fapp%2Fapp.component.ts
Per the #Swiggels comment above, his solution for an element of id "input", using his solution after callback:
this.renderer.selectRootElement('#input').focus();
worked perfectly in Angular 12 for an element statically defined in the HTML (which is admittedly different somewhat from the OP's question).
TS:
#ViewChild('licenseIdCode') licenseIdCodeElement: ElementRef;
// do something and in callback
...
this.notifyService.info("License Updated.", "Done.");
this.renderer.selectRootElement('#licenseIdCode').focus();
HTML:
<input class="col-3" id="licenseIdCode" type="text" formControlName="licenseIdCode"
autocomplete="off" size="40" />
If you are using Angular Material and your <input> is a matInput, you can avoid using .nativeElement and ngAfterViewInit() as follows:
Component Class
import { ChangeDetectorRef, QueryList, ViewChildren } from '#angular/core';
import { MatInput } from '#angular/material/input';
// more imports...
class MyComponent {
// other variables
#ViewChildren('theInput') theInputs: QueryList<MatInput>;
constructor(
private cdRef: ChangeDetectorRef,
) { }
// ...other code
addInputToFormArray() {
// Code for pushing an input to a FormArray
// Force Angular to update the DOM before proceeding.
this.cdRef.detectChanges();
// Use the matInput's focus() method
this.theInputs.last.focus();
}
}
Component Template
<ng-container *ngFor="iterateThroughYourFormArrayHere">
<input #theInput="matInput" type="text" matInput>
</ng-container>
I can imagine following approaches to exchange Data between multi step forms:
1) Create a component for each form step and exchange data between components over #input, #output (e.g. you cannot change from step5 to 2)
2) Use the new property data in the new router (see here) (e.g. you cannot change from step5 to 2))
3) A shared Service (Dependency Injection) to store data (Component Interaction) (e.g. you can change from step5 to 2)
4) New rudiments with #ngrx/store (not really experienced yet)
Can you give some "gained experience values", what do you use and why?
See my edit below.
Using SessionStorage is not strictly the 'angular' way to approach this in my opinion—a shared service is the way to go. Implementing routing between steps would be even better (as each component can have its own form and different logic as you see fit:
const multistepRoutes: Routes = [
{
path: 'multistep',
component: MultistepComponent,
children: [
{
path: '',
component: MultistepBaseComponent,
},
{
path: 'step1',
component: MultistepStep1Component
},
{
path: 'step2',
component: MultistepStep2Component
}
]
}
];
The service multistep.service can hold the model and implement logic for components:
import { Injectable, Inject } from '#angular/core';
import { Router } from '#angular/router';
#Injectable()
export class MultistepService {
public model = {};
public baseRoute = '/multistep';
public steps = [
'step1',
'step2'
];
constructor (
#Inject(Router) public router: Router) { };
public getInitialStep() {
this.router.navigate([this.baseRoute + '/' + this.steps[0]]);
};
public goToNextStep (direction /* pass 'forward' or 'backward' to service from view */): any {
let stepIndex = this.steps.indexOf(this.router.url.split('/')[2]);
if (stepIndex === -1 || stepIndex === this.steps.length) return;
this.router.navigate([this.baseRoute + '/' + this.steps[stepIndex + (direction === 'forward' ? 1 : -1)]]);
};
};
Good luck.
EDIT 12/6/2016
Actually, now having worked with the form API for a while I don't believe my previous answer is the best way to achieve this.
A preferrable approach is to create a top level FormGroup which has each step in your multistep form as it's own FormControl (either a FormGroup or a FormArray) under it's controls property. The top level form in such a case would be the single-source of truth for the form's state, and each step on creation (ngOnInit / constructor) would be able to read data for its respective step from the top level FormGroup. See the pseudocode:
const topLevelFormGroup = new FormGroup({
step1: new FormGroup({fieldForStepOne: new FormControl('')}),
step2: new FormGroup({fieldForStepTwo}),
// ...
});
...
// Step1Component
class Step1Component {
private stepName: string = 'step1';
private formGroup: FormGroup;
constructor(private topLevelFormGroup: any /* DI */) {
this.formGroup = topLevelFormGroup.controls[this.stepName];
}
}
Therefore, the state of the form and each step is kept exactly where it should be—in the form itself!
Why not use session storage? For instance you can use this static helper class (TypeScript):
export class Session {
static set(key:string, value:any) {
window.sessionStorage.setItem(key, JSON.stringify(value));
}
static get(key:string) {
if(Session.has(key)) return JSON.parse(window.sessionStorage[key])
return null;
}
static has(key:string) {
if(window.sessionStorage[key]) return true;
return false;
}
static remove(key:string) {
Session.set(key,JSON.stringify(null)); // this line is only for IE11 (problems with sessionStorage.removeItem)
window.sessionStorage.removeItem(key);
}
}
And using above class, you can put your object with multi-steps-forms data and share it (idea is similar like for 'session helper' in many backend frameworks like e.g. php laravel).
The other approach is to create Singleton service. It can look like that (in very simple from for sake of clarity) (I not test below code, I do it from head):
import { Injectable } from '#angular/core';
#Injectable()
export class SessionService {
_session = {};
set(key:string, value:any) {
this._session[key]= value; // You can also json-ize 'value' here
}
get(key:string) {
return this._session[key]; // optionally de-json-ize here
}
has(key:string) {
if(this.get(key)) return true;
return false;
}
remove(key:string) {
this._session[key]=null;
}
}
And then in your main file where you bootstrap application:
...
return bootstrap(App, [
...
SessionService
])
...
And the last step - critical: When you want to use you singleton service in your component - don't put int in providers section (this is due to angular2 DI behavior - read above link about singleton services). Example below for go from form step 2 to step 3:
import {Component} from '#angular/core';
import {SessionService} from './sessionService.service';
...
#Component({
selector: 'my-form-step-2',
// NO 'providers: [ SessionService ]' due to Angular DI behavior for singletons
template: require('./my-form-step-2.html'),
})
export class MyFormStep2 {
_formData = null;
constructor(private _SessionService: SessionService) {
this._formData = this._SessionService.get('my-form-data')
}
...
submit() {
this._SessionService.set('my-form-data', this._formData)
}
}
It should looks like that.