Angular Typeahead dynamically added results - angularjs

I have having difficulties getting the uib-typeahead to work as I need it to.
See: https://jsfiddle.net/0wp1t0ut/ - if you type "g" into the input box, "germany" correctly gets added to the source array, but the typeahead view does not get updated until the next keypress. I.e. Germany is there, but I can't select it until I press the "e".
Basically what I'm trying to achieve is a typeahead that will dynamically update as new results come in (rather than having to wait for all my calls to complete before I return the array to the typeahead).
[Note in my real code, the ng-change on the typeahead is a function that makes multiple calls to different sources, and I want the typeahead to show with data as soon as the first call returns data, and add to that as later calls return more results.]
Has anyone come across this problem before, or able to offer any suggestions (happy to use a different typeahead if a better alternative already exists!)
HTML:
<div ng-app="myApp" ng-controller="MyCtrl as vm">
<label>TypeAhead:</label>
<input type="text" ng-model="selected" ng-change="vm.add()" uib-typeahead="state as state.descrizione for state in vm.states | filter:$viewValue | limitTo:8" typeahead-model-change class="form-control" >
<label>Model</label>
<pre>{{vm.states|json}}</pre>
<label>Modify Model Description:</label>
<input ng-model="selected.descrizione" class="form-control">
</div>
JS:
var myApp = angular.module('myApp', ['ui.bootstrap']);
myApp.controller('MyCtrl', [function() {
var vm = this;
vm.add = function(){
var a = {
codice: 'at',
descrizione: 'germany'};
vm.states.push(a)
}
vm.states = [{
codice: 'it',
descrizione: 'italia'
}, {
codice: 'fr',
descrizione: 'francia'
}];
}]);
myApp.directive('typeaheadModelChange', function() {
return {
require: ['ngModel', 'typeaheadModelChange'],
controller: ['$scope', '$element', '$attrs', '$transclude', 'uibTypeaheadParser', function($scope, $element, $attrs, $transclude, uibTypeaheadParser) {
var watchers = [];
var parserResult = uibTypeaheadParser.parse($attrs.uibTypeahead);
var removeWatchers = function() {
angular.forEach(watchers, function(value, key) {
value();
});
watchers.length = 0;
}
var addWatchers = function(modelCtrl) {
watchers.push($scope.$watch('selected', function(newValue, oldValue) {
if (oldValue === newValue)
return;
if (newValue) {
var locals = [];
locals[parserResult.itemName] = newValue;
$element.val(parserResult.viewMapper($scope, locals));
}
}, true));
}
this.init = function(modelCtrl) {
modelCtrl.$formatters.push(function(modelValue) {
removeWatchers();
addWatchers(modelCtrl);
return modelValue;
});
};
}],
link: function(originalScope, element, attrs, ctrls) {
ctrls[1].init(ctrls[0]);
}
};
});

Related

How to append a character to a number in an input field using AngularJS?

I have an input field of type text and bound to a property in a scope.
<input type=text= ng-model="vm.someProperty">
If someProperty has a value of 4.32, I want the input to display 4.32%. I don't want someProperty to equal 4.32%. The value stays the same. Only the display changes. When the page renders, the % character shows up after the value. When the user is changing the input value, only the number shows and when the input loses focus, the % shows again.
I know this can be done using a directive but I am not sure how. I am not proficient in AngularJS.
Use this
angular
.module("docsSimpleDirective", [])
.controller("Controller", [
"$scope",
function($scope) {
$scope.text = '';
}
])
.directive("percent", function() {
return {
restrict: "A",
transclude: true,
scope: {},
link: function(scope, element, attr) {
element.on("focusout", function() {
var str = element.val();
element.val(str + "%");
});
element.on("focus", function() {
var str = element.val();
element.val(str.substring(0, str.length - 1));
});
}
};
});
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="docsSimpleDirective">
<input type "text" id="someProperty" percent ng-model="text"> {{text}}
</div>
vm.percentSymbol = function (type) {
vm.someProperty = vm.replace(/\%/g, '') + '%';
};
<input type=text= ng-model="vm.someProperty" ng-blur = "vm.percentSymbol()">
In your html
<input ng-focus="onFocus(someProperty)" ng-blur="onBlur(someProperty)" ng-model="someProperty" />
in your controller
$scope.someProperty= '';
$scope.onFocus = function(someProperty) {
$scope.someProperty = (someProperty.length >0)? someProperty.split("%")[0] : '';
}
$scope.onBlur = function(someProperty) {
$scope.someProperty = (someProperty.length >0)? someProperty+'%' : '';
}
This working fine.. Hope this will help.

Directive for comparing two dates

I have used following code for directive which compares two dates (reference Custom form validation directive to compare two fields)
define(['./module'], function(directives) {
'use strict';
directives.directive('lowerThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
ctrl.$setValidity('lowerThan', false);
var validate = function(viewValue) {
var comparisonModel = $attrs.lowerThan;
/*if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
//console.log("It's valid because we have nothing to compare against");
ctrl.$setValidity('lowerThan', true);
}*/
// It's valid if model is lower than the model we're comparing against
//ctrl.$setValidity('lowerThan', parseInt(viewValue, 10) < parseInt(comparisonModel, 10) );
if(comparisonModel){
var to = comparisonModel.split("-");
var t = new Date(to[2], to[1] - 1, to[0]);
}
if(viewValue){
var from=viewValue.split("-");
var f=new Date(from[2],from[1]-1,from[0]);
}
console.log(Date.parse(t)>Date.parse(f));
ctrl.$setValidity('lowerThan', Date.parse(t)>Date.parse(f));
return viewValue;
};
ctrl.$parsers.unshift(validate);
ctrl.$formatters.push(validate);
$attrs.$observe('lowerThan', function(comparisonModel){
// Whenever the comparison model changes we'll re-validate
return validate(ctrl.$viewValue);
});
};
return {
require: 'ngModel',
link: link
};
}
]);
});
but when page is loaded first time it displays error message. i have tried using ctrl.$setValidity('lowerThan', false); to make it invisible first time. but it is not working.
Here is plunker for the same.
http://plnkr.co/edit/UPN1g1JEoQMSUQZoCDAk?p=preview
Your directive is fine. You're setting your date values inside the controller, and you're setting the lower date to a higher value, which means the dates are invalid on load. Your directive correctly detects that. If you don't want your directive to validate your data on load, than you'll need three things:
Remove the $attrs.$observe
Create and apply a higherThan directive to the other field
Tell your directive not to apply to the model value ($formatters array) but only to the input value ($parsers array).
PLUNKER
'use strict';
var app = angular.module('myApp', []);
app.controller('MainCtrl', function($scope) {
$scope.field = {
min: "02-04-2014",
max: "01-04-2014"
};
});
app.directive('lowerThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
var validate = function(viewValue) {
var comparisonModel = $attrs.lowerThan;
var t, f;
if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
ctrl.$setValidity('lowerThan', true);
}
if (comparisonModel) {
var to = comparisonModel.split("-");
t = new Date(to[2], to[1] - 1, to[0]);
}
if (viewValue) {
var from = viewValue.split("-");
f = new Date(from[2], from[1] - 1, from[0]);
}
ctrl.$setValidity('lowerThan', Date.parse(t) > Date.parse(f));
// It's valid if model is lower than the model we're comparing against
return viewValue;
};
ctrl.$parsers.unshift(validate);
//ctrl.$formatters.push(validate);
};
return {
require: 'ngModel',
link: link
};
}
]);
app.directive('higherThan', [
function() {
var link = function($scope, $element, $attrs, ctrl) {
var validate = function(viewValue) {
var comparisonModel = $attrs.higherThan;
var t, f;
if(!viewValue || !comparisonModel){
// It's valid because we have nothing to compare against
ctrl.$setValidity('higherThan', true);
}
if (comparisonModel) {
var to = comparisonModel.split("-");
t = new Date(to[2], to[1] - 1, to[0]);
}
if (viewValue) {
var from = viewValue.split("-");
f = new Date(from[2], from[1] - 1, from[0]);
}
ctrl.$setValidity('higherThan', Date.parse(t) < Date.parse(f));
// It's valid if model is higher than the model we're comparing against
return viewValue;
};
ctrl.$parsers.unshift(validate);
//ctrl.$formatters.push(validate);
};
return {
require: 'ngModel',
link: link
};
}
]);
<form name="form" >
Min: <input name="min" type="text" ng-model="field.min" lower-than="{{field.max}}" />
<span class="error" ng-show="form.min.$error.lowerThan">
Min cannot exceed max.
</span>
<br />
Max: <input name="max" type="text" ng-model="field.max" higher-than="{{field.min}}" />
<span class="error" ng-show="form.max.$error.higherThan">
Max cannot be lower than min.
</span>
</form>

Word counter in angularjs

I'm a newbie in angular so please bear with me. I have a character counter and word counter in my textarea. My problem is that everytime I press the space, it is also being counted by getWordCounter function. How can I fix this? Thank you in advance.
HTML:
<textarea id="notesContent" type="text" class="form-control" rows="10" ng-model="notesNode.text" ng-trim="false" maxlength="5000"></textarea>
<span class="wordCount">{{getWordCounter()}}</span>
<span style="float:right">{{getCharCounter()}} / 5000</span>
JS:
$scope.getCharCounter = function() {
return 5000 - notesNode.text.length;
}
$scope.getWordCounter = function() {
return $.trim(notesNode.text.split(' ').length);
}
It seems like you need to call 'trim' before calling split, like this:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(' ').length;
}
If you want to support multiple spaces between words, use a regular expression instead:
$scope.getWordCounter = function() {
return notesNode.text.trim().split(/\s+/).length;
}
Filter implementation
You can also implement wordCounter as a filter, to make it reusable among different views:
myApp.filter('wordCounter', function () {
return function (value) {
if (value && (typeof value === 'string')) {
return value.trim().split(/\s+/).length;
} else {
return 0;
}
};
});
Then, in the view, use it like this:
<span class="wordCount">{{notesNode.text|wordCounter}</span>
See Example on JSFiddle
This is a more advanced answer for your problem, since it can be reusable as a directive:
var App = angular.module('app', []);
App.controller('Main', ['$scope', function($scope){
var notesNode = {
text: '',
counter: 0
};
this.notesNode = notesNode;
}]);
App.directive('counter', [function(){
return {
restrict: 'A',
scope: {
counter: '='
},
require: '?ngModel',
link: function(scope, el, attr, model) {
if (!model) { return; }
model.$viewChangeListeners.push(function(){
var count = model.$viewValue.split(/\b/g).filter(function(i){
return !/^\s+$/.test(i);
}).length;
scope.counter = count;
});
}
};
}]);
And the HTML
<body ng-app="app">
<div ng-controller="Main as main"></div>
<input type="text" ng-model="main.notesNode.text" class="county" counter="main.notesNode.counter">
<span ng-bind="main.notesNode.counter"></span>
</body>
See it in here http://plnkr.co/edit/9blLIiaMg0V3nbOG7SKo?p=preview
It creates a two way data binding to where the count should go, and update it automatically for you. No need for extra shovelling inside your scope and controllers code, plus you can reuse it in any other input.

Angular : how to re-render compiled template after model update?

I am working on an angular form builder which generate a json.
Everything works fine except one thing.
You can find an example here : http://jsfiddle.net/dJRS5/8/
HTML :
<div ng-app='app'>
<div class='formBuilderWrapper' id='builderDiv' ng-controller="FormBuilderCtrl" >
<div class='configArea' data-ng-controller="elementDrag">
<h2>drag/drop</h2>
<form name="form" novalidate class='editBloc'>
<div data-ng-repeat="field in fields" class='inputEdit'>
<data-ng-switch on="field.type">
<div class='labelOrder' ng-class='{column : !$last}' drag="$index" dragStyle="columnDrag" drop="$index" dropStyle="columnDrop">{{field.type}}
</div>
<label for="{{field.name}}" data-ng-bind-html-unsafe="field.caption"></label>
<input data-ng-switch-when="Text" type="text" placeholder="{{field.placeholder}}" data-ng-model="field.value" />
<p data-ng-switch-when="Text/paragraph" data-ng-model="field.value" data-ng-bind-html-unsafe="field.paragraph"></p>
<span data-ng-switch-when="Yes/no question">
<p data-ng-bind-html-unsafe="field.yesNoQuestion"></p>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_yes" value="yesNoQuestion_yes" />
<label for="yesNoQuestion_yes">Oui</label>
<input type='radio' name="yesNoQuestion" id="yesNoQuestion_no" value="yesNoQuestion_no"/>
<label for="yesNoQuestion_no">Non</label>
</span>
<p data-ng-switch-when="Submit button" class='submit' data-ng-model="field.value">
<input value="{{field.name}}" type="submit">
</p>
</data-ng-switch>
</div>
</form>
</div>
<div id='previewArea' data-ng-controller="formWriterCtrl">
<h2>preview</h2>
<div data-ng-repeat="item in fields" content="item" class='templating-html'></div>
</div>
</div>
</div>
The JS :
var app = angular.module('app', []);
app.controller('FormBuilderCtrl', ['$scope', function ($scope){
$scope.fields = [{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
}]);
app.controller('elementDrag', ["$scope", "$rootScope", function($scope, $rootScope, $compile) {
$rootScope.$on('dropEvent', function(evt, dragged, dropped) {
if($scope.fields[dropped].type == 'submitButton' || $scope.fields[dragged].type == 'submitButton'){
return;
}
var tempElement = $scope.fields[dragged];
$scope.fields[dragged] = $scope.fields[dropped];
$scope.fields[dropped] = tempElement;
$scope.$apply();
});
}]);
app.directive("drag", ["$rootScope", function($rootScope) {
function dragStart(evt, element, dragStyle) {
if(element.hasClass('column')){
element.addClass(dragStyle);
evt.dataTransfer.setData("id", evt.target.id);
evt.dataTransfer.effectAllowed = 'move';
}
};
function dragEnd(evt, element, dragStyle) {
element.removeClass(dragStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
attrs.$set('draggable', 'true');
scope.dragStyle = attrs["dragstyle"];
element.bind('dragstart', function(evt) {
$rootScope.draggedElement = scope[attrs["drag"]];
dragStart(evt, element, scope.dragStyle);
});
element.bind('dragend', function(evt) {
dragEnd(evt, element, scope.dragStyle);
});
}
}
}
}]);
app.directive("drop", ['$rootScope', function($rootScope) {
function dragEnter(evt, element, dropStyle) {
element.addClass(dropStyle);
evt.preventDefault();
};
function dragLeave(evt, element, dropStyle) {
element.removeClass(dropStyle);
};
function dragOver(evt) {
evt.preventDefault();
};
function drop(evt, element, dropStyle) {
evt.preventDefault();
element.removeClass(dropStyle);
};
return {
restrict: 'A',
link: function(scope, element, attrs) {
if(scope.$last === false){
scope.dropStyle = attrs["dropstyle"];
element.bind('dragenter', function(evt) {
dragEnter(evt, element, scope.dropStyle);
});
element.bind('dragleave', function(evt) {
dragLeave(evt, element, scope.dropStyle);
});
element.bind('dragover', dragOver);
element.bind('drop', function(evt) {
drop(evt, element, scope.dropStyle);
var dropData = scope[attrs["drop"]];
$rootScope.$broadcast('dropEvent', $rootScope.draggedElement, dropData);
});
}
}
}
}]);
app.controller('formWriterCtrl', ['$scope', function ($scope){
}]);
app.directive('templatingHtml', function ($compile) {
var previousElement;
var previousIndex;
var i=0;
var inputs = {};
var paragraphTemplate = '<p data-ng-bind-html-unsafe="content.paragraph"></p>';
var noYesQuestionTemplate = '<p data-ng-bind-html-unsafe="content.yesNoQuestion"></p><input id="a__index__yes" type="radio" name="a__index__"><label for="a__index__yes" />{{content.yes}}</label><input id="a__index__no" class="no" type="radio" name="a__index__" /><label for="a__index__no">{{content.no}}</label>';
var submitTemplate = '<p class="submit"><input value="{{content.name}}" type="submit" /></p>';
var getTemplate = function(contentType, contentReplace, contentRequired) {
var template = '';
switch(contentType) {
case 'Text/paragraph':
template = paragraphTemplate;
break;
case 'Yes/no question':
template = noYesQuestionTemplate;
break;
case 'Submit button':
template = submitTemplate;
break;
}
template = template.replace(/__index__/g, i);
return template;
}
var linker = function(scope, element, attrs) {
i++;
elementTemplate = getTemplate(scope.content.type);
element.html(elementTemplate);
if(previousElement == 'Yes/no question'){
element.children().addClass('hidden');
element.children().addClass('noYes'+previousIndex);
}
if(scope.content.type == 'Yes/no question'){
previousElement = scope.content.type;
previousIndex = i;
}
$compile(element.contents())(scope);
}
return {
restrict: "C",
link: linker,
scope:{
content:'='
}
};
});
On the example there are 2 areas :
- the first one does a ngRepeat on Json and allow to reorder items with drag and drop
- the second area also does a ngRepeat, it is a preview templated by a directive using compile function. Some elements are hidden if they are after what I called "Yes/no question"
Here is an example of Json generated by the form builder :
$scope.fields =
[{"type":"Text/paragraph","paragraph":"hello1"},{"type":"Yes/no question","yesNoQuestion":"following items must be hidden","yes":"yes","no":"no"},
{"type":"Text/paragraph","paragraph":"hello2"},{"type":"Submit button","name":"last item"}] ;
When the page load everything is ok, Hello1 is visible and Hello2 is hidden.
But when I drop Hello1 after "Yes/no question", dom elements are reorganised but Hello1 is not hidden.
I think it comes from $compile but I don't know how to resolve it.
Could you help me with this please?
Thank you
I only see you setting the 'hidden' class on the element based on that rule (after a yes/no) in the link function. That's only called once for the DOM element - when it's first created. Updating the data model doesn't re-create the element, it updates it in place. You would need a mechanism that does re-create it if you wanted to do it this way.
I see three ways you can do this:
In your linker function, listen for the same dropEvent that you listen for above. This is more efficient than you'd think (it's very fast) and you can re-evaluate whether to apply this hidden class or not.
Use something like ngIf or literally re-creating it in your collection to force the element to be recreated entirely. This is not as efficient, but sometimes is still desirable for various reasons.
If your use case is actually this simple (if this wasn't a redux of something more complicated you're trying to do) you could use CSS to do something like this. A simple rule like
.yes-no-question + .text-paragraph { display: none; }
using a sibling target could handle this directly without as much work. This is much more limited in what it can do, obviously, but it's the most efficient option if it covers what you need.

AngularJS - Binding only changed model properties with animation

I have an app that polls the server for a list of items and displays the results. Example fiddle here.
I would like some custom animation when the model changes but only for the properties that have changed value i.e. Item2 in the example.
I'm replacing the entire items collection in the controller each time it polls, how do I update the current list with only those values that have changed? Do I need to loop through and compare or is there a nicer way?
Is the example the best way to fire every time the bound value within the span changes value?
HTML:
<div ng-app="myApp">
<div ng-controller='ItemCtrl'>
<div ng-repeat="item in items">
<div>{{item.Name}}</div>
<div><span>Item1: </span><span animate ng-model="item.Item1"></span></div>
<div><span>Item2: </span><span animate ng-model="item.Item2"></span></div>
<div><br>
</div>
</div>
JS:
var myApp = angular.module('myApp', []);
myApp.controller("ItemCtrl",
['$scope', '$timeout', 'getItems1', 'getItems2',
function ($scope, $timeout, getItems1, getItems2) {
$scope.items = [];
var change = false;
(function tick() {
bindItems();
$timeout(tick, 2000);
})();
function bindItems() {
if (change) {
$scope.items = getItems2;
}
else if (!change){
$scope.items = getItems1;
}
change = !change;
}
}]);
myApp.factory('getItems1', function() {
return [
{
Name: 'foo1',
Item1: 1,
Item2: 2
},
{
Name: 'foo2',
Item1: 3,
Item2: 4
},
];
});
myApp.factory('getItems2', function() {
return [
{
Name: 'foo1',
Item1: 1,
Item2: 6
},
{
Name: 'foo2',
Item1: 3,
Item2: 8
},
];
});
myApp.directive('animate', function(){
// Some custom animation when the item within this span changes
return {
require: 'ngModel',
link: function(scope, elem, attrs, ngModel) {
scope.$watch(function() {
return ngModel.$modelValue;
}, function (newValue, oldValue, other) {
var $elem = $(elem);
console.log(newValue + ' ' + oldValue + ' ' + other);
// I don't want to animate if the values are the same, but here
// oldValue and newValue are the same, because I'm replacing the
// entire items list??
//if (newValue === oldValue)
//{
// $elem.html(newValue);
//} else
{
// oldValue same as newValue, because I'm replacing the entire items
// list??
$elem.html(oldValue);
$elem.slideUp(1000, function() {
$elem.html(newValue);
}).slideDown(1000);
}
});
}
};
})
UPDATE:
Got this working by looping through the list and updating properties individually. jsfiddle
Though feel there should be a better way where a) no need to loop through properties, b) can hook into before and after events on the watch, so no need to set the value using .html()

Resources