Angular 1.3-RC.0 is out! It especially helps in taming forms. Year of Moo has a great writeup.

We have a directive which - primarily - allows the easy editing of a JSON object in a textarea. It also allows editing of an arbitrary JSON value (of any primitive type) in a text input (the value is tied to the form input, so values like true and "a string!" are valid).

Prior to Angular 1.3 (release candidate released this past week), the $validators pipeine did not exist. The only way of preventing model propagation was to short-circuit the $parsers pipeline by throwing an error.

Old Version

We handled this by passing a model on a custom attribute, adding our own ngModel attribute to gain access to ngModelController, and handling a lot of the checks ourselves - see below.

The complexity (and because the replace attribute will no longer be allowed in the next major version of angular) warranted a re-write of this core directive.

 1 /*
 2  example usage: <textarea json-edit="myObject" rows="8" class="form-control"></textarea>
 3 
 4  jsonEditing is a string which we edit in a textarea. we try parsing to JSON with each change. when it is valid, propagate model changes via ngModelCtrl use isolate scope to prevent model propagation when invalid - will update manually. cannot replace with template, or will override ngModelCtrl, and not hide behind facade will override element type to textarea and add own attribute ngModel tied to jsonEditing
 5 
 6  As far as I know, there is currently no way to achieve this using $parsers (other than one of the function errors and kills the pipeline)
 7  */
 8 
 9 angular.module('editor')
10 .directive('jsonEdit', function () {
11   return {
12     restrict: 'A',
13     require: 'ngModel',
14     template: '<textarea ng-model="jsonEditing"></textarea>',
15     replace : true,
16     scope: {
17       model: '=jsonEdit'
18     },
19     link: function (scope, element, attrs, ngModelCtrl) {
20 
21       function string2JSON(text) {
22         try {
23           return angular.fromJson(text);
24         } catch (err) {
25           setInvalid();
26           return text;
27         }
28       }
29 
30       function JSON2String(object) {
31         // NOTE that angular.toJson will remove all $-prefixed values
32         // alternatively, use JSON.stringify(object, null, 2);
33         return angular.toJson(object, true);
34       }
35 
36       function setEditing (value) {
37         scope.jsonEditing = JSON2String(value);
38       }
39 
40       function updateModel (value) {
41         scope.model = string2JSON(value);
42       }
43 
44       function setValid() {
45         ngModelCtrl.$setValidity('json', true);
46       }
47 
48       function setInvalid () {
49         ngModelCtrl.$setValidity('json', false);
50       }
51 
52       function isValidJson(model) {
53         var flag = true;
54         try {
55           angular.fromJson(model);
56         } catch (err) {
57           flag = false;
58         }
59         return flag;
60       }
61 
62       //init
63       setEditing(scope.model);
64 
65       //check for changes going out
66       scope.$watch('jsonEditing', function (newval, oldval) {
67         if (newval != oldval) {
68           if (isValidJson(newval)) {
69             setValid();
70             updateModel(newval);
71           } else {
72             setInvalid();
73           }
74         }
75       }, true);
76 
77       //check for changes coming in
78       scope.$watch('model', function (newval, oldval) {
79         if (newval != oldval) {
80           setEditing(newval);
81         }
82       }, true);
83 
84     }
85   };
86 });

Lots of checks, but it worked, and we used it for quite some time. But with the new release, we could really simplify it.

New Version

The $validators object allows for custom validation, which used to be checked + handled in the $parsers and $formatters pipelines.

I decided to stay more inline with other model validation, which sets the $modelValue to undefined if the $viewValue is invalid, rather than preventing propagation.

This was made possible because of the new handling of model updating. Previously, doing this would move the cursor when the model was valid, or set the whole $viewValue to undefined as well. This change cut down on the complexity significantly as well.

As of Angular-1.3-rc.0, returning undefined for a $parser will short-circuit the pipeline, and a parse error will be attached, and none of the validators will be run. Therefore, we return the text.

 1 /*
 2 When used on a textarea, allows the direct editing of a JSON object. Invalid JSON will set the value to undefined. It is recommended you do not allow saving etc. while this field is invalid.
 3 
 4 Handles validation under the `json` attribute.
 5 
 6 example usage: 
 7 
 8  <form name="myForm">
 9   <textarea json-editor ng-model="myObject" rows="8" name="myFormElement" class="form-control"></textarea>
10   <p ng-show="myForm.myFormElement.$error.json">JSON is invalid!</p>
11  </form>
12   
13  */
14  
15 angular.module('editor')
16 .directive('jsonEditor', function () {
17 	return {
18 		restrict: 'A',
19 		require: 'ngModel',
20 		link: function (scope, element, attrs, ngModelCtrl) {
21 
22       //no longer in use
23 			function isValidJson(model) {
24         var flag = angular.isDefined(model);
25 				try {
26 					angular.fromJson(model);
27 				} catch (err) {
28 					flag = false;
29 				}
30 				return flag;
31 			}
32 
33       //need to do validation here, because validator is passed the model, which will not work for strings -- i.e. passing "" here will work, but when parsed to validator quotes will be stripped
34       function string2JSON(text) {
35         try {
36           var j = angular.fromJson(text);
37           ngModelCtrl.$setValidity('json', true);
38           return j;
39         } catch (err) {
40           //returning undefined results in a parser error as of angular-1.3-rc.0, and will not go through $validators
41           //return undefined
42           ngModelCtrl.$setValidity('json', false);
43           return text;
44         }
45       }
46 
47 			function JSON2String(object) {
48 				// NOTE that angular.toJson will remove all $$-prefixed values
49 				// alternatively, use JSON.stringify(object, null, 2);
50 				return angular.toJson(object, true);
51 			}
52 
53 			//$validators is an object, where key is the error
54 			//ngModelCtrl.$validators.json = isValidJson;
55 
56 			//array pipelines
57 			ngModelCtrl.$parsers.push(string2JSON);
58 			ngModelCtrl.$formatters.push(JSON2String);
59 		}
60 	}
61 });

The directive needs to run validation in the parser pipeline rather than using the new $validators, because a "string" in the view will be parsed in $parsers properly, but $validators will receieve the already parsed string, sans quote.

This approach allows us to also parse primitives. For example, try typing “bob” in the code pen, and it will be validated.

A version which uses $validators is available at Codepen as well, but does not work with strings (because quotes are stripped).