How to preprocess a model value for AngularJS Schema Form - angular-schema-form

I am using the Angular Schema Form module to create some dynamic forms. The module has been very helpful and straightforward, but I'm new to Angular and have a question about pre-processing some json data.
The scenario: We have some existing json that we'd like to feed into our json schema form application. Most fields work well, but for boolean values our integrators used string values "true" and "false", which Angular Schema Form does not recognize as actual values true and false when parsing (understandably so).
Example, we have this in our schema:
{
....
"isRequred":{
"type":"boolean",
"title":"Required"
}
....
}
And for our actual data we have:
{
...
"isRequired":"true"
...
}
I have corrected the issue by changing the checkbox template and adding a directive to it, then performing the conversion logic via the directive. Relevant code:
angular.module('jsonFormBuilderApp').directive('stringToBoolean', function($parse){
return {
require: 'ngModel',
link: function(scope, element, attrs, modelCtrl) {
var value = scope.$eval(attrs.ngModel);
var boolvalue = (value === true || value === 'true');
modelCtrl.$setViewValue(boolvalue);
modelCtrl.$render();
}
};
});
angular.module('schemaForm').run(['$templateCache', function($templateCache) {
$templateCache.put("directives/decorators/bootstrap/checkbox.html","<div ... copy the template from bootstrap decorators and add string-to-boolean to the input field ...> </div>");
}]);
This works, but I have to wonder if this is the correct way to go about this task or if there is a better, more recommended manner? Another concern is that I am changing all of the templates for all form/schema types that use the checkbox.html, because I don't think with this method that I can check that the schema is actually a type "boolean", which I suspect could create an issue...
Creating a directive and processing these anomalies, as suggested by Josef, is much cleaner to provide separation of concerns.
To distill the question further, however, I guess what I'm ultimately asking is this: What is the best way to interject into the process in which the model value is fit to the schema?
Thank you for taking a look and any recommendations are welcome.

Perhaps this is an answer to a different question, but your problem could also be solved by using a schema where isRequired has "type": "string" with "enum": ["true", "false"].
For the form you could use "type": "radiobuttons" and a "titleMap": ["Required", "Not required"] to choose "true"/"false", or write your own custom form type add-on to get a checkbox.

After some more familiarity with Angular, I discovered that directives can be linked several times. My final solution was: instead of mapping an entirely new template to the form item, I check all ngModel items to see if they are booleans and, if they are, I filter their values to convert string "true" to boolean true, as here:
angular.module('schemaForm')
.directive('ngModel', function() {
return {
priority: 1001,
link: function(scope, element, attrs) {
if(typeof(scope.form) !== 'undefined' && scope.form.schema.type === 'boolean') {
scope.$watch(attrs.ngModel,function() {
var value = scope.$eval(attrs.ngModel);
var boolvalue = (value === true || value === 'true' || value === '1');
scope.ngModel.$setViewValue(boolvalue);
scope.ngModel.$render();
});
}
}
};
});
I'm sure this could be further optimized. If there are any thoughts on cons to this or a suggestion for a better approach please share.

Related

Angularjs >1.3 $validator causes modelValue to go undefined

I am using $validator to write a custom form validation directive.
Currently it looks like this :
module.directive('tweetLength', function(URLDetector) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attrs, ctrl) {
var allowedCharacters;
allowedCharacters = parseInt(attrs.tweetLength);
ctrl.$validators.tweetLength = function(modelValue, viewValue) {
var result;
return result = URLDetector.urlAdjustedCharacterCount(modelValue) <= allowedCharacters;
};
}
};
});
It checks the model of the element it is attached to for the number of characters, whilst taking into account link shortening (so ng-minlength and ng-maxlength don't work). It returns false when the requirements aren't met. The problem is that when it returns false modelValue goes undefined. I know at this point the value is meant to be stored in $$invalidModelValue, but I still need the value in the original model since it is being used elsewhere in the view.
Is there a way to stop Angular from moving it and making the original model undefined? I know this problem could be solved in the form controller, but I don't think that is the correct way to do it since I want to disable the form submission button using the form state and not some external variable. Is there an alternate way to approach this problem whilst using Angular form validation?
Beginning in Angular v. 1.3, when $validate() returns false, the value of ng-model is set to undefined (docs here)
To prevent this behavior, set allowInvalid property of ngModelOptions to true, like so:
ng-model-options="{allowInvalid: true}"

How to update a form controller in AngularJS after changing input constraints

I have been recently working on dynamically changing constraints on input fields on a form. The aim was to make a back-end-driven form, whereas all fields and their constraints are being sent to us by a server. Whilst I managed to get the constraints to be added/removed as we please by creating a simple directive, it seems like a form controller is not picking up these changes, and so the form is always $valid. Have a look at this jsfiddle, here is the directive:
myApp.directive('uiConstraints', [function(){
function applyConstraints(element, newVal, oldVal){
//remove old constraints
if(oldVal !== undefined && oldVal !== null){
for (var i = 0; i < oldVal.length; i++) {
element.removeAttr(oldVal[i].key);
}
}
//apply new constraints
if(newVal !== undefined && newVal !== null){
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
function link(scope, element, attrs){
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
});
}
return {
restrict : 'A',
link : link
};
}]);
The required behavior is so it works like on the official angularjs plunker. However, it seems the FormController is being created before the directive populates constraints on the input fields, and updating these constraints doesn't update the corresponding values in the FormController.
Does any1 know if I can force the FormController to pickup the changes to constraints made by the directive? And if so, how? I have no idea where to even start... Thanks.
-- EDIT --
I couldn't get plunker to work (show to others my latest changes) so here is jsfiddle of what I have: latest
To more in detail describe the issue:
go to the jsfiddle described
if you remove the initial value from the textbox, it will become red (invalid), however the controller won't pick that up and will still show:
myform.$valid = true
myform.myfield.$valid = true
-- EDIT --
The bounty description doesn't recognize Stack Overflow formatting (like 2 spaces for new line etc) so here it is in more readable form:
Since this is still unsolved and interesting question I decided to start a bounty.
The requirements are:
- works on ancient AngularJS(1.0.3) and newer (if it can't be done on 1.0.3 but someone did it on newer version of angular I will award bounty)
- initially a field has no constraints on it (is not required, max and min not set etc)
- at any time constraints for the field can change (it can become required, or a pattern for the value is set etc), as well as any existing constraints can be removed
- all constraints are stored in a controller in an object or array
- FormController picks up the changes, so that any $scope.FormName.$valid is being changed appropriately when constraints
on any fields in that form change
A good starting point is my jsfiddle.
Thanks for your time and good luck!
Check out this PLUNK
.directive('uiConstraints', ["$compile",
function($compile) {
function applyConstraints(element, newVal, oldVal) {
//apply new constraints
if (newVal !== undefined && newVal !== null) {
for (var i = 0; i < newVal.length; i++) {
var constraint = newVal[i];
element.attr(constraint.key, constraint.value);
}
}
}
return {
restrict: 'A',
terminal: true,
priority: 1000,
require: "^form",
link: function(scope, element, attrs, formController) {
var templateElement;
var previousTemplate;
templateElement = element.clone(); //get the template element and store it
templateElement.removeAttr("ui-constraints");// remove the directive so that the next compile does not run this directive again
previousTemplate = element;
scope.$watch(attrs.uiConstraints, function(newVal, oldVal) {
var clonedTemplate = templateElement.clone();
applyConstraints(clonedTemplate, newVal);
clonedTemplate.insertBefore(previousTemplate);
var control = formController[previousTemplate.attr("name")];
if (control){
formController.$removeControl(control);
}
if (previousTemplate) {
previousTemplate.remove();
}
$compile(clonedTemplate)(scope);
previousTemplate = clonedTemplate;
});
}
};
}]);
The idea here is to set terminal: true and priority: 1000 to let our directive be compiled first and skip all other directives on the same element in order to get the template element. In case you need to understand more, check out my answer: Add directives from directive in AngularJS.
After getting the template element, I remove the ui-constraints directive to avoid this directive getting compiled again and again which would add a $watch to the digest cycle every time we toggle the constraints.
Whenever the constraints change, I use this template element to build a new element containing all the constraints without the ui-constraints directive and compile it. Then I remove the previous element from the DOM and its controller from the Form Controller to avoid leaking and problems caused by previous element's controller existing in the Form Controller.
For starters, AngularJS has ng-required directive that allows you to toggle required validation
<input type="text" data-ng-required="isRequired"
data-ng-model="mydata" name="myfield" />
see the Fiddle:
Fiddle
Making Min and Max logic dynamic is a little more complex. Your current approach JQuery-like in alter elements which is NOT AngularJS strength. The ideal Answer is a directive like ng-minlnegth with more intelligence.
I'll look into that.
Quick and dirty solution:
scope.$watch(attrs.uiConstraints, function(newVal, oldVal){
applyConstraints(element, newVal, oldVal);
if (newVal != oldVal) {
element = $compile(element)(scope);
}
});
I don't like solution and it's not perfect. This is an interesting problem, so I'm still working on finding a more elegant solution.
Here is an example.
What went wrong for you was that you tried to use scope in your apply thing, and you were watching the wrong thing.
I changed your link function:
link : function(scope, element, attrs){
scope.$watch(attrs.constraints, function(newConstraints, oldConstraints){
applyConstraints(newConstraints, oldConstraints, element);
}, true);
}
And your toggle function now does some random attributes and stuff:
$scope.toggleConstraints = function(myconstraints){
var getRandomPropOrValueName = function(){
return Math.random().toString(36).substring(7);
}
$scope.myconstraints = [{key: getRandomPropOrValueName(), value: getRandomPropOrValueName() }];
};

Combining Polymer elements and angular

I have looked at a number of answers for binding Angular-JS scope data to Polymer components. (Use Angular-Bind-Polymer Another, And a third). It seems like this shouldn't be this hard, if the polymer components are truly just DOM. (Note that I'm using Chrome beta (36)).
I tried the Angular-Bind-Polymer suggestion, but no luck. My real interest is extending ngModel to work with Polymer so that I can use the Polymer check boxes, radio buttons, etc. For example, I tried getting paper-checkbox to work, so I tried the following, thinking that it should work:
var ngPaper = angular.module('ng-paper', []);
ngPaper.directive('paper-checkbox', function() {
console.log("Processing directive");
return {
restrict: 'E',
require: '?ngModel',
link: function(scope, element, attr, ctrl) {
console.log("Running linker");
element.on('click', function() {
scope.$apply(function() {
ctrl.$setViewValue(element[0].checked);
});
});
ctrl.$render = function() {
element[0].checked = ctrl.$viewValue;
};
ctrl.$isEmpty = function(value) {
return value != true;
};
ctrl.$formatters.push(function(value) {
return value === true;
});
ctrl.$parsers.push(function(value) {
return value ? true : false;
});
}
};
});
But no.
I then tried using angular-bind-polymer to bind the checked value on the paper-checkbox to a model attribute but didn't have any success.
I feel like if I could figure out how to get one of the form control elements to work, the others should fall quickly in line. Does anyone have a better idea on how to do this or an explanation as to why the directive I wrote isn't getting picked up and applied to the paper-checkbox?
I made this generic work-around that I use to watch changes on check-boxes and most Polymer elements from AngularJS, it's really useful while you find a more proper way, I hope it helps you.
You can also use it to manipulate Polymer Elements (E.g. Toggle).
in your HTML:
<paper-radio-group selected="firstOption">
<paper-radio-button label="First Option" id="firstOption" ng-click="change()"></paper-radio-button>
<paper-radio-button label="Second Option" id="secondOption" ng-click="change()"></paper-radio-button>
</paper-radio-group>
In the corresponding AngularJS controller, requites $scope.
var firstOption= document.querySelector('paper-radio-button[id="firstOption"]');
var secondOption= document.querySelector('paper-radio-button[id="secondOption"]');
console.log(firstOption);
console.log(secondOption);
$scope.change = function()
{
console.log(firstOption);
console.log(secondOption);
}
This way every time the user changes the selection, AngularJS will get notified so it can query the changes, you can scope the data you get back to something more specific, this is particularly useful to toggle Polymer Elements from AngularJS.
Let me know if this works for you, happy coding!

Is it possible to provide an implicit way of converting object into string for angular templates?

Let's assume I've got a few objects that have the same prototype and I want to customize their display in Angular template. I know I can create my own filter, and then use it like that:
<p>{{anObjectOfProtoP | myCustomFilter}}</p>
or a function attached to $scope:
<p>{{myCustomFunction(anotherObjectOfProtoP)}}</p>
My question is: is it possible to achieve similar functionality without explicitly specifying the rendering function every time? The ideal solution would be if angular checked for function toAngularString on object inside the {{}}, and then used it's return value in template.
In other words, I'd like Angular to do
function (o) {
if (typeof o.toAngularString === 'function') return o.toAngularString();
return o;
}
on every object inside {{}}.
Depending on whether you use {{ ... }} or ng-bind syntax, the .toJSON and the .toString function on your object will be called to determine its representation. Hence, you can provide the representation you want in either .toString or .toJSON function of your object.
This discrepancy in which function is called has let to some problems, actually.
Another way you can do that is by writing your own directive my-toangularstr as this:
app.directive('myToangularstr', function () {
return {
scope: true,
template: '<span class="my-angular-value">{{ val.toAngularString() }}</span>',
link: function (scope, elem, attrs) {
scope.$watch(attrs['myToangularstr'], function (newVal) {
if (typeof newVal !== 'undefined') {
scope.val = newVal;
}
})
}
}
})
A working demo showing all three methods is here.
I think that is as close as one can get using the external API of angular.

Angularjs + kendoui dropdownlist

I have this directive
angular.module('xxx', [
])
.directive('qnDropdown', [
'$parse',
function($parse) {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, elem, attr, ngModel) {
scope.$watch(attr.qnDropdown, function(source) {
var model = $parse(attr.ngModel);
elem.kendoDropDownList({
dataTextField: "Name",
dataValueField: "ID",
value: attr.value,
select: function(e) {
var item = this.dataItem(e.item.index());
scope.$apply(function() {
model.assign(scope, item.value);
});
},
//template: '<strong>${ data.Name }</strong><p>${ data.ID }</p>',
dataSource: source
});
});
}
};
}]);
Input field is
<input qn:dropdown="locations" ng:model="installation.LocationID" value="{{installation.LocationID}}" />
EVerything works fine but initial value for kendoDropDownList is not filled (value: attr.value).
I suppose I am doing something at wrong place or time but not sure what?
You probably need to use $observe:
Use $observe to observe the value changes of attributes that contain interpolation (e.g. src="{{bar}}"). Not only is this very efficient but it's also the only way to easily get the actual value because during the linking phase the interpolation hasn't been evaluated yet and so the value is at this time set to undefined. -- docs, see section Attributes.
Here's an example where I used $observe recently. See also #asgoth's answer there, where he uses $watch, but he also created an isolate scope.
I'm still not clear on when we need to use $observe vs when we can use $watch.
Are you sure {{installation.LocationID}} has a value you expect? I was able to copy-paste your code with some tweaks for my situation and the dropdownlist is working wonderfully (thank you for doing the hard work for me!). I'm populating value on the input field and when the directive executes, attr.value has it and Kendo shows it as expected. Perhaps this was an Angular issue a couple versions ago?
I had the same problem, the attr.value was empty. The problem was related to an $http async call being made to get the data. The scope data was not yet populated when the dropdownlist was being defined in the directive.
I fixed this by watching attr.ngModel instead of attr.qnDropdown in the link function of the directive. This way the dropdownlist gets defined when the scope data is populated.

Resources