How to get checked values into an array for Angula

2019-08-29 19:05发布

问题:

I'm attempting to use template driven forms, binding to a model in the html to a new instance of lets say Person. I've been unsuccessful in creating a proper binding for checkboxes to a single array property on my model.

The idea is data will come from an api or other source, dynamically render checkboxes via *ngFor and bind what is selected to the Person models property which would be an array of numbers. For example:

class Person {
  firstName: string;
  someCheckboxPropList: number[];
}

and data could be anything really

const dynamicData = [
  { name: 'name1', value: 1 },
  { name: 'name2', value: 2 }
];

my expected output would be something along the lines of [ 1, 2 ] if both values where to be checked and if only the second was checked [ 2 ].

Here's a sample of what the PersonComponent.ts file might look like

@Component({ ... })
export class PersonComponent {

    submitted = false;

    model = new Person();

    constructor() { }

    onSubmit(form) {
        this.submitted = true;
        console.log(form);
    }

}

And where I'm at with the components html file.

<form (ngSubmit)="onSubmit(form)" #form="ngForm">

    <input type="text" [(ngModel)] name="person.firstName">

    <div *ngFor="let dataItem of dynamicData" >
        <input 
            type="checkbox"
            ngModel
            name="dynamicData"
            [value]="dataItem.value">
        <label>{{dataItem.name}}</label>
    </div>

</form>

This does not work (and is sample code anyway).

回答1:

the idea is have two things: Person and PersonForm, so,e.g

person={firstName:"Jessy",props:[1,2]
//but 
personForm={firstName:"Jessy",props:[true,true]

So, make two functions

  createForm(person) {
    return {
      firstName: person.firstName,
      props: this.dynamicData.map(x => person.props.indexOf(x.value) >= 0)
    }
  }
  retrieveData(personForm) {
    let props: number[] = [];
    personForm.props.forEach((v, index) => {
      if (v)
        props.push(this.dynamicData[index].value)
    }
    )
    return {
      firstName: personForm.firstName,
      props: props
    }
  }

Well, we have already all we need. When we received a person, create a personForm that it's the data we change in the form. In submit simply call to retrieveData to get the value of person.

When we have a person create a personForm,e.g.

this.service.getPerson().subscribe(person=>
    {
       this.personForm=createForm(person)
    }
)

Our form

<form *ngIf="personForm" (submit)="sendData(personForm)">
  <input name="firtName" [(ngModel)]="personForm.firstName">
  <div *ngFor="let item of dynamicData;let i=index">
  <input name="{{'prop'+i}}" 
         type="checkBox" [(ngModel)]="personForm.props[i]">{{item.name}}
  </div>
  <button>Submit</button>
</form>
{{personForm|json}}<br/>
{{retrieveData(personForm)|json}}

And our sendData function

sendData(personForm)
{
    console.log(this.retrieveData(personForm))
}

I make a simple stackblitz

Update

NOTE:We can use the spred operator to asing properties, so

  createForm(person) {
    return {
      ...person, //All the properties of person, but
      props: this.dynamicData.map(x => person.props.indexOf(x.value) >= 0)
    }
  }
  retrieveData(personForm) {
    let props: number[] = [];
    personForm.props.forEach((v, index) => {
      if (v)
        props.push(this.dynamicData[index].value)
    }
    )
    return {
      ..personForm, //all the properties of personForm, but
      props: props
    }
  }

NOTE2: In a "real world" the persons goes from a service. Consider the idea that service get/received the "personForm" and put the functions to transform in the service

//in service
getPerson()
{
    return this.httpClient.get("...").map(res=>this.createForm(res))
}
savePerson(personForm)
{
    return this.httpClient.post("...",this.retrieveData(personForm))
}


回答2:

If we want, we can make a custom Form control.

In this case we need as input, a source, and the cols of the source -the first will be the key, and the second one the text that appears.

I make a stackblitz

the .html will be

  <check-box-group name="props" [(ngModel)]="person.props"
      [source]="dynamicData" cols="value,name" >
  </check-box-group>

the component it's a tipical custom form control

@Component({
  selector: 'check-box-group',
  template: `
      <div class="form-check" *ngFor="let item of source;let i=index">
        <input class="form-check-input" id="{{_name+''+i}}"
             type="checkBox" [ngModel]="_selectedItems[i]"
             (ngModelChange)="setValue($event,i)">
        <label class="form-check-label" for="{{_name+''+i}}">
              {{item[_col]}}
        </label>
      </div>
  `,
  providers: [
    {
      provide: NG_VALUE_ACCESSOR,
      useExisting: forwardRef(() => CheckBoxGroupComponent),
      multi: true
    }
  ]
})
export class CheckBoxGroupComponent implements ControlValueAccessor {

  @Input() source;
  @Input()
  set cols(value:string){ //cols is a string separated by commas
                          //e.g. "value,text", the "key" will be "value" and show the text
    let _cols=value.split(',')
    this._key = _cols[0];
    this._col = _cols[1]
  }
  _selectedItems: any[] = [];
  _key: string;
  _col: string;
  _name:string="";
  onChange;
  onTouched;

  constructor(el:ElementRef) { 
    let name=el.nativeElement.getAttribute('name')
    this._name=name?name:"ck";
  }
  writeValue(value: any[]): void {
    this._selectedItems = this.propsToBoolean(value);
  }

  registerOnChange(fn: any): void {
    this.onChange = fn;
  }

  registerOnTouched(fn: any): void {
    this.onTouched = fn;
  }

  setDisabledState(isDisabled: boolean): void {
  }
  setValue(value: boolean, index: number) {
    this._selectedItems[index] = value;
    this.onChange(this.booleanToProps(this._selectedItems));

  }

  propsToBoolean(props): any[] {
    console.log(props);
    return props ? this.source.map((x: any) => props.indexOf(x[this._key]) >= 0)
      : this.source.map(x => false);

  }
  booleanToProps(propsBoolean: boolean[]) {
    let props: any[] = [];
    if (propsBoolean) {
      propsBoolean.forEach((item, index) => {
        if (item)
          props.push(this.source[index][this._key])
      })
    }
    return props;

  }

}

Update: add validations

when we have a custom form component and we want to make a "validation" we have two options, make the validation outside the component or make a validation inside the component. For the second option we must to add as provider provide: NG_VALIDATORS,,

{
  provide: NG_VALIDATORS,
  useExisting: forwardRef(() => CheckBoxGroupComponent),
  multi: true,
}

And add a function validate

validate(control: AbstractControl): ValidationErrors | null{
    ...your logic here.., e.g.
    if (!this._selectedItems.find(x=>x))
       return {error:"you must select one option at last"}

    return null
  }

Well, there're a more thing we must to do that is decide when our custom control are touched. Remember that a control is touched when lost the focus after received it. we can do it in a (blur) of our checkbox (or enclose the control in a div with tabindex=0)

 <input type="checkbox" .... (blur)="onTouched()">

The last step is make give an error or not is we add an attribute to the control. I like that if we add an attribute isRequired, check the error else not. So we add a new property _isRequired and, in contructor check if has the attribute

 constructor(el:ElementRef) { 
    let name=el.nativeElement.getAttribute('name');
    this._isRequired=el.nativeElement.getAttribute('isRequired')!=null?true:false;
    this._name=name?name:"ck"; //<--this is necesary for give value to
                               //for="..." in label
    }

And our validation take account of this

  validate(control: AbstractControl): ValidationErrors | null{
    if (!this._isRequired)
      return null;
    ....
  }

NOTE: I updated the custom control (and we add a propertie [customClass])