I created simple custom input eu-input element using 'ControlValueAccessor'. Now I have something like that:
<eu-input></eu-input>
<div *ngFor="let item of items" (click)="choose(item)">
<span>{{item.name}}</span>
</div>
But there is problem I can't figure out. When eu-input is focused, click event doesn't get fired, it's fired only after eu-input is blured (on second attempt).
so what could be a problem?
this is html:
<input [(ngModel)]="value"/>local: {{val}}
and this is ts file:
import { Component, OnInit, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'eu-input',
templateUrl: './eu-input.component.html',
styleUrls: ['./eu-input.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => EuInputComponent),
multi: true,
},
],
})
export class EuInputComponent implements OnInit, ControlValueAccessor {
constructor() {}
ngOnInit(): void {}
onChange: any = () => {};
onTouch: any = () => {};
val = '';
set value(val) {
if (val !== undefined && this.val !== val) {
this.val = val;
this.onChange(val);
this.onTouch(val);
}
}
writeValue(value: any) {
this.value = value;
}
registerOnChange(fn: any) {
this.onChange = fn;
}
registerOnTouched(fn: any) {
this.onTouch = fn;
}
}
choose() method is just console.log('choose clicked')
Change eu-input component html to
<input [(ngModel)]="value" (focus)="emitFocused(true)" (blur)="emitFocused(false)"/>local: {{val}}
In eu-input TypeScript
#Output() focus = new EventEmitter();
public emitFocused(value) {
this.focus.emit(value);
}
Can be used like
<eu-input (focus)="myMethod($event)"></eu-input>
Related
I'm not sure how can I use a custom component if it's wrapper under another component.
Like:
ComponentA_withForm
|
--ComponentA1_withWrapperOfCustomInput
|
--ComponentA11_withCustomInput
if I have a structure like this:
ComponentA_withForm
|
--ComponentA11_withCustomInput
Everything's fine
But for my case (tons of async data) I need a wrapper... Is it possible somehow to do this?
Here is my fiddle code:
ComponentA:
import { Component } from '#angular/core';
import { FormBuilder } from '#angular/forms';
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
ComponentA1:
import { Component } from '#angular/core';
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input></custom-input>',
})
export class CustomInputWrapperComponent {
constructor() { }
}
ComponentA11:
import { Component, forwardRef } from '#angular/core';
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '#angular/forms';
#Component({
selector: 'custom-input',
template: `Hey there! <button (click)="inc()">Value: {{ value }}</button>`,
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputComponent),
multi: true,
}],
})
export class CustomInputComponent implements ControlValueAccessor {
private value = 0;
writeValue(value: number): void {
this.value = value;
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
inc() {
this.value = this.value + 1;
this.onChangeFn(this.value);
}
onChangeFn = (_: any) => { };
}
And here I have a working sample:
https://stackblitz.com/edit/angular-qmrj3a
so: basically removing & refactoring code not to use CustomInputWrapperComponent makes my code working. But I need this wrapper and I'm not sure how to pass formControlName then.
I don't want a dirty solution with passing parent formGroup :)
Since you don't want a dirty solution ;) , you could just implement ControlValueAccessor in the CustomInputWrapperComponent also. That way any change in the parent will be reflected in the child, any change in the child will be reflected in the parent as well with just few lines of code.
Wrapper Component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="value"></custom-input>',
providers: [{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => CustomInputWrapperComponent),
multi: true,
}]
})
export class CustomInputWrapperComponent implements AfterViewInit, ControlValueAccessor {
public value = new FormControl();
constructor() { }
ngAfterViewInit() {
this.value.valueChanges.subscribe((x) => {
this.onChangeFn(x);
});
}
writeValue(value: number): void {
this.value.setValue(value);
}
registerOnChange(fn: (_: any) => void): void {
this.onChangeFn = fn;
}
registerOnTouched(fn: any): void {
}
onChangeFn = (_: any) => { };
}
Parent Template
<form [formGroup]="form"><custom-input-wrapper formControlName="someInput"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>
I have made a stackbitz demo here - https://stackblitz.com/edit/angular-csaxcz
you cannot use formControlName on custom-input-wrapper because it doesn't implement ControlValueAccessor. implementing ControlValueAccessor on custom-input-wrapper might be a solution but it seems to be overkill. Instead pass the control from formGroup to custom-input-wrapper as an #Input() and pass the inputed formControl to custom-input
app.component
#Component({
selector: 'my-app',
template: `<form [formGroup]="form"><custom-input-wrapper [formCtrl]="form.get('someInput')"></custom-input-wrapper></form> <p>value is: {{formVal | json}}</p>`
})
export class AppComponent {
form = this.fb.group({
someInput: [],
});
get formVal() {
return this.form.getRawValue();
}
constructor(private fb: FormBuilder) { }
}
custom-input-wrapper.component
#Component({
selector: 'custom-input-wrapper',
template: '<custom-input [formControl]="formCtrl"></custom-input>',
})
export class CustomInputWrapperComponent {
#Input() formCtrl: AbstractControl;
constructor() { }
}
here is a working demo https://stackblitz.com/edit/angular-3lrfqv
I have a custom text-area component, with text-area input inside. I have created a custom validator to check the max length (not the html one) of the text.
All work fine, the problem is that the inner input is set to invalid (with ng-invalid) while che component itself don't and so also the form that contains the component remains valid.
It's seems to work with the built-it required validator, placed on both the component and the input.
How can I make the changes in a custom input to be reflected on the external form?
Thanks!
//sorry for my english!
Edit: I made a plunker: https://plnkr.co/edit/NHc25bo8K9OsgcxSWyds?p=preview
This is the custom text-area component html:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
<span *ngIf="input.errors && (input.dirty || input.touched)">
<span *ngIf="input.errors?.required" class="error-message">Required!</span>
<span *ngIf="input.errors?.customMaxLength" class="error-message">Max Length Reached({{maxLength}})</span>
</span>
This is the code of the component
import { Component, Input, forwardRef, ViewChild } from '#angular/core';
import { NgModel, ControlValueAccessor, NG_VALUE_ACCESSOR, AbstractControl } from '#angular/forms';
#Component({
selector: 'custom-text-area',
templateUrl: './custom-text-area.component.html',
styleUrls: ['./custom-text-area.component.less'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: forwardRef(() => TextAreaComponent),
multi: true
}
]
})
export class TextAreaComponent implements ControlValueAccessor{
#Input() required = false;
#Input() name;
#Input() data;
#Input() disabled;
#Input() placeholder = '';
#Input() errorMsg;
#Input() maxLength = null;
#ViewChild('input') input: NgModel;
constructor() {
this.data = '';
}
propagateChange = (_: any) => {};
writeValue(value: any): void {
if (value !== undefined) {
this.data = value;
} else {
this.data = '';
}
}
registerOnChange(fn: any): void {
this.propagateChange = fn;
}
registerOnTouched(fn: any): void {}
setDisabledState?(isDisabled: boolean): void {
this.disabled = isDisabled;
}
onInput(e) {
this.data = e.target.value || '';
this.propagateChange(this.data);
}
}
This is the validator
import { NG_VALIDATORS, Validator, FormControl } from '#angular/forms';
import { Directive, forwardRef, Input } from '#angular/core';
#Directive({
selector: '[customMaxLength][ngModel]',
providers: [
{ provide: NG_VALIDATORS, useExisting: forwardRef(() => MaxLengthValidatorDirective), multi: true}
]
})
export class MaxLengthValidatorDirective implements Validator{
#Input() customMaxLength: number;
ngOnInit(){
}
validate(c: FormControl): { [key: string]: any; } {
if(c.value && this.customMaxLength){
return c.value.length < this.customMaxLength ? null : { customMaxLength:{ valid: false } };
} else {
return null;
}
}
}
Aaand finally this is an use:
<form #form="ngForm">
<custom-text-area [maxLength]="3" required ngModel name="textArea"></custom-text-area>
</form>
The main problem is how you are using the NgModel. You are using it in both the custom component and inside your form. You should only be using it inside of your form. Meaning, textarea should not have an NgModel.
No:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[(ngModel)] = "data"
[name]="name"
#input="ngModel"
customMaxLength="{{maxLength}}"
></textarea>
Yes:
<textarea
[disabled]='disabled'
[required]='required'
[placeholder]="placeholder"
(changes)="onInput($event)"
(keyup)="onInput($event)"
[name]="name"
customMaxLength="{{maxLength}}"
></textarea>
Here is a working example:
https://plnkr.co/edit/lWZpEpPdnfG7YDiH21jB?p=preview
I'm trying to create a custom component that groups some Radio Button this way
<group>
<radio></radio>
<radio></radio>
<radio></radio>
</group
Inside of the component I add dynamically a name for the inputs, so they all have the same name and change the selected one when I click on another.
It works good if I only have one component, if i have more than one, it extends the values like if it were only a group of RadioButtons with only one name.
This is the code I'm using:
import {AfterViewInit, Component, ContentChildren, ElementRef, Input, NgModule, QueryList} from "#angular/core";
import {CommonModule} from "#angular/common";
import {ControlValueComponent} from "../shared/ControlValueComponent";
import {SysSharedModule} from "../shared/SysSharedModule";
#Component({
selector: 'sys-radio-button',
styleUrls: ['sysRadioButton.css', '../shared/sys.css'],
providers: ControlValueComponent.providerValueAcessor(SysRadioButton),
template: `
<input type="radio" id="rb{{randomId}}" [value]="val" [(ngModel)]="value">
<label for="rb{{randomId}}">{{label}}</label>
`
})
export class SysRadioButton extends ControlValueComponent {
constructor (public elem: ElementRef) {
super();
}
#Input() groupName = 'radiobutton';
#Input() val: any;
#Input() label: string;
randomId = (Math.floor(Math.random() * (1 - 10000 + 1)) + 1) * -1;
}
#Component({
selector: 'sys-radio-group',
styleUrls: ['sysRadioButton.css', '../shared/sys.css'],
providers: ControlValueComponent.providerValueAcessor(SysRadioGroup),
template: `
<div class="t{{tam}}">
<label class="header">{{header}}</label>
<div class="radioButtonContainer"></div>
</div>
`
})
export class SysRadioGroup extends ControlValueComponent implements AfterViewInit {
#Input() name: string;
#Input() header: string;
#Input() tam = '3-of-10';
#ContentChildren(SysRadioButton) radioButtons: QueryList<SysRadioButton>;
constructor (public elem: ElementRef) {
super();
}
ngAfterViewInit() {
this.addNameToInputs();
}
addNameToInputs() {
const container = this.elem.nativeElement.getElementsByClassName('radioButtonContainer')[0];
this.radioButtons.forEach(item => {
const input = item.elem.nativeElement;
input.getElementsByTagName('input')[0].name = this.name;
container.appendChild(input);
});
}
}
#NgModule({
imports: [CommonModule, SysSharedModule],
declarations: [SysRadioButton, SysRadioGroup],
exports: [SysRadioButton, SysRadioGroup]
})
export class SysRadioButtonModule {
}
And i use it like this:
<sys-radio-group header="Select your destiny" name="name1">
<sys-radio-button val="hola1" label="Label 1"></sys-radio-button>
<sys-radio-button val="hola2" label="Label 2"></sys-radio-button>
<sys-radio-button val="hola3" label="Label 3"></sys-radio-button>
<sys-radio-button val="hola4" label="Label 4"></sys-radio-button>
</sys-radio-group>
<sys-radio-group header="Select your destiny" name="name2">
<sys-radio-button val="hola1" label="Label 1"></sys-radio-button>
<sys-radio-button val="hola2" label="Label 2"></sys-radio-button>
<sys-radio-button val="hola3" label="Label 3"></sys-radio-button>
<sys-radio-button val="hola4" label="Label 4"></sys-radio-button>
</sys-radio-group>
Here are some images of how it works
This is how it is when i don't click on anything
and this is how it looks when i click on one with the same value but a different name
If i check the elements in the chrome's console, i can see how the name's are different, so i don't understand why this is happening
EDIT
The ControlValueComponent class that extends the main classes,is just the one for the custom form. This is the code:
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "#angular/forms";
import {forwardRef, Input} from "#angular/core";
export class ControlValueComponent implements ControlValueAccessor {
#Input() disabled: boolean;
innerValue: any = '';
static providerValueAcessor( ref: any): any {
return [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ref), multi: true }
];
}
onTouchedCallback: () => void = () => {};
onChangeCallback: (_: any) => void = () => {};
constructor() {
}
get value(): any {
return this.innerValue;
}
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any): void {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedCallback = fn;
}
}
So here is where the "value" variable of the [(ngModel)] comes from
change ngModel to :
[(ngModel)]="val"
This question already has answers here:
Can't bind to 'ngModel' since it isn't a known property of 'input'
(48 answers)
Closed 5 years ago.
So, i'm trying to make a custom form component in angular 4, i added everything needed for the ngModel to work but it doesnt.
This is my child component:
export class Search extends ControlValueComponent {
}
It extends to the class ControlValueComponent that handles all the "ControlValueAccesor".
This is the class ControlValueAccesor:
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "#angular/forms";
import {forwardRef, Input} from "#angular/core";
export class ControlValueComponent implements ControlValueAccessor {
#Input() disabled: boolean;
innerValue: any = '';
static providerValueAcessor( ref: any): any {
return [
{ provide: NG_VALUE_ACCESSOR, useExisting: forwardRef(() => ref), multi: true }
];
}
onTouchedCallback: () => void = () => {};
onChangeCallback: (_: any) => void = () => {};
constructor() {
}
get value(): any {
return this.innerValue;
}
set value(v: any) {
if (v !== this.innerValue) {
this.innerValue = v;
this.onChangeCallback(v);
}
}
writeValue(value: any) {
if (value !== this.innerValue) {
this.innerValue = value;
}
}
registerOnChange(fn: any): void {
this.onChangeCallback = fn;
}
registerOnTouched(fn: any): void {
this.onTouchedCallback = fn;
}
}
And in order to have the provider right i use the function ProviderValueAccesor (that is in the ControlValueComponent) like this
providers: ControlValueComponent.providerValueAcessor(SysSearch)
But when i add to my child component the [(ngModel)] it keeps getting the error that ngModel is not a property of the component
I try to use it like this:
<search [(ngModel)] = 'value'><search>
I already fixed the problem.
I needed to import FormsModule directly on the parent component that calls the child component in order for the ngModel to work... I did it like this:
import {FormsModule} from "#angular/forms";
#NgModule({
imports: [
FormsModule,
SearchModule
]
This way, the father component is able to use the ngmodel property. Thanks for the help
import FormsModule in your class file:
import {ControlValueAccessor, NG_VALUE_ACCESSOR} from "#angular/forms";
---->
import {ControlValueAccessor, NG_VALUE_ACCESSOR, FormsModule} from "#angular/forms";
Lets say I have a component
const DEFAULT_VALUE_ACCESSOR = CONST_EXPR(new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => MyComp), multi: true}));
#Component({
selector: 'my-comp',
template: `
<another-one></another-one>
`,
providers: [DEFAULT_VALUE_ACCESSOR]
and now when I use the component like:
#Component({
selector: 'app',
template: `<my-comp [(ngModel)]="someValue"></my-comp>
<input [(ngModel)]="someValue" />`
})
class App {
someValue: number = 5
}
The value is passed to the <my-comp> component, however after that when the value is changed inside the input, the ngModel is updated, but not the <my-comp>. Is there something else that I need to configure?
In fact, you need to explicitly call the onChange callback that is registered by Angular2:
#Component({
(...)
})
export class MyComp implements ControlValueAccessor {
onChange = (_) => {};
onTouched = () => {};
writeValue(value: any): void {
(...)
}
registerOnChange(fn: (_: any) => void): void { this.onChange = fn; }
registerOnTouched(fn: () => void): void { this.onTouched = fn; }
}
For example when you consider that the state of this custom component changes. Here is a sample on a click:
#Component({
(...)
template: `
<div (click)="updateState()">Update state</div>
`
})
export class MyComp implements ControlValueAccessor {
writeValue(value: any): void {
this.internalState = value;
}
updateState() {
this.internalState = 'new state';
this.onChange(this.internalState);
}
}
This article could give you more hints (see section "NgModel-compatible component"):
http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/
ngOnChanges() is called when #Input()s values change:
#Component({
selector: 'my-comp',
template: `
<another-one></another-one>
`
})
class MyComp {
#Input() someField:number;
ngOnChanges(changes) {
console.log(changes);
}
}
and now when I use the component like:
#Component({
selector: 'app',
template: `<my-comp [someField]="someValue"></my-comp>
<input [(ngModel)]="someValue" />`
})
class App {
someValue: number = 5
}