RadioButtons acting strange Angular 4 - javascript

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"

Related

Angular does not rerender on Input() change

Whatever i do angular does not detect change on talks array. I have a handleSubmit function to send the toolbar. Toolbar use it to send the changes to parent from input field.
My app.component.ts file
import { Component, Type, OnChanges, SimpleChanges } from '#angular/core';
import { getResponse } from '../api/API';
declare module '../api/API' {
export interface NlpAPI {
getResponse(data: any): Promise<any>;
}
}
#Component({
selector: 'app-root',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css'],
})
export class AppComponent implements OnChanges {
talks: string[];
title: string;
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
constructor() {
this.talks = [];
this.title = 'Talks';
}
ngOnInit() {
this.talks.push('Welcome to ProjectX! How can I help you?');
this.talks.push('I am a chatbot. I can help you with your queries.');
}
handleSubmit(data: any): void {
this.talks.push(data.talk);
}
messageResponse() {
// #ts-ignore: Object is possibly 'null'.
const x = document.getElementById('txt').value;
// #ts-ignore: Object is possibly 'null'.
document.getElementById('output').innerHTML =
'Your message is ' + '"' + x + '"';
}
}
My app.component.html
<!-- Toolbar -->
<app-custom-toolbar [handleSubmit]="handleSubmit"></app-custom-toolbar>
<!-- Highlight Card -->
<app-text-area [talksFromUser]="talks" [title]="title"></app-text-area>
<!-- Bottombar -->
<router-outlet></router-outlet>
My text-area.component.ts file
import { Component, Input, OnChanges, SimpleChanges } from '#angular/core';
#Component({
selector: 'app-text-area',
templateUrl: './text-area.component.html',
styleUrls: ['./text-area.component.css'],
})
export class TextAreaComponent implements OnChanges {
#Input() talksFromUser: string[] = [];
#Input() title: string = '';
constructor() {}
ngOnChanges(changes: SimpleChanges): void {
console.log(changes);
}
}
My text-area.component.html
<div class="main-text-area">
<div *ngFor="let item of talksFromUser">{{ item }}</div>
</div>
custom-toolbar.component.ts file
import { Component, Input, OnInit } from '#angular/core';
import { NgForm } from '#angular/forms';
#Component({
selector: 'app-custom-toolbar',
templateUrl: './custom-toolbar.component.html',
styleUrls: ['./custom-toolbar.component.css'],
})
export class CustomToolbarComponent implements OnInit {
talks: string[] = [];
#Input() handleSubmit!: (args: any) => void;
constructor() {}
ngOnInit(): void {}
onSubmit(f: NgForm) {
this.handleSubmit(f.value);
f.resetForm();
}
}
I tried also
this.talks = [...this.talks, data.talk]
Thank you all.
There are two issues in your code:
First one, you are calling handleSubmit("string") (so data is a string), but you are pushing data.talk, which is undefined (so talks will be [undefined, undefined, ...]). To fix it, use data:
handleSubmit(data: any): void {
this.talks.push(data); // use "data" instead of "data.talk"
}
Second one, you are using a AppComponent method into CustomToolbarComponent class. You need to keep the this scope of AppComponent. Also, you should use arrow functions:
handleSubmit = (data: any): void => {
this.talks.push(data);
}

Angular click event not fired when custom input is focused

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>

Angular: custom input with ControlValueAccessor

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

validation not propagated with custom input component - Angular 4

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

Cannot bind ngModel since is not a property of "Component" [duplicate]

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";

Categories

Resources