4

In my Angular app, I have a reactive form which for simplicity I will assume to have only one control called configJson which is represented by a <textarea> in the DOM.

I need to validate this form control to only accept valid JSON text from the user input, and display an error message otherwise.

Here's my component's class and template:

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup } from '@angular/forms';

@Component({
  selector: 'app-configuration',
  templateUrl: './configuration.component.html',
  styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {

  form: FormGroup;

  constructor() {}

  ngOnInit() {
    this.form = new FormGroup({
      'configJson': new FormControl(),
    });

    // TODO: someone add JSON validation
  }

  loadJsonConfiguration() {
    const config = JSON.parse(this.form.get('configJson').value);

    // some logic here using the parsed "config" object...
  }
}
<form [formGroup]="form">
  <div class="form-group">
    <label for="json-config-textarea">Parse from JSON:</label>
    <textarea
      class="form-control"
      id="json-config-textarea"
      rows="10"
      [formControlName]="'configJson'"
    ></textarea>
  </div>
  <div [hidden]="form.get('configJson').pristine || form.get('configJson').valid">
    Please insert a valid JSON.
  </div>
  <div class="form-group text-right">
    <button
      class="btn btn-primary"
      (click)="loadJsonConfiguration()"
      [disabled]="form.get('configJson').pristine || form.get('configJson').invalid"
    >Load JSON Configuration</button>
  </div>
</form>
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252

2 Answers2

8

I originally tried to edit the answer by the OP, but it was rejected by peer reviewers due to:

This edit was intended to address the author of the post and makes no sense as an edit. It should have been written as a comment or an answer.

So, here is my modified version:

import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';

export function jsonValidator(control: AbstractControl): ValidationErrors | null {
  try {
    JSON.parse(control.value);
  } catch (e) {
    return { jsonInvalid: true };
  }

  return null;
};
import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { jsonValidator } from './json.validator';

@Component({
  selector: 'app-configuration',
  templateUrl: './configuration.component.html',
  styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {

  form: FormGroup;

  ngOnInit() {
    this.form = new FormGroup({
      configJson: new FormControl(Validators.compose(Validators.required, jsonValidator))
    });
  }

  loadJsonConfiguration() {
    ...
  }
}
Daniel Smith
  • 553
  • 1
  • 6
  • 12
5

One solution is creating a custom form validator and attach it to the form control. The job of the validator is to only accept valid JSON.

This is how my validator looks like:

import {AbstractControl, ValidationErrors, ValidatorFn} from '@angular/forms';

export function jsonValidator(): ValidatorFn {
  return (control: AbstractControl): ValidationErrors | null => {
    const error: ValidationErrors = { jsonInvalid: true };

    try {
      JSON.parse(control.value);
    } catch (e) {
      control.setErrors(error);
      return error;
    }

    control.setErrors(null);
    return null;
  };
}

It can be easily unit-tested with the following:

import { FormControl, ValidationErrors, ValidatorFn } from '@angular/forms';
import Spy = jasmine.Spy;

import { jsonValidator } from './json.validator';

describe('JSON Validator', () => {
  let control: FormControl;
  let spySetErrors: Spy;
  let validator: ValidatorFn;

  const errorName = 'jsonInvalid';

  beforeEach(() => {
    control = new FormControl(null);
    validator = jsonValidator();
    spySetErrors = spyOn(control, 'setErrors').and.callThrough();
  });


  for (const { testId, valid, value } of [

    { testId: 1, valid: true, value: '{}' },
    { testId: 2, valid: true, value: '{"myKey": "myValue"}' },
    { testId: 3, valid: true, value: '{"myKey1": "myValue1", "myKey2": "myValue2"}' },
    // more valid cases can be added...

    { testId: 4, valid: false, value: 'this is not a valid json' },
    { testId: 5, valid: false, value: '{"theJsonFormat": "doesntLikePendingCommas",}' },
    { testId: 6, valid: false, value: '{"theJsonFormat": doesntLikeMissingQuotes }' },
    // more invalid cases ca be added...

  ]) {
    it(`should only trigger the error when the control's value is not a valid JSON [${testId}]`, () => {
      const error: ValidationErrors = { [errorName]: true };
      control.setValue(value);

      if (valid) {
        expect(validator(control)).toBeNull();
        expect(control.getError(errorName)).toBeFalsy();
      } else {
        expect(validator(control)).toEqual(error);
        expect(control.getError(errorName)).toBe(true);
      }
    });
  }
});

In the component's ngOnInit, the new validator should be added:

    this.form.get('configJson').setValidators([
      Validators.required, // this makes the field mandatory
      jsonValidator(), // this forces the user to insert valid json
    ]);

So the component's class now looks like this:

import { Component, OnInit } from '@angular/core';
import { FormControl, FormGroup, Validators } from '@angular/forms';

import { jsonValidator } from './json.validator';

@Component({
  selector: 'app-configuration',
  templateUrl: './configuration.component.html',
  styleUrls: ['./configuration.component.scss']
})
export class ConfigurationComponent implements OnInit {

  form: FormGroup;

  constructor() {}

  ngOnInit() {
    this.form = new FormGroup({
      'configJson': new FormControl(),
    });

    this.form.get('configJson').setValidators([
      Validators.required,
      jsonValidator(),
    ]);
  }

  loadJsonConfiguration() {
    const config = JSON.parse(this.form.get('configJson').value);

    // some logic here using the parsed "config" object...
  }
}
Francesco Borzi
  • 56,083
  • 47
  • 179
  • 252
  • 1
    I would go with your custom validator. I will note that calls to `control.setErrors()` in the validator are unnecessary. The validation is going to be run automatically since you are registering the custom validator. Also, it will wipe out any other errors that have have been registered. – Daniel Smith Apr 18 '19 at 14:53
  • I submitted a edit that includes other suggestions as well. – Daniel Smith Apr 18 '19 at 15:05