I'm experiencing some problems while inputing user data into ordered fields displayed with Angular ng-repeat.
Say that you want some values to display on a list, and those values might be editable. At the same time, you are ordering that data. Due to how ng-model works and Angular reflow cycle, if the value of one input surpases another one while still editing, you'll find yourself typing on the wrong field. Look at this example:
var app = angular.module('app', []);
app.directive('myrow', Row);
app.controller('controller', Controller);
function Controller () {
this.order = '-value';
this.inputs = [
{value: 1, tag: "Peas"},
{value: 2, tag: "Apples"},
{value: 3, tag: "Potatos"}
];
}
function Row($compile, $sce){
var linker = function($scope, $element, $attrs){
var template = '<div>- <input type="number" ng-model="data.value"><span ng-bind="data.tag"></span></div>';
a = $element.html(template);
$element.html(template);
$compile($element.contents())($scope);
}
return {
restrict: 'AE',
replace: true,
scope: {
data: "="
},
link: linker
}
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.5/angular.min.js"></script>
<div ng-app="app" ng-controller="controller as ctrl">
List:
<div ng-repeat="item in ctrl.inputs | orderBy: ctrl.order">
<div myrow data="item"></div>
</div>
</div>
I've made this simplified example as the original component has thousands of lines and some dependencies. Here this problem is not reproduced exactly, yet, when you write, sometimes the input loses focus, thing that, for example, doesn't happen when not compiling on the directive (which is completly necessary in my real code). Any ideas on how to solve this? Is it possible to activate ng-model update on change instead of on user typing.
You can use ng-model-options and its updateOn property so that your model is updated only when user leaves the field.
You can see how it works here: https://docs.angularjs.org/api/ng/directive/ngModelOptions (There is a sample in the 'Triggering and debouncing model updates' section)
example:
<input ng-model-options="{ updateOn: 'blur'}" />
Related
Im binding an HTML form stored as JSON into a view template dynamically using a custom directive.
This HTML consists of a select tag and options that are generated dynamically using ng-repeat and who's model are set using ng-model.
The problem is that the bound data from the select tags, comes back as null
Here is the entire Model-View-Controller setup:
JSON with HTML:
{"location":"<input class='form-control' type='text' placeholder='City' ng-model='model.location.city'/><select class='form-control' ng-model='model.location.state'><option value=''>State</option><option ng-repeat='(k, val) in states' value='{{k}}'>{{val}}</option></select>"}
..resolves to this:
<input class='form-control' type='text' placeholder='City' ng-model='model.location.city'/>
<select class='form-control' ng-model='model.location.state'>
<option value=''>State</option>
<option ng-repeat='(k, val) in states' value='{{k}}'>{{val}}</option>
</select>
View Template:
<div bind-html-compile="html_from_json">
<!-- to be filled with HTML from JSON file -->
</div>
<button ng-click="getdata()">get form data</button>
Directive doing the binding:
.directive('bindHtmlCompile', ['$compile', function ($compile) {
return {
restrict: 'AE',
link: function (scope, element, attrs) {
scope.$watch(function () {
return scope.$eval(attrs.bindHtmlCompile);
}, function (value) {
element.html(value && value.toString());
var compileScope = scope;
if (attrs.bindHtmlScope) {
compileScope = scope.$eval(attrs.bindHtmlScope);
}
$compile(element.contents())(compileScope);
});
}
};
}]);
The Controller & Model
.controller('testCtrl', function($scope, $http, $state, $sce){
$scope.model = {
location : {
city:'', // this works fine!
state:'' // this comes back as null
}
};
$http.get('html_form.json').then(function(res){
$scope.html_from_json = $sce.trustAsHtml(res.data['location']);
$scope.getdata = function(){
console.log($scope.model);
};
});
});
This is the output to the log when "new york" is entered into the "city" field and a given state is chosen:
If I put the HTML form directly into the view (not pull it from JSON) everything works correctly. It seems that getting the data from the JSON string is what is causing this issue.
Even more strangely, the following:
console.log($scope.model.location);
does return the selected state, but expanding the object in the dev console, or trying to use the data resolves to null.
Does anyone know what may be causing this and how it can be resolved??
--UPDATE--
I found that I can get around this issue by setting the ng-models for the JSON html to non-object values. For example:
<select ng-model="state"></select> <!-- this works -->
vs.
<select ng-model="location.state"></select> <!-- this returns null -->
Then I just feed the values back to the model in my controller:
$scope.model.location.state = $scope.state;
But this is kinda crude. I would still like to know what the issue is.
Use this code below, using ng-repeat in select options is not a good practice as there is ng-options for that.
<select class='form-control' ng-model='model.location.state' ng-options='k as val for (k, val) in states'>
</select>
May be it helps you, Best of luck :)
In the following example a new field is added (by adding a blank row to $scope) when the last field loses focus if it is not empty. The problem is that the new field is not added to the DOM in time to receive focus.
Is there a way to detect when angular has finished appending new field to the DOM and then pass focus to it?
Please, no "timer" solutions; the time it takes to change DOM is unknown and I need this focus switch to happen as fast as possible. We can do better!
JSFiddle
HTML
<div ng-app='a' ng-controller='b'>
<input type="text" ng-repeat="row in rows" ng-model="row.word" ng-model-options="{'updateOn': 'blur'}">
</div>
JS
angular.module('a', []).controller('b', function ($scope) {
$scope.rows = [{'word': ''}];
$scope.$watch('rows', function (n, o) {
var last = $scope.rows[$scope.rows.length - 1];
last.word && $scope.rows.push({'word': ''});
}, true);
});
This is a View-concern and so should be dealt with by using directives.
One way to do so, is to create a directive that grabs the focus when it's linked:
.directive("focus", function(){
return {
link: function(scope, element){
element[0].focus();
}
}
});
and use it like so:
<input type="text"
ng-repeat="row in rows"
ng-model="row.word"
focus>
Demo
Use $timeout without specifying a number of milliseconds. It will, by default, run after the DOM loads, as mentioned in the answer to this question.
angular.module('a', []).controller('b', function($scope, $timeout) {
$scope.rows = [{
'word': ''
}];
$scope.addRow = function() {
$scope.rows.push({
'word': ''
});
$timeout(function() {
//DOM has finished rendering
var inputs = document.querySelectorAll('input[type="text"]');
inputs[inputs.length - 1].focus();
});
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app='a' ng-controller='b'>
<div ng-repeat="row in rows">
<input type="text" ng-model="row.word" ng-model-options="{'updateOn': 'blur'}"><br>
</div>
<input type="button" ng-click="addRow()" value="Add Row">
</div>
I've got a UI page setup through Angular, and I'm trying to take advantage of the built in ng-maxlength validator on an input element. Long story short, I know about $scope.form.$error and how that object has a maxlength property in the case that the validation fails. But I want to display an error message specific to the character length that was violated, and I don't see anywhere that the length that I specified was stored on this object. Does anyone know if it's possible to access this, so I don't have to write out a separate error message for each input that has the max length violated?
EDIT: To answer your question, yes angular does store a boolean value in the $error object that is accessible to your via the key(s) that are set in the object. In the case of the code I provided below and in th jsFiddle, we are setting the key for angular, and the value of either true or false.
Be mindful when setting the value as it is reversed. ex. $setValidity( true ), flips the $error to false.
Ok, here is what I think you were looking for...
In Angularjs v1.2.13 you will not have access to ng-message or the $validator pipeline,
which is why are are using $formatters and $parsers.
In this case, I am using named inputs, but perhaps in your case you need dynamic input names?
Plus, if you are using inputs but no form, then getting the error message to display would have to be done with a separate custom directive.
If so, then please look here for dynamically named input fields for some help.
dynamic input name in Angularjs link
Let me know if this works; I'll make changes as needed to HOOK YOU UP!
In case you don't know, you can write over Angular's maxlength for each individual input.
If you changed 'maxlength' in the updateValidity() function in the directive below, to something like 'butter', then $scope.form.inputname.$error would be something like
$scope.formname.inputname.$error { butter: true }
if you also used ng-maxlength="true", then it would be
$scope.formname.inputname.$error { butter: true, maxlength: true }
Another example if you used ng-maxlength, and capitalized the 'maxlength' in the directive to 'Maxlength'
Then you would get
$scope.formname.inputname.$error { maxlength: true(angular maxlength), Maxlength: true(your maxlength)
And of course if you name it the same, then yours writes over angulars
$scope.formname.inputname.$error { maxlength: true };
The point is YOU can add your own names to the angular $error object; you can write over Angular's; and you can just use what Angular gives you when you use Angular's directives: like ng-required="true", or ng-maxlength="true"
Link to YOUR angularjs version on jsFiddle
jsFiddle LInk
<div ng-app="myApp">
<form name="myForm">
<div ng-controller="MyCtrl">
<br>
<label>Input #1</label>
<br>
<input ng-model="field.myName" name='myName' my-custom-length="8" />
<span ng-show="myForm.myName.$error.maxlength">
Max length exceeded by {{ myForm.myName.maxlength }}
</span>
<br>
<br>
<label>Input #2</label>
<br>
<input ng-model="field.myEmail" name='myEmail' my-custom-length="3" />
<span ng-show="myForm.myEmail.$error.maxlength">
Max length exceeded by {{ myForm.myEmail.maxlength }}
</span>
</div>
</form>
</div>
var app = angular.module('myApp', []);
app.controller('MyCtrl', function ($scope) {
$scope.field = {};
});
app.directive("myCustomLength", function () {
return {
restrict: 'A',
require: 'ngModel',
link: function (scope, element, attrs, ctrl) {
if (!ctrl) { return } // ignore if no ngModel controller
ctrl.$formatters.push(validateInput);
ctrl.$parsers.unshift(validateInput);
function validateInput(value) {
if (!value) {
updateValidity(false);
return;
}
inputLength(value);
var state = value.length > attrs.myCustomLength;
updateValidity(state);
}
function inputLength(value) {
ctrl.maxlength = null;
var length = value.length > attrs.myCustomLength;
if (length) {
ctrl.maxlength = (value.length - attrs.myCustomLength).toString();
}
}
function updateValidity(state) {
ctrl.$setValidity('maxlength', !state);
}
} // end link
} // end return
});
CSS Here if you need it.
input.ng-invalid {
border: 3px solid red !important;
}
So I have access to a REST API that I am hitting, that returns the following pre-generated HTML:
<p class="p">
<sup id="John.3.16" class="v">16</sup>
<span class="wj">“For </span>
<span class="wj">God so loved </span>
<span class="wj">the world,</span>
<span class="wj">that he gave his only Son, that whoever believes in him should not </span>
<span class="wj">perish but have eternal life.</span>
</p>
This has presented an interesting new challenge for me in my learning of AngularJS. I have no control over the HTML that is returned from the API, since it's not an API that I built.
What I'm trying to do (and this could be the completely wrong approach) is to build a class directive on the "v" class, so that I can add an ng-click attribute to the verse number and pass the verse information on to another part of my application.
Below is the code I currently have, which doesn't seem to do anything, though I thought it would.
var app = angular.module('ProjectTimothy');
app.filter("sanitize", ['$sce', function($sce) {
return function(htmlCode){
return $sce.trustAsHtml(htmlCode);
}
}]);
app.controller("timothy.ctrl.search", ['$scope', '$http', function($scope, $http){
$scope.url = "http://apiendpoint.com/";
$scope.copyright = "";
$scope.search = function() {
// Make the request to the API for the verse that was entered
// Had to modify some defaults in $http to get post to work with JSON data
// but this part is working well now
$http.post($scope.url, { "query" : $scope.searchwords, "version": "eng-ESV"})
.success(function(data, status) {
// For now I'm just grabbing parts of the object that I know exists
$scope.copyright = data.response.search.result.passages[0].copyright;
$scope.result = data.response.search.result.passages[0].text;
})
.error(function(data, status) {
$scope.data = data || "Request failed";
$scope.status = status;
});
};
}]);
app.directive("v", ['$compile', function($compile) {
return {
restrict: 'C',
transclude: true,
link: function(scope, element, attrs) {
element.html("<ng-transclude></ng-transclude>").show();
$compile(element.contents())(scope);
},
scope: { id:'#' },
/*template: "<ng-transclude></ng-transclude>",*/
replace: false
};
}]);
HTML Template that is being populated with the HTML returned by API:
<div class="bible_verse_search_container" ng-controller="timothy.ctrl.search">
<div class="input-group">
<input type="text" class="form-control" placeholder="Bible Verse To Read (i.e. John 11:35)" ng-model="searchwords">
<span class="input-group-btn">
<button class="btn btn-default" type="button" ng-click="search()">Search</button>
</span>
</div>
<div class="well" ng-show="copyright" ng-bind-html="copyright | sanitize"></div>
<div ng-bind-html="result | sanitize"></div>
</div>
So What I was hoping would happen would be that the HTML is populated into the bottom div that binds the html, and then somehow $compile would be called to convert the "v" class sup's into directives that I can modify. Again, I'm pretty new to Angular, so there may be a super easy way to do this like most other things in Anguler that I just haven't found yet.
Really, the end goal is that each verse number is converted into a directive of its own to be able to make it clickable and access the id attribute that it has so that I can send that information with some user content back to my own API.
This feels like a lot of information, so let me know if anything is unclear. I'll be working on it, so if I figure it out first, I'll be sure to update with an answer.
IN PROGRESS
Checked out this question: https://stackoverflow.com/a/21067137/1507210
Now I'm wondering if it would make more sense to try and convert the section where the verse is displayed into a directive, and then making the search controller populate a scope variable with the HTML from the server, and then use that as the template for the directive... think think think
I think your second approach--convert the section where the verse is displayed into a directive--would be a nice way of doing it.
You could replace this:
<div ng-bind-html="result | sanitize"></div>
with a directive like this:
<verse-display verse-html="{{result}}"></verse-display>
The directive definition would look like this:
app.directive('verseDisplay', ['$compile', function($compile) {
function handleClickOnVerse(e) {
var verseNumber = e.target.id;
// do what you want with the verse number here
}
return {
restrict: 'E',
scope: {
verseHtml: '#'
},
replace: true,
transclude: false,
template: '<div ng-bind-html="verseHtml | sanitize"></div>',
link: function(scope, element, attrs) {
$compile(element.contents())(scope);
element.on('click', '.v', handleClickOnVerse);
}
};
}]);
So you could apply your own click handler to the element.
Here's a fiddle. (Open the console to see the verse number getting logged out.)
Probably the most unwisest of things I have posted here, but it's pretty cool code. I do not know if I recommend actually running this, but here's a jsfiddle
So one of the reasons I call this unwise is because the injected code will run any directives you have not just the one you wanted. There may be many other security risks beyond that as well. But it works fantastically. If you trust the HTML you are retrieving then go for it.
Check out the fiddle for the rest of the code:
function unwiseCompile($compile) {
return function (scope, element, attrs) {
var compileWatch = scope.$watch(
function (scope) { return scope.$eval(attrs.unwiseCompile); },
function (unwise) {
element.html(unwise);
$compile(element.contents())(scope);
// for better performance, compile once then un-watch
if (scope.onlyOnce) {
// un-watch
compileWatch();
}
});
};
}
I have a bit of a strange scenario that is a little different to the other childscope and two way binding issues I have seen on Stackoverflow.
I have a field generation directive that receives a configuration object and some data and dynamically creates the correct type of field on screen and populates the data.
directive.js
.directive('myField', function () {
var stringTemplate = "scripts/directives/templates/my-string.tpl.html";
var textTemplate = "scripts/directives/templates/my-text.tpl.html";
var selectTemplate = "scripts/directives/templates/my-select.tpl.html";
var linker = function ($scope, elem, attrs) {
// Function to dynamically select the correct template
$scope.getTemplateUrl = function () {
var template = '';
if ($scope.options) {
if ($scope.options.optionList) {
template = selectTemplate;
} else {
switch ($scope.options.type) {
case 'String':
template = stringTemplate;
break;
case 'Text':
template = textTemplate;
break;
}
}
return template;
}
};
return {
restrict: 'E',
replace: true,
scope: {
options: '=',
data: '=',
fieldName: '#',
fieldWidth: '#',
labelWidth: '#',
},
link: linker,
template: '<ng-include src="getTemplateUrl()"/>'
}
});
I then have the corresponding template... I'm showing just the string template in this case.
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" ng-model="data" tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
An example of how this might then be used would be
controller.js
$scope.person.firstName = "John";
$scope.person.lastName = "Doe";
$scope.options.person.firstName.type = "String";
index.html
<div class="row">
<my-field options="options.person.firstName" data="person.firstName" field-name="firstName" label-width="small" field-width="medium"></my-field>
The problem is the usual one, my-field directive has an isolated scope with a "data" property that is two-way bound to the controller. Because I am then using ng-include to dynamically load the correct template I am creating a further child scope that due to prototypical inheritance still populates correctly as it doesn't have its own data property so reaches to the parent. However when I modify the field, a shadow property is created on my child scope called data that doesn't propagate upwards the way that two way binding should.
I hope you are still with me
controller > my-field
ng-include causes the following scopes to exist
controller > my-field > ng-include
From reading around I understand that what I need to do to rectify this is pass an object rather than a primitive, however as there is effectively an intermediate layer between my controller and my final directive this is not straightforward.
I thought about changing the isolate scope in my-field to look like this
scope: {
....
data: {value: '=data'}
....
}
and then updating the template to refer to the object
my-string.tpl.html
<div class="form-group col-md-12">
<label for="{{fieldName}}" class="{{labelWidth}}">
{{options.label}}
</label>
<div class="{{fieldWidth}}">
<input type="text" class="form-control input-sm" id="{{fieldName}}" placeholder="{{options.watermark}}" **ng-model="data.value"** tooltip="{{options.tipText}}" ng-disabled="options.editable === false">
</div>
</div>
but this kills angular.
I have successfully got it to work by reaching back to the controller scope for binding by using
ng-model="$parent.$parent.data"
but I am not really happy with this as a solution as A it is ugly and B it involves knowing the depth of scope you are at which could vary.
Really stumped with this. Any help would be appreciated.