Edge-case using ng-model in angularjs forms - javascript

I have an annoying edge case that I'm dealing with and would love some help.
The situation:
I'm working on an angular app that is a 'theme' for a donation platform; the donation platform allows us to host theme files (images, stylesheets, javascript, html markup, etc.)
the platform uses liquid templating to allow use of CSRF tokens as well as to prevent some XSS nastiness.
the interpolation provider for angular was changed to // some.expression // so it wouldn't interfere with the liquid brackets {{}}
so: a 99:1 mix of html/angular and some liquid.
using angular 1.2.27
The problem:
the liquid tags also enable us to read data back from the server if a user submits a form and there's an error in it. e.g.: when a user puts their card number in but forgets a field, we can use liquid tags like {{ user.first_name }} to safely access that info and populate the value attributes of fields when the form gets reloaded. That way, users don't face a blank form with errors at the top.
for example:
<input ng-blur="validateFName=true" ng-required="true" placeholder="First name" type="text" name="donation[first_name]" ng-model="donation.first_name" value="'{{ donation.first_name }}'">
becomes:
<input ng-blur="validateFName=true" ng-required="true" placeholder="First name" type="text" name="donation[first_name]" ng-model="donation.first_name" value="John"> the value attr does get populated, angular is just ignoring it (and that's the intended use, of course)
however, because of the way that an ng-model attribute will work, even though the form elements get their value attrs populated, ng-model ignores it and the fields are effectively blank (to the user, anyways)
So: is there any way around this? Essentially, I'm looking for a way to populate the ng-models with the value attribute of their respective field or have ng-model acknowledge the value attribute. I've looked into ng-bind, -init, -value, and others, but most are for more 'normal' usage of angular. And yes, I know this isn't the best way to utilize angular, but I'm bound in this case by my requirements. Any help would be greatly appreciated. Thanks!

I would try creating a directive that watches the value attribute and updates ng-model on changes.
.directive('value', function() {
return {
restrict: 'A',
require: 'ngModel',
link: function(scope, element, attr, ctrl) {
attr.$observe('value', function(val) {
ctrl.$setViewValue(val);
}
}
}
})

Another option might be to populate the data on the model side in your Javascript instead of on the template?
Instead of
<input ng-blur="validateFName=true"
ng-required="true"
placeholder="First name"
type="text"
name="donation[first_name]"
ng-model="donation.first_name"
value="'{{ donation.first_name }}'">
What if you had in your controller
...
$scope.donation.first_name = "'{{ donation.first_name }}'";
...

Related

Disabling submit button based on fields added with ng-bind-html

JSFiddle here: http://jsfiddle.net/c6tzj6Lf/4/
I am dynamically creating forms and buttons and want to disable the buttons if the required form inputs are not completed.
HTML:
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
JavaScript:
angular.module('choicesApp', ['ngSanitize'])
.controller('ChoicesCtrl', ['$scope', '$sce', function($scope, $sce) {
$scope.custom = "Required Input: <input required type='text'>";
$scope.trustCustom = function() {
return $sce.trustAsHtml($scope.custom);
};
$scope.buttons = [
{text:'Submit 1'},
{text:'Submit 2'}];
}]);
choicesForm.$invalid is false and does not change when entering text into the input field.
Solution:
I ended up using the angular-bind-html-compile directive from here: https://github.com/incuna/angular-bind-html-compile
Here is the relevant bit of working code:
<ng-form name="choicesForm">
<div ng-if="choices" bind-html-compile="choices"></div>
<button ng-click="submitForm()" ng-disabled="choicesForm.$invalid">
Submit
</button>
</ng-form>
And choices might be a snippit of HTML like this:
<div><strong>What is your sex?</strong></div>
<div>
<input type="radio" name="gender" ng-model="gender" value="female" required>
<label for="female"> Female</label><br>
<input type="radio" name="gender" ng-model="gender" value="male" required>
<label for="male"> Male</label>
</div>
The main problem is that ngBindHtml doesn't compile the html - it inserts the html as it is. You can even inspect the dynamic input and see that it doesn't have the ngModel's CSS classes (ng-pristine, ng-untouched, etc) which is a major red flag.
In your case, the form simply doesn't know that you've added another input or anything has changed for that matter. Its state ($pristine, $valid, etc) isn't determined by its HTML but by the registered NgModelControllers. These controllers are added automatically when an ngModel is linked.
For example this <input required type='text'> won't affect the form's validity, even if it's required, since it doesn't have ngModel assigned to it.
But this <div ng-model="myDiv" required></div> will affect it since it's required and has ngModel assigned to it.
The ngDisabled directive on your buttons works as expected since it depends on the form's $invalid property.
See this fiddle which showcases how ngModel registers its controller. Note that the html containing the dynamic input gets compiled after 750ms just to show how NgModelControllers can be added after FormController has been instantiated.
There are a few solutions in your case:
use a custom directive to bind and compile html - like this one
use ngInclude which does compile the html
use $compile to compile the newly added HTML but this is a bit tricky as you won't know exactly when to perform this action
This is an answer yet imcomplete because i cannot do the code at the moment.
I think your html will be included, not compiled. So the inputs are not bind to angular and are not part of the angular form object.
The only way i see is to use a directive that will compile the passed html and add it to your form. This may be quite tricky though, if you want to go on this way i suggest to edit your question to ask for the said directive.
However i'm not really familiar with $compile so i don't know if it'll work to just add $compile around $sce.trustAsHtml()
You can write a method as ng-disabled does not work with booleans, it works with 'checked' string instead:
So on your controller place a method :
$scope.buttonDisabled = function(invalid){
return invalid ? "checked" : "";
};
And on your view use it on angular expression :
<button ng-repeat="button in buttons" ng-disabled="buttonDisabled(choicesForm.$invalid)">
Here is a working fiddle
Working DEMO
This is the solution you are looking for. You need a custom directive. In my example I have used a directive named compile-template and incorporated it in div element.
<div ng-bind-html="trustCustom()" compile-template></div>
Directive Code:
.directive('compileTemplate', function($compile, $parse){
return {
link: function(scope, element, attr){
var parsed = $parse(attr.ngBindHtml);
function getStringValue() { return (parsed(scope) || '').toString(); }
//Recompile if the template changes
scope.$watch(getStringValue, function() {
$compile(element, null, -9999)(scope); //The -9999 makes it skip directives so that we do not recompile ourselves
});
}
}
});
I found the directive in this fiddle.
I believe what is really happening though due to jsfiddle I'm unable to dissect the actual scopes being created here.
<div ng-app="choicesApp">
<ng-form name="choicesForm" ng-controller="ChoicesCtrl">
<div ng-bind-html="trustCustom()"></div>
<button ng-repeat="button in buttons" ng-disabled="choicesForm.$invalid">
{{button.text}}
</button>
</ng-form>
</div>
The first div is your top level scope, your form is the first child scope. Adding the div using a function creates the dynamically added input field as a child of the first child, a grandchild of the top level scope. Therefore your form is not aware of the elements you're adding dynamically causing only the static field to be required for valid form entry.
A better solution would be to use ng-inclue for additional form fields or if your form isn't to large then simply put them on the page or template you're using.

Getting typeahead to work in an Angular template?

I currently have a partial HTML that is being routed to, with a template and a custom Controller. The code snippet in my Angular template I would like to get working is:
<input type="text" typeahead=val for val in getValue($viewValue)>
However, it never enters into the function getValue(), while all other functions in my controller seem to be okay. When I take the typeahead out of the Angular template/partial, it seems to work. Why is this and how do I fix it?
You need to have an ng-model attribute to use the typeahead directive from AngularUI, even if you don't need to bind it to anything.
Change your markup to similar to the following:
<input type="text" ng-model="typeaheadVal" typeahead="val for val in getValue($viewValue)">

When should prefer angular directive over html native tag?

I'm a big fan of angularjs, I started lately to use it in all of my 'coding for fun' projects.
I have a big curiosity:
I have a two inputs, one disabled by a ng-disabled directive and the other disabled with an html tag (A better illustration in this link):
//...
<input type="text" disabled value="This is an html input text disabled" />
<input type="text" ng-disabled="true" value="disabled with angular js directive" />
//...
Using the browser ability I can right click on the input and remove the disabled and ng-disabled tags but only the one with the disabled tag would be editable, the other one will still be tracked by angular even when ng-disabled directives has been removed.
So, When and Why should I prefer using ng directives over native html tags? Which could be the impact of letting angular track all these actions? is it really worth to use it everywhere?
Use the native html 'disabled' if the element should always be disabled. (static, for example if you want to provide an input with text and never let the user change it)
Use angular if it should change based on variables value in the scope.
For example, say a button should change the state of an input.
<input type="button" ng-click="inpDisabled = true" >Disable Input</input>
<input type="text" ng-disabled="inpDisabled" />
live example
No harm will come if you still use ng-disabled="true" but it's redundant.
If you want to make directive static, you shoud use native html
<your-tag disable><your-tag>
against
<your-tag ng-disabled="true"><your-tag>
But AngularJS does not work this way, you shoud initialize your application and controller, then pass a variable as parameter to your directive:
JS:
$scope.isDisabled = true;
HTML:
<your-tag ng-disabled="isDisabled"><your-tag>
You shoud read more tutorials to make things clear

Angular directive inserts wrong DOM node

I have an app with many forms. Each field has several HTML elements, so I thought I could extract some directives (one per type of field) to keep my forms tidy.
I've created a sample app to demonstrate the problem, but I'm getting inconsistent behavior. In the sample app, a <link /> element replaces the <input />. In my real app, <input /> just gets removed from the DOM completely. I feel like this should be easy; why doesn't it work?
To answer your stated question, it's because you told it to, with ng-transclude. That replaces the contents of the tag with the original element, which I don't think you wanted; you probably wanted the original contents to be transcluded as the label instead.
This is probably what you're looking for:
<div class="form-group" >
<label for="{{htmlId}}" ng-transclude></label>
<input id="{{htmlId}}" class="form-control" type="text" ng-model="model" />
<span ng-repeat="error in errors">{{error}}</span>
</div>
I've moved the tranclusion into the label. While this works, I would also recommend the style of actually passing a label attribute, rather than transclude it, just for the sake of having a consistent API and simpler code; it's functionally equivalent, though, so don't let me bully you.
Also, you've got a few errors in your .js as well. First, you want to use = in your scope instead of &
scope: {
model: '=',
errors: '='
},
& is used to pass methods, while = is used for objects (this is a simplification). Since your model and errors are objects, you'll want to use = instead.
Finally, in your example, your html template and your directive's template don't have the same name... you've got an extra 's' in your .js, but that's probably just in the plunker and not your real app.

AngularJS Finding all Hidden Input in a scope WITHOUT Jquery

What is a way of finding all hidden input in a scope of a controller? And ideally can this be done when the controller is initizialied?
In my example I have mutliple comments like this:
<div ng-controller="CommentCtrl">
<form method="post">
<label>Leave Comment</label>
<textarea name="comment" ng-bind="comment"></textarea>
<input type="hidden" name="comment_id" value="1" />
<input type="hidden" name="site_id" value="2" />
</form>
</div>
So I the init, I only want to iterate through scope to find the hidden values of that controller and then assign it a value. Is there a way I can do this in AngularJS?
If you really need to access those hidden fields within your controller (which is not good a practice with angular, as #Ye Liu said above) try angular.element("input[type=hidden]"); It will give you a list with all the hidden inputs. You need to have jquery linked up in your html file before angularjs script.
You need to create a directive so you can access the DOM in the proper way.
Option 1
Create a directive that you put on the top DIV. In the link function of this directive, you can access the DOM element and find all hidden inputs.
link: function postLink(scope, iElement, iAttrs) {
angular.forEach(iElement.find('input'),function(inputElement) {
if(inputElement.attr('type') == 'hidden') {
// do something
}
});
}
This is using jqLite, which comes with Angular.
Option 2
Do something similar as Angular does with input and simular directives: create a directive based on standard HTML. So you could create a directive named 'type', restricted to attributes.

Categories

Resources