5

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>

2 Answers 2

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() {
    ...
  }
}
Sign up to request clarification or add additional context in comments.

Comments

7

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...
  }
}

2 Comments

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.
I submitted a edit that includes other suggestions as well.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.