AngularJS - Binding only changed model properties with animation - angularjs

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()

Related

Angular Select Option Class Expression

I cant change the option class attribute. OptionClass actually work. I founded it from the this site. But my real code not work. I dont know where is the bug.
I only want to change the options background-color with expression, for example if id>100 then background color is red etc.
My Select
<select ng-model="firstSection" ng-options="o.label for o in options" multiple
class="invoiceSelect" options-class="{ 'is-eligible' : Id<100, 'not-eligible': !Id<100 }"></select>
My Filling methods
var myApp = angular.module('app', []);
myApp.controller('myCtrl', function ($scope, $http, $filter) {
$scope.getInvoiceByRfiPolicyId = function (rfiPolicyId) {
$http.get('http://somesitelink.com/' + rfiPolicyId)
.then(function (response) {
$scope.options = response.data.map(o=> {
return { label: 'Numbers:' + o.InvoiceNumber + ' InvDate:' + $scope.dateParse(o.InvoiceDate), value: o.InvoiceId, Id:o.InvoiceId, eligible: true }
});
});
}
});
Here is my optionClass function
myApp.directive('optionsClass', function ($parse) {
return {
require: 'select',
link: function (scope, elem, attrs, ngSelect) {
var parts = attrs.ngOptions.split(' ');
var optionsSourceStr = parts[parts.indexOf('in') + 1];
var getOptionsClass = $parse(attrs.optionsClass);
scope.$watchCollection(optionsSourceStr, function (items) {
scope.$$postDigest(function () {
angular.forEach(items, function (item, index) {
var classes = getOptionsClass(item),
option = elem.find('option[value="' + item.id + '"]');
angular.forEach(classes, function (add, className) {
if (add) {
angular.element(option).addClass(className);
}
});
});
});
});
}
};
});
A working example is given in the embedded code snippet below.
You must have got the solution from here. There are two issues in the code used:
1. Finding option element
find method in this statement option = elem.find('option[value="' + item.id + '"]') tries to look using attribute selector but find method of jqLite (which is embedded in AngularJS) is limited to look up by tag names only so this doesn't work in your code. Added a simple method to find the required option.
It is working in the question that you referred because code pasted there had an assumption to be working with jQuery and find method in jQuery allows you to find based on a variety of selectors.
2. Expressions in options-class directive
Group the expressions using (<expression>) as shown here options-class="{ 'is-eligible' : (Id<2), 'not-eligible': !(Id<2) }", otherwise ! negation operator will first negate the Id value and then perform the comparison.
angular.module('ExampleApp', [])
.controller('ExampleController', function($scope) {
$scope.options = [{
label: 'Numbers: Invoice001',
value: 1,
Id: 1,
eligible: true
},
{
label: 'Numbers: Invoice002',
value: 2,
Id: 2,
eligible: true
},
{
label: 'Numbers: Invoice003',
value: 3,
Id: 3,
eligible: true
}
];
})
.directive('optionsClass', function($parse) {
return {
require: 'select',
link: function(scope, elem, attrs, ngSelect) {
function findOptionByValueAttr(options, findByValue) {
for (var i = 0; i < options.length; i++) {
var option = angular.element(options[i]);
if (option.attr('value') === findByValue.toString()) {
return option;
}
}
}
var parts = attrs.ngOptions.split(' ');
var optionsSourceStr = parts[parts.indexOf('in') + 1];
console.log(attrs.optionsClass);
var getOptionsClass = $parse(attrs.optionsClass);
scope.$watchCollection(optionsSourceStr, function(items) {
angular.forEach(items, function(item, index) {
console.log(getOptionsClass(item));
var classes = getOptionsClass(item),
/*option = elem.find('option[value="' + item.Id + '"]');*/
option = findOptionByValueAttr(elem.children(), item.Id);
console.log(item);
angular.forEach(classes, function(add, className) {
if (add) {
option.addClass(className);
}
});
});
});
} // link function ends
}; // return value ends
}); // directive ends
.is-eligible {
background-color: lightgreen;
}
.not-eligible {
background-color: red;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<div ng-app="ExampleApp" ng-controller="ExampleController">
<select ng-model="firstSection" ng-options="o.label for o in options track by o.Id" multiple class="invoiceSelect" options-class="{ 'is-eligible' : (Id<2), 'not-eligible': !(Id<2) }"></select>
</div>
Design Suggestions
Styling the options is not recommended as this behavior is not consistent across browsers and option elements are rendered differently on mobile.
You may check following design considerations to avoid styling:
Modify the label and/or disable those options which you eventually want the user to NOT select. This is possible using ng-options.
On selecting an option, highlight the external surrounding parent area enclosing the select element to alert the user to understand the difference and may accompany with a supporting text, disabling the relevant areas, submit buttons etc.
Hope these will be helpful.

Angular Typeahead dynamically added results

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]);
}
};
});

How to get async html attribut

I have a list of items retreived by an async call and the list is shown with the help of ng-repeat. Since the div container of that list has a fixed height (400px) I want the scrollbar to be at the bottom. And for doing so I need the scrollHeight. But the scrollHeight in postLink is not the final height but the initial height.
Example
ppChat.tpl.html
<!-- Height of "chatroom" is "400px" -->
<div class="chatroom">
<!-- Height of "messages" after all messages have been loaded is "4468px" -->
<div class="messages" ng-repeat="message in chat.messages">
<chat-message data="message"></chat-message>
</div>
</div>
ppChat.js
// [...]
compile: function(element) {
element.addClass('pp-chat');
return function(scope, element, attrs, PpChatController) {
var messagesDiv;
// My idea was to wait until the messages have been loaded...
PpChatController.messages.$loaded(function() {
// ...and then recompile the messages div container
messagesDiv = $compile(element.children()[0])(scope);
// Unfortunately this doesn't work. "messagesDiv[0].scrollHeight" still has its initial height of "400px"
});
}
}
Can someone explain what I missed here?
As required here is a plunk of it
You can get the scrollHeight of the div after the DOM is updated by doing it in the following way.
The below directive sets up a watch on the array i.e. a collection, and uses the $timeout service to wait for the DOM to be updated and then it scrolls to the bottom of the div.
chatDirective.$inject = ['$timeout'];
function chatDirective($timeout) {
return {
require: 'chat',
scope: {
messages: '='
},
templateUrl: 'partials/chat.tpl.html',
bindToController: true,
controllerAs: 'chat',
controller: ChatController,
link: function(scope, element, attrs, ChatController) {
scope.$watchCollection(function () {
return scope.chat.messages;
}, function (newValue, oldValue) {
if (newValue.length) {
$timeout(function () {
var chatBox = document.getElementsByClassName('chat')[0];
console.log(element.children(), chatBox.scrollHeight);
chatBox.scrollTop = chatBox.scrollHeight;
});
}
});
}
};
}
The updated plunker is here.
Also in your Controller you have written as,
var Controller = this;
this.messages = [];
It's better to write in this way, here vm stands for ViewModel
AppController.$inject = ['$timeout'];
function AppController($timeout) {
var vm = this;
vm.messages = [];
$timeout(
function() {
for (var i = 0; i < 100; i++) {
vm.messages.push({
message: getRandomString(),
created: new Date()
});
}
},
3000
);
}

How to show star-rating dynamically based on response?

I need to display star rating dynamically based on response.
I am able to display values from 1 to 5 but if rating is 0 then no empty stars are displaying.
If rating = 0.4 also it's showing 1 star filled.
My controller:
(function() {
'use strict';
angular
var app = angular
.module('app')
app.directive('starRating', function () {
return {
restrict: 'A',
template: '<ul class="rating">' +
'<li ng-repeat="star in stars" ng-class="star" ng-click="toggle($index)">' +
'\u2605' +
'</li>' +
'</ul>',
scope: {
ratingValue: '=',
max: '='
},
link: function (scope, elem, attrs) {
var updateStars = function () {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
if(i == 0) {
scope.stars = [];
scope.stars.push({
empty: i = 0
});
} else {
scope.stars.push({
filled: i < scope.ratingValue
});
}
}
};
scope.$watch('ratingValue', function (oldVal, newVal) {
if (newVal) {
updateStars();
}
});
}
}
});
app.controller('Controller', Controller);
Controller.$inject = ['UserService', '$location', '$rootScope', '$scope', 'fileUpload', 'FlashService', '$cookieStore', '$timeout', '$window'];
function Controller(UserService, $location, $rootScope, $scope, fileUpload, FlashService, $cookieStore, $timeout, $window) {
$scope.selectTestSubject = function() {
$scope.rating = 0;
console.log(levels.length);
for(var k=0; k<levels.length; k++) {
var respRating = levels[k].rating;
// var respRating = 1.5;
console.log(respRating);
$scope.ratings = [{
current: respRating,
max: 5
}];
if(respRating == 0) {
$scope.defaultRating = true;
} else {
$scope.defaultRating = false;
}
}
}
}
}) ();
My HTML page:
<div><span ng-repeat="rating in ratings">
<div star-rating rating-value="rating.current" max="rating.max"></div>
</span>
</div>
One problem with your solution is your $watch expression. Where you have the following:
scope.$watch('ratingValue', function (oldVal, newVal) {
if (newVal) {
updateStars();
}
});
oldVal and newVal are actually the wrong way around, the $watch function first takes in the new value followed by the old value. Secondly, the condition if (newVal) doesn't work for 0, because 0 is a falsey value.
Instead, you should have:
scope.$watch('ratingValue', function(value, previousValue) {
// Only update the view when the value has changed.
if (value !== previousValue) {
updateStars();
}
});
Your updateStars function also always reinitialises the scope.stars variable and appends onto it. Doing this can have some unwanted side effects and results in the view not reflecting the model value. It's best to initialise the array, then append the item if it doesn't yet exist or update the existing value. So you'll have something like this:
// Initialise the stars array.
scope.stars = [];
var updateStars = function() {
for (var i = 0; i < scope.max; i++) {
var filled = i < Math.round(scope.ratingValue);
// Check if the item in the stars array exists and
// append it, otherwise update it.
if (scope.stars[i] === undefined) {
scope.stars.push({
filled: filled
});
} else {
scope.stars[i].filled = filled;
}
}
};
Since the $watch expression only updates the stars when the value has changed, you'll now need to trigger the update the first time your link function fires. So this is simply:
// Trigger an update immediately.
updateStars();
Your template also does not correctly utilise the filled property on the star, it should instead contain the appropriate ng-class like so:
<ul class="rating">
<li class="star"
ng-repeat="star in stars"
ng-class="{ filled: star.filled }"
ng-click="toggle($index)">
\u2605
</li>
</ul>
With a simple style,
.star {
cursor: pointer;
color: black;
}
.star.filled {
color: yellow;
}
You can also improve this rating system by listening to mouseenter and mouseleave effects, so that the stars appear yellow when the user is selecting a new value. This is pretty common functionality. You can achieve this by making a few modifications.
To begin with, the template should be updated to listen for these events:
<ul class="rating">
<li class="star"
ng-repeat="star in stars"
ng-class="{ filled: star.filled }"
ng-mouseenter="onMouseEnter($event, $index + 1)"
ng-mouseleave="onMouseLeave($event)"
ng-click="toggle($index)">
\u2605
</li>
</ul>
Next, we want to make a small adjustment to the updateStars function to take in a rating parameter:
var updateStars = function(rating /* instead of blank */ ) {
for (var i = 0; i < scope.max; i++) {
var filled = i < Math.round(rating); // instead of scope.ratingValue
// Check if the item in the stars array exists and
// append it, otherwise update it.
if (scope.stars[i] === undefined) {
scope.stars.push({
filled: filled
});
} else {
scope.stars[i].filled = filled;
}
}
};
// Trigger an update immediately.
updateStars(scope.ratingValue /* instead of blank */ );
scope.$watch('ratingValue', function(value, previousValue) {
// Only update the view when the value changed.
if (value !== previousValue) {
updateStars(scope.ratingValue /* instead of blank */ );
}
});
Now we can add in our event callbacks from the view,
// Triggered when the cursor enters a star rating (li element).
scope.onMouseEnter = function (event, rating) {
updateStars(rating);
};
// Triggered when the cursor leaves a star rating.
scope.onMouseLeave = function (event) {
updateStars(scope.ratingValue);
};
And that's it! Full demo here.

$watch not finding changes on property in array of objects on new item in angular

I have an array of objects like this...
[{ name: 'foo', price: 9.99, qty: 1 }]
The UI allows for new items to be added to this array. I'm trying to listen for those new items to be added AND for changes on the qty property of each item.
$scope.order = [];
$scope.totalItems = 0;
$scope.$watchCollection('order', function() {
$scope.totalItems = $scope.order.reduce(function(memo, o) {
return memo + o.qty;
}, 0);
});
$scope.addItem = function() {
$scope.order.push({
name: $scope.item.name,
qty: 1,
options: []
});
};
As you can see from this... http://d.pr/v/MJzP The function fires when a new item is added, but NOT when the qty changes on that new item.
Maybe you should attach an ngChange to your quantity input box? Like this
<input ng-model="item.qty" ng-change="calculateTotal()" type="number"/>
Then in your controller:
$scope.calculateTotal = function() {
$scope.totalItems = $scope.order.reduce(function(memo, o) {
return memo + o.qty;
}, 0);
};
Then at the end of your add item function:
$scope.addItem = function() {
// Your logic
$scope.calculateTotal();
};
EDIT: After thinking about this more, the ngChange might automatically be invoked when the addItem function is called so calling it from addItem might not be entirely necessary

Resources