27

For some of my components I would like to change input field readonly and required attributes back and forth.

I've managed to get a running code, that changes both of them on demand, but problem is that it works for readonly, but seems not to be working on required: although element attribute changes to required Angular2 still thinks fieldCtrl is valid.

Here is my plunker, where I illustrated this problem: https://plnkr.co/edit/Yq2RDzUJjLPgReIgSBAO?p=preview

//our root app component
import {Component} from 'angular2/core'

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div>
    <form #f="ngForm">
      <button type="button" (click)="toggleReadOnly()">Change readonly!</button>
      <button type="button" (click)="toggleRequired()">Change required!</button>
      <input id="field" [(ngModel)]="field" ngControl="fieldCtrl" #fieldCtrl="ngForm"/>
      {{fieldCtrl.valid}}
    </form>
    </div>
  `,
  directives: []
})
export class App {
  constructor() {
    this.name = 'Angular2'
  }

  toggleRequired(){
    this.isRequired = !this.isRequired;
    var fieldElement = <HTMLInputElement>document.getElementById('field');
    if (this.isRequired){
      fieldElement.required = true;
      this.field = "it's required now";
    }
    else{
      fieldElement.required = false;
      this.field = "can leave it blank";
    }
  }

  toggleReadOnly(){
    this.isReadOnly = !this.isReadOnly;
    var fieldElement = <HTMLInputElement>document.getElementById('field');
    if (this.isReadOnly){
      fieldElement.readOnly = true;
      this.field = "it's readonly now";
    }
    else{
      fieldElement.readOnly = false;
      this.field = "feel free to edit";
    }
  }

  private isReadOnly:boolean=false;

  private field:string = "feel free to edit";

  private isRequired:boolean=false;

}

UPDATE: Tried suggested method

[required]="isRequired" [readonly]="isReadOnly"

And it works like a charm for readonly, and for required=true, but I can't turn the required attribute off anymore - it shows empty field is invalid allthough not required anymore.

Updated plunker: https://plnkr.co/edit/6LvMqFzXHaLlV8fHbdOE

UPDATE2: Tried suggested method

[required]="isRequired ? true : null"

It does add/remove required attribute from element by demand, however field controller's valid property shows false for empty field that is not required.

What would be correct way of changing required attribute in Angular2 Typescript?

Andris Krauze
  • 2,092
  • 8
  • 27
  • 39
  • 1
    For sure, you shouldn't be reaching into the DOM yourself. Have you tried `[readonly]=readOnly`? – Ruan Mendes Feb 04 '16 at 22:05
  • Tried it now, and indeed it was that simple: [required]="isRequired" [readonly]="isReadOnly". – Andris Krauze Feb 04 '16 at 22:13
  • Actually now I got the problem, that I can't turn "required" off anymore. Updated my question. – Andris Krauze Feb 04 '16 at 22:39
  • I was going to mention that, you have to be able to remove the required attribute for it not to be required, because `required="false"` does not mean it will not be required, oh do I hate attributes that have to be present/absent – Ruan Mendes Feb 04 '16 at 22:43
  • It's weird that disabled supposedly works for disabled which also is one of those attributes. Can you create a live example on plunkr or jsfiddle? See https://angular.io/docs/ts/latest/guide/forms.html – Ruan Mendes Feb 04 '16 at 22:56
  • have you tried [attr.readonly] instead of [readonly] ? – Gustavo Ulises Arias Méndez Nov 18 '16 at 00:39

3 Answers3

36

For bound attributes to be removed they need to be set to null. There was a discussion to change it to remove on false but it was declined, at least for now.

 [required]="isRequired ? '' : null"

or

 [required]="isRequired ? 'required' : null"

Your Plunker produces an error because of missing [] around ngControl.

See also this Plunker for a working example

See also Deilan's comments below.

Günter Zöchbauer
  • 623,577
  • 216
  • 2,003
  • 1,567
  • 1
    I updated my answer (see also the Plunker link with a working example). – Günter Zöchbauer Feb 05 '16 at 07:06
  • you are champ . you have eagle eye.. +1 – Pankaj Parkar Feb 05 '16 at 07:08
  • 1
    Required attribute is removed indeed, but after removal fieldCtrl.valid still gives false for empty field. Any ideas why is that? – Andris Krauze Feb 05 '16 at 07:32
  • 1
    `[required]="isRequired ? '' : null"` works best for me since it applies `required` attribute to the DOM element if the condition is `true` without any junky values and removes the attribute if the condition is `false`. – Deilan Mar 30 '17 at 17:13
  • @Deilan right, it doesn't really matter what value is used for when `isRequired == true`. `null` removes the attribute, any other value retains it. – Günter Zöchbauer Mar 30 '17 at 17:19
  • To be more specific: `[required]="isRequired ? 'true' : null"` results in `required="true"` in the element's markup if `isRequired == true`. The observable behavior of browsers is all the same regardless of the fact whether a value to the right of the attribute is present or not. But the [HTML specs](https://html.spec.whatwg.org/#boolean-attributes) clearly states: `If the attribute is present, its value must either be the empty string or a value that is an ASCII case-insensitive match for the attribute's canonical name, with no leading or trailing whitespace.` – Deilan Mar 30 '17 at 17:33
  • @Deilan, thanks for the hint, I wasn't aware of this rule. – Günter Zöchbauer Mar 30 '17 at 17:34
6

It seems you already have an answer for adding/removing the readonly attribute. For the required attribute, I suggest creating a service to keep track of the enabled/disabled state of the validator, and then to leverage the service when binding to your validation controls.

State Validator

This class is responsible for keeping track of the validator and its enabled/disabled state.

export class StateValidator {
    public enabled: boolean = true;
    validator: (control: Control) => { [key: string]: boolean };
    constructor(validator: (control: Control) => 
        { [key: string]: boolean }, enabled: boolean) {
        this.enabled = enabled;
        this.validator = validator;

    }

    enable() {
        this.enabled = true;
    }
    disable() {
        this.enabled = false;
    }
    toggle() {
        this.enabled = !this.enabled;
    }
    get() {
        return (control: Control) => {
            if (this.enabled)
                return this.validator(control);
            return null;
        }
    }
}

It has methods to enable, disable, or toggle the validator; and also a get method that returns a new validator function, which when invoked will call the underlying validator function if the validator is enabled, or return null when the validator is not enabled.

Validation Service

This class is a singleton service responsible for registering validators by key, and supporting methods to enable, disable, or toggle a validator based on that key.

export class ValidationService {
    validators: { [key: string]: StateValidator } = {};
    register(key: string, validator: Function): Function {
        var stateValidator = new StateValidator(<(control: Control) => { [key: string]: boolean }>validator, true);
        this.validators[key] = stateValidator;
        return stateValidator.get();
    }
    enable(key: string) {
        this.validators[key].enable();
    }
    disable(key: string) {
        this.validators[key].disable();
    }
    toggle(key: string) {
        this.validators[key].toggle();
    }
    list() {
        var l = [];
        for (var key in this.validators) {
            if (this.validators.hasOwnProperty(key)) {
                l.push({ key: key, value: this.validators[key].enabled });
            }
        }
        return l;
    }
}

The service also has a list function for returning a list of key/value pairs where the key represents the registered validator key, and the value is a Boolean indicator that represents the validator's enabled state.

Component

To use the ValidationService, register the service with the root injector during bootstrap:

bootstrap(AppComponent, [ValidationService]);

Or register the service with the component-level injector:

@Component({
  selector: 'app',
  providers: [ValidationService],
  ...
})

Then inject the service in your component's constructor:

export class AppComponent {
    form: ControlGroup;
    constructor(public validationService:ValidationService) {
      ...
    }
}

Next, bind to the validation controls as you normally would, except use the ValidationService to register and return the state validator:

new Control('', validationService.register('required', Validators.required));

One of the great things about this solution is that you can easily compose and mix state validators with other built-in or custom validators:

this.form = new ControlGroup({
    name: new Control('hello',
        Validators.compose([
            validationService.register('required', Validators.required),
            validationService.register('minlength', Validators.minLength(4)),
            Validators.maxLength(10)]))

});

Toggling the Validator

Use the service to toggle the state of the validator:

validationService.toggle('required');

Here is an example of how to bind to the list of state validators in a table, and hookup the toggle function to a button click event:

<table>
  <tr>
     <td>Validator</td>
     <td>Is Enabled?</td>
     <td></td>
  </tr>
  <tr *ngFor="#v of validationService.list()">
     <td>{{v.key}}</td>
     <td>{{v.value }}</td>
     <td><button (click)="validationService.toggle(v.key)">Toggle</button></td>
  </tr>
</table>

Demo Plnkr

enter image description here

Michael Kang
  • 52,003
  • 16
  • 103
  • 135
  • Just noticed syntax trick in your plunker example (angular2 template) that I am curious about: "!name?.errors?.required". Could you please comment, what does question marks stand for? – Andris Krauze May 31 '16 at 07:53
  • @AndrisKrauze They let you safely access properties on objects that may be null or undefined. This post has more info: http://www.syntaxsuccess.com/viewarticle/elvis-operator-in-angular-2.0 – eppsilon Sep 28 '16 at 17:58
3

An alternative solution I use:

import {Directive, ElementRef, Input} from '@angular/core';

@Directive({
    selector: '[symToggleRequired]'
})
export class ToggleRequiredDirective {
    @Input() public set symToggleRequired(condition: boolean) {
        if (condition) {
            (<HTMLElement>this.element.nativeElement).setAttribute('required', 'true');
        } else {
            (<HTMLElement>this.element.nativeElement).removeAttribute("required");
        }
    } 

    constructor(
        private element: ElementRef
    ) { } 
}

Use this directive on a html element to remove or add the required attribute:

<input [symToggleRequired]='FlagPropertyOfYourComponent'>