Customize ng-options selection look - angularjs

I'm using a ng-options for a select dropdown menu. I would like to use different color for an option depending on a condition:
select(ng-model='myCompany', ng-options='company.code as company.name for company in companies' **if company.active -> text-color=green**)
Is it possible to do that?
Edit (my jade code):
form(role='form', name='addForm', novalidate, rc-submit="add()")
.form-group
div.row
div.col-xs-12.col-md-3
select.form-control(ng-model='form.contract', ng-options='contract as contract.number for contract in contracts', options-class="{true:'active',false:'inactive'}[active]")

If you only need to bind the select to string values (not object), you can easily achieve what you want by using ngRepeated <option> elements (instead of ngOptions):
<select ng-model="color">
<option value="">--- Select a color ---</option>
<option value="{{ c }}" style="background-color:{{ c }}" ng-repeat="c in colors">
{{ c }}
</option>
</select>
If you are in for a little custom directive, you can implement it like this:
app.directive('optionClassExpr', function ($compile, $parse) {
const NG_OPTIONS_REGEXP = /^\s*([\s\S]+?)(?:\s+as\s+([\s\S]+?))?(?:\s+group\s+by\s+([\s\S]+?))?\s+for\s+(?:([\$\w][\$\w]*)|(?:\(\s*([\$\w][\$\w]*)\s*,\s*([\$\w][\$\w]*)\s*\)))\s+in\s+([\s\S]+?)(?:\s+track\s+by\s+([\s\S]+?))?$/;
return {
restrict: 'A',
link: function optionClassExprPostLink(scope, elem, attrs) {
const optionsExp = attrs.ngOptions;
if (!optionsExp) return;
const match = optionsExp.match(NG_OPTIONS_REGEXP);
if (!match) return;
const values = match[7];
const classExpr = $parse(attrs.optionClassExpr);
scope.$watchCollection(() => elem.children(), newValue => {
angular.forEach(newValue, child => {
const child = angular.element(child);
const val = child.val();
if (val) {
child.attr('ng-class', `${values}[${val}].${attrs.optionClassExpr}`);
$compile(child)(scope);
}
});
});
}
};
});
And use it like this:
<select
ng-model="someObj"
ng-options="obj as obj.color for obj in objects"
option-class-expr="color">
</select>
See, also, this updated short demo.

Define your CSS classes:
.green {
background-color: green;
}
.blue {
background-color: blue;
}
and use a function as an angular expression:
$scope.getCompanyClass = function(company)
{
return company.active ? "green" : "blue";
}
and in your html:
<select>
<option data-ng-repeat="company in companies" data-ng-class='getCompanyClass(company)'>...</option>
</select>
and the working example as jsfiddle

There is a neat solution for the special case that you want to show disabled options in a certain color. You can use disable when:
ng-options='c.code as c.name disable when !c.active for c in companies'
You can then use CSS to match on the disabled attribute and style the respective options the way you like.

Please see here http://jsbin.com/wahak/2/edit?html,css,console,output you can do that using css
CSS:
select { color: red; }
option:not(:checked) { color: black; }

This is how I am colouring the ng-options. I am using my own example.
var app = angular.module("app", []);
app.controller("homeController", [homeController]);
function homeController() {
var vm = this;
vm.differentOptions = [
{ name: "External Visitors", color: "Red" },
{ name: "Internal Visitors", color: "Green" },
{ name: "Other Visitors", color: "Gray" },
{ name: "Extrateresstrial Visitors", color: "Yellow" }
];
}
angular.module("app").directive("colorTheOptions", colorTheOptions);
function colorTheOptions($timeout) {
return {
link: function (scope, element) {
$timeout(function () {
var options = $("option", element);
options.each(function (index, eachOption) {
$eachOption = $(eachOption);
var optionText = $eachOption.text();
if (optionText) {
for (var i = 0; i < scope.vm.differentOptions.length; i++) {
var eachAngularOption = scope.vm.differentOptions[i];
if (eachAngularOption.name === optionText) {
$eachOption.addClass(eachAngularOption.color);
}
}
}
});
});
}
}
}
.Red {
color: red;
}
.Green {
color: green;
}
.Gray {
color: gray;
}
.Yellow {
color: yellow;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha.6/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.9.1/jquery.min.js"></script>
<div class="container" ng-app="app">
<div class="row" ng-controller="homeController as vm">
<select class="form-control" color-the-options
ng-options="option.name for option in vm.differentOptions"
ng-model="vm.selectedOption"></select>
</div>
</div>

Related

AngularJS - NVDA screen reader not finding names of child elements

Apologies for the bare-bones HTML here...
I've got some AngularJS components that are rendering this HTML for a multiselectable dropdown:
<ul role="listbox">
<li>
<div ng-attr-id="ui-select-choices-row-{{ $select.generatedId }}-{{$index}}" class="ui-select-choices-row ng-scope" ng-class="{active: $select.isActive(this), disabled: $select.isDisabled(this)}" role="option" ng-repeat="opt in $select.items" ng-if="$select.open" ng-click="$select.select(opt,$select.skipFocusser,$event)" tabindex="0" id="ui-select-choices-row-0-1" style="">
<a href="" class="ui-select-choices-row-inner" uis-transclude-append="">
<span ng-class="{'strikethrough' : rendererInactive(opt)}" title="ALBANY" aria-label="ALBANY" class="ng-binding ng-scope">ALBANY</span>
</a>
</div>
(a hundred or so more options in similar divs)
</li>
</ul>
What we need is for screen reading software to speak aloud each option as it's highlighted via arrow key navigation. As it is now, NVDA says "blank" when keying through the list. If, in the directive we're using to create this HTML, I add role="presentation" to the <ul>, then NVDA will recite the entire list of options as soon as the dropdown opens, but not individually for each arrow key keystroke (and after hitting Escape to make it stop talking, keying through the options says "blank" again).
I keep thinking that the listbox and option roles are in the correct places, but is something else in the structure preventing the screen reader from finding the values correctly?
This answer got quite long, the first 3 points are most likely the problem, the rest are other considerations / observations
There are a few things that are likely to cause this issue, although without seeing the generated HTML rather than the Angular Source there could be others.
Most likely culprit is that your anchors are not valid. You cannot have a blank href (href="") for it to be valid. Looking at your source code could you not remove this and adjust your CSS or change it to a <div>?
Second most likely culprit is that role="option" should be on the direct children on role="listbox". Move it to your <li>s and make them selectable with tabindex="-1" (see below point on tabindex="0") instead. (in fact why not simply remove the surrounding <div> and apply all of your angular directives to the <li> directly).
Third most likely culprit is the fact that aria-label is not needed and may in fact be interfering, a screen reader will read the text within your <span> without this. Golden rule - do not use aria unless you can't portray the information another way.
You also need to add aria-selected="true" (or false) to each <li role="option"> to indicate whether an item is selected or not.
Also you should add aria-multiselectable="true" to the <ul> to indicate it is a multi select.
While you are at it, remove the title attribute, it doesn't add anything useful here.
aria-activedescendant="id" should be used to indicate which item is currently focused.
Be careful with tabindex="0" - I can't see if this is applied to everything but really it should be tabindex="-1" and you programatically manage focus as otherwise users could tab to items that they aren't meant to. tabindex="0" should be on the main <ul>.
Due to the complex nature of multi-selects you would be much better using a group of checkboxes as they provide a lot of the functionality for free, but that is just a suggestion.
The following example I found on codepen.io covers 95% of everything if you use a checkbox instead and would be a good base for you to pick apart and adapt to your needs, as you can see checkboxes make life a lot easier as all the selected not selected functionality is built in.
(function($){
'use strict';
const DataStatePropertyName = 'multiselect';
const EventNamespace = '.multiselect';
const PluginName = 'MultiSelect';
var old = $.fn[PluginName];
$.fn[PluginName] = plugin;
$.fn[PluginName].Constructor = MultiSelect;
$.fn[PluginName].noConflict = function () {
$.fn[PluginName] = old;
return this;
};
// Defaults
$.fn[PluginName].defaults = {
};
// Static members
$.fn[PluginName].EventNamespace = function () {
return EventNamespace.replace(/^\./ig, '');
};
$.fn[PluginName].GetNamespacedEvents = function (eventsArray) {
return getNamespacedEvents(eventsArray);
};
function getNamespacedEvents(eventsArray) {
var event;
var namespacedEvents = "";
while (event = eventsArray.shift()) {
namespacedEvents += event + EventNamespace + " ";
}
return namespacedEvents.replace(/\s+$/g, '');
}
function plugin(option) {
this.each(function () {
var $target = $(this);
var multiSelect = $target.data(DataStatePropertyName);
var options = (typeof option === typeof {} && option) || {};
if (!multiSelect) {
$target.data(DataStatePropertyName, multiSelect = new MultiSelect(this, options));
}
if (typeof option === typeof "") {
if (!(option in multiSelect)) {
throw "MultiSelect does not contain a method named '" + option + "'";
}
return multiSelect[option]();
}
});
}
function MultiSelect(element, options) {
this.$element = $(element);
this.options = $.extend({}, $.fn[PluginName].defaults, options);
this.destroyFns = [];
this.$toggle = this.$element.children('.toggle');
this.$toggle.attr('id', this.$element.attr('id') + 'multi-select-label');
this.$backdrop = null;
this.$allToggle = null;
init.apply(this);
}
MultiSelect.prototype.open = open;
MultiSelect.prototype.close = close;
function init() {
this.$element
.addClass('multi-select')
.attr('tabindex', 0);
initAria.apply(this);
initEvents.apply(this);
updateLabel.apply(this);
injectToggleAll.apply(this);
this.destroyFns.push(function() {
return '|'
});
}
function injectToggleAll() {
if(this.$allToggle && !this.$allToggle.parent()) {
this.$allToggle = null;
}
this.$allToggle = $("<li><label><input type='checkbox'/>(all)</label><li>");
this.$element
.children('ul:first')
.prepend(this.$allToggle);
}
function initAria() {
this.$element
.attr('role', 'combobox')
.attr('aria-multiselect', true)
.attr('aria-expanded', false)
.attr('aria-haspopup', false)
.attr('aria-labeledby', this.$element.attr("aria-labeledby") + " " + this.$toggle.attr('id'));
this.$toggle
.attr('aria-label', '');
}
function initEvents() {
var that = this;
this.$element
.on(getNamespacedEvents(['click']), function($event) {
if($event.target !== that.$toggle[0] && !that.$toggle.has($event.target).length) {
return;
}
if($(this).hasClass('in')) {
that.close();
} else {
that.open();
}
})
.on(getNamespacedEvents(['keydown']), function($event) {
var next = false;
switch($event.keyCode) {
case 13:
if($(this).hasClass('in')) {
that.close();
} else {
that.open();
}
break;
case 9:
if($event.target !== that.$element[0] ) {
$event.preventDefault();
}
case 27:
that.close();
break;
case 40:
next = true;
case 38:
var $items = $(this)
.children("ul:first")
.find(":input, button, a");
var foundAt = $.inArray(document.activeElement, $items);
if(next && ++foundAt === $items.length) {
foundAt = 0;
} else if(!next && --foundAt < 0) {
foundAt = $items.length - 1;
}
$($items[foundAt])
.trigger('focus');
}
})
.on(getNamespacedEvents(['focus']), 'a, button, :input', function() {
$(this)
.parents('li:last')
.addClass('focused');
})
.on(getNamespacedEvents(['blur']), 'a, button, :input', function() {
$(this)
.parents('li:last')
.removeClass('focused');
})
.on(getNamespacedEvents(['change']), ':checkbox', function() {
if(that.$allToggle && $(this).is(that.$allToggle.find(':checkbox'))) {
var allChecked = that.$allToggle
.find(':checkbox')
.prop("checked");
that.$element
.find(':checkbox')
.not(that.$allToggle.find(":checkbox"))
.each(function(){
$(this).prop("checked", allChecked);
$(this)
.parents('li:last')
.toggleClass('selected', $(this).prop('checked'));
});
updateLabel.apply(that);
return;
}
$(this)
.parents('li:last')
.toggleClass('selected', $(this).prop('checked'));
var checkboxes = that.$element
.find(":checkbox")
.not(that.$allToggle.find(":checkbox"))
.filter(":checked");
that.$allToggle.find(":checkbox").prop("checked", checkboxes.length === checkboxes.end().length);
updateLabel.apply(that);
})
.on(getNamespacedEvents(['mouseover']), 'ul', function() {
$(this)
.children(".focused")
.removeClass("focused");
});
}
function updateLabel() {
var pluralize = function(wordSingular, count) {
if(count !== 1) {
switch(true) {
case /y$/.test(wordSingular):
wordSingular = wordSingular.replace(/y$/, "ies");
default:
wordSingular = wordSingular + "s";
}
}
return wordSingular;
}
var $checkboxes = this.$element
.find('ul :checkbox');
var allCount = $checkboxes.length;
var checkedCount = $checkboxes.filter(":checked").length
var label = checkedCount + " " + pluralize("item", checkedCount) + " selected";
this.$toggle
.children("label")
.text(checkedCount ? (checkedCount === allCount ? '(all)' : label) : 'Select a value');
this.$element
.children('ul')
.attr("aria-label", label + " of " + allCount + " " + pluralize("item", allCount));
}
function ensureFocus() {
this.$element
.children("ul:first")
.find(":input, button, a")
.first()
.trigger('focus')
.end()
.end()
.find(":checked")
.first()
.trigger('focus');
}
function addBackdrop() {
if(this.$backdrop) {
return;
}
var that = this;
this.$backdrop = $("<div class='multi-select-backdrop'/>");
this.$element.append(this.$backdrop);
this.$backdrop
.on('click', function() {
$(this)
.off('click')
.remove();
that.$backdrop = null;
that.close();
});
}
function open() {
if(this.$element.hasClass('in')) {
return;
}
this.$element
.addClass('in');
this.$element
.attr('aria-expanded', true)
.attr('aria-haspopup', true);
addBackdrop.apply(this);
//ensureFocus.apply(this);
}
function close() {
this.$element
.removeClass('in')
.trigger('focus');
this.$element
.attr('aria-expanded', false)
.attr('aria-haspopup', false);
if(this.$backdrop) {
this.$backdrop.trigger('click');
}
}
})(jQuery);
$(document).ready(function(){
$('#multi-select-plugin')
.MultiSelect();
});
* {
box-sizing: border-box;
}
.multi-select, .multi-select-plugin {
display: inline-block;
position: relative;
}
.multi-select > span, .multi-select-plugin > span {
border: none;
background: none;
position: relative;
padding: .25em .5em;
padding-right: 1.5em;
display: block;
border: solid 1px #000;
cursor: default;
}
.multi-select > span > .chevron, .multi-select-plugin > span > .chevron {
display: inline-block;
transform: rotate(-90deg) scale(1, 2) translate(-50%, 0);
font-weight: bold;
font-size: .75em;
position: absolute;
top: .2em;
right: .75em;
}
.multi-select > ul, .multi-select-plugin > ul {
position: absolute;
list-style: none;
padding: 0;
margin: 0;
left: 0;
top: 100%;
min-width: 100%;
z-index: 1000;
background: #fff;
border: 1px solid rgba(0, 0, 0, 0.15);
box-shadow: 0 6px 12px rgba(0, 0, 0, 0.175);
display: none;
max-height: 320px;
overflow-x: hidden;
overflow-y: auto;
}
.multi-select > ul > li, .multi-select-plugin > ul > li {
white-space: nowrap;
}
.multi-select > ul > li.selected > label, .multi-select-plugin > ul > li.selected > label {
background-color: LightBlue;
}
.multi-select > ul > li.focused > label, .multi-select-plugin > ul > li.focused > label {
background-color: DodgerBlue;
}
.multi-select > ul > li > label, .multi-select-plugin > ul > li > label {
padding: .25em .5em;
display: block;
}
.multi-select > ul > li > label:focus, .multi-select > ul > li > label:hover, .multi-select-plugin > ul > li > label:focus, .multi-select-plugin > ul > li > label:hover {
background-color: DodgerBlue;
}
.multi-select.in > ul, .multi-select-plugin.in > ul {
display: block;
}
.multi-select-backdrop, .multi-select-plugin-backdrop {
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
z-index: 900;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<label id="multi-select-plugin-label" style="display:block;">Multi Select</label>
<div id="multi-select-plugin" aria-labeledby="multi-select-plugin-label">
<span class="toggle">
<label>Select a value</label>
<span class="chevron"><</span>
</span>
<ul>
<li>
<label>
<input type="checkbox" name="selected" value="0"/>
Item 1
</label>
</li>
<li>
<label>
<input type="checkbox" name="selected" value="1"/>
Item 2
</label>
</li>
<li>
<label>
<input type="checkbox" name="selected" value="2"/>
Item 3
</label>
</li>
<li>
<label>
<input type="checkbox" name="selected" value="3"/>
Item 4
</label>
</li>
</ul>
</div>
Also you will see that gov.uk uses a checkbox pattern (within the organisation filter on the left on the linked page) for their multi-selects (with a filter - something you may consider with 100 different options as they have highlighted some key concerns in this article).
As you can see (and I wasn't finished) there is a lot to consider.
Hope I haven't scared you too much and the first few points solve the issue you originally asked about!

Can I add style and ng-style both for a single element?

I am setting style and ng-style for a single element for two different actions which is not working.
If I add both together it is not working. both are working separately
<div class="glyphicon glyphicon-heart" style="width:{{wide}}%;" ng-style="{'color': (istrue()) ? 'red' : 'yellow' }"></div>
JS
$scope.wide = 50; // dynamic value
$scope.istrue = function () {
var abc = true;
if (abc) {
return true;
}
else {
return false;
}
}
You can also apply styles byadding dynamic class name like below:
HTML
<div class="glyphicon glyphicon-heart" style="width:78%;" ng-class="isArrabic()"></div>
JS
$scope.wide = 78; // dynamic value
$scope.isArrabic = function () {
var arabic = true;
if (arabic) {
return 'floatRight';
}
else {
return 'floatLeft';
}
}
CSS:
.floatRight {
float: right;
color: red;
}
.floatLeft {
float: left;
color: blue;
}

QuillJS Reload toolbar controls

I am using QuillJS and I need to add some controls to toolbar during runtime. Is there any way to make it from code after whole Quill has been initialized?
This is how I make it now.
quillEditor.getModule('toolbar').addHandler('color', (value) => {
if (value == 'new-color') {
value = prompt('Give me hex color baby!');
// unfortunately this code does not work
let n = toolbar.querySelector('select.ql-color');
n.innerHTML += '<option value="'+value+'"></option>';
}
quillEditor.format('color', value);
console.log("Color handler", value);
});
It looks like you're only adding the new options to the select element which is hidden. The element used in the UI to select colors is a span with the class ql-picker-options.
Check out this snippet
var tools = [
['bold', 'italic', 'underline', 'strike'],
[{'color': ['red', 'blue', 'green', 'new-color']}]
]
var quillEditor = new Quill('#editor-container', {
modules: {
toolbar: tools
},
theme: 'snow'
});
var toolbar = document.querySelector('.ql-toolbar');
quillEditor.getModule('toolbar').addHandler('color', (value) => {
if (value == 'new-color') {
value = prompt('Give me hex color baby!');
// This element is what the user sees
let uiSelect = toolbar.querySelector('.ql-color .ql-picker-options');
// This is a hidden element
let select = toolbar.querySelector('select.ql-color');
uiSelect.innerHTML += '<span class="ql-picker-item ql-primary" data-value="'+value+'" style="background-color: '+value+';"></span>';
select.innerHTML += '<option value="'+value+'"></option>';
}
quillEditor.format('color', value);
});
.ql-color .ql-picker-options [data-value=new-color] {
background: none !important;
width: 90px !important;
height: 20px !important;
}
.ql-color .ql-picker-options [data-value=new-color]:before {
content: 'New Color';
}
<script src="//cdn.quilljs.com/1.3.4/quill.min.js"></script>
<link href="//cdn.quilljs.com/1.0.0/quill.snow.css" rel="stylesheet"/>
<link href="//cdn.quilljs.com/1.3.4/quill.core.css" rel="stylesheet"/>
<div id="editor-container"></div>
Then with insertBefore(), you could keep the "New Color" option at the end.

How can I assign a class to a report not on odd, even rows but on the change of a date column?

I have a very simple report in AngularJS:
<div class="gridHeader">
<div>User</div>
<div>Date</div>
<div>Count</div>
</div>
<div class="gridBody"
<div class="gridRow" ng-repeat="row in rps.reports">
<div>{{row.user}}</div>
<div>{{row.date}}</div>
<div>{{row.count}}</div>
</div>
</div>
The report works but it's difficult to notice when the date changes.
Is there some way that I could assign a class to the grid row so that one date grid row has one class and the next date the grid row has another class. I think this is already available for odd and even rows with Angular but here I need it to work on every date change.
I've done a different solution (PLUNKER) where whole work is inside the controller. Maybe is a little bit more of code, but you will gain a lot of performance if you have thousand records because you will avoid dirty checking of ng-class. Additionally if your report is static and it won't have any changes, you can disable the two data binding...
CONTROLLER
vm.reports = addReportCssClases();
function addReportCssClases() {
var reportsData = [...];
var classes = ['dateOdd', 'dateEven'];
var index = 0;
reportsData[0].cssClass = classes[index];
for (var i = 1; i < reportsData.length; i++) {
var row = reportsData[i];
index = (row.date !== reportsData[i-1].date) ? (1 - index) : index;
row.cssClass = classes[index] ;
}
return reportsData;
}
HTML
<div ng-repeat="row in vm.reports track by $index" class="gridRow {{::row.cssClass}}">
<div>{{::row.user}}</div>
<div>{{::row.date}}</div>
<div>{{::row.count}}</div>
</div>
You can use ng-class with a function defined in your controller. For example:
var currentColor = "color1";
$scope.getClass = function(index)
{
if (index !== 0 && $scope.data[index].date !== $scope.data[index - 1].date)
{
currentColor = currentColor == "color1" ? "color2" : "color1";
}
return currentColor;
}
And in your template:
<div class="gridRow" ng-repeat="(i, d) in data" data-ng-class="getClass(i)">
See the plunker: http://plnkr.co/edit/PPPJRJJ1jHuJOgwf9lNK
This can be done with a one line given all the dates are actually date with the same format and not date time
<div class="gridHeader">
<div>User</div>
<div>Date</div>
<div>Count</div>
</div>
<div class="gridBody">
<div ng-repeat="row in rps.reports" class="gridRow" ng-class="{'backblue':$index>0 && row.date!=rps.reports[$index-1].date}">
<div>{{row.user}}</div>
<div>{{row.date}}</div>
<div>{{row.count}}</div>
</div>
</div>
your gridRow class will have to contain the background-color
.gridRow{
//other styles
background-color:red;
}
and the class backblue will have to have only the background-color
.backblue{
background-color:blue !important;
}
IMPORTANT
This will only work if the date field is only date and does not have time. If in any case it does have time you will have to convert each datetime to date
I have created a very simple and working solution for this using angular-filter, only you need to add dynamic class on gridRow.
working jsfiddle
HTML
<div ng-repeat="row in rps.reports"
class="gridRow {{row.date | filterDate}}">
Styles
.even {
background-color: green;
color: #fff;
}
.odd {
background-color: red;
color: #000;
}
Angular-filter
myApp.filter('filterDate', function() {
var lastDate,
count = 0,
calssName = 'even';
return function(date) {
var newDate = new Date(date).toDateString();
!lastDate && (lastDate = newDate);
if (newDate != lastDate) {
if (calssName == 'even') {
calssName = 'odd';
} else {
calssName = 'even';
}
}
lastDate = newDate;
return calssName;
}
});
A modified version of #ssougnez answer by storing the current date also in addition to color:
if(!(currentDate && currentDate === data.date)){
currentColor = currentColor == "color1" ? "color2" : "color1";
currentDate = data.date;
}
Plunker: http://plnkr.co/edit/o3YVBB
This might have less impact on performance than his version.
var app = angular.module('sample', []);
app.controller('SampleController', function($scope)
{
$scope.data = [
{
user: "A",
date: "3/2/2017"
},
{
user: "B",
date: "3/4/2017"
},
{
user: "C",
date: "4/3/2017"
},
{
user: "D",
date: "4/3/2017"
},
{
user: "E",
date: "4/3/2017"
},
{
user: "F",
date: "4/2/2017"
}
];
});
.same{
background-color:#ddd;
}
.differ{
background-color:yellow;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.23/angular.min.js"></script>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<html ng-app="sample">
<head>
</head>
<body>
<div ng-controller="SampleController">
<table id="myTable">
<thead>
<tr>
<th>User</th>
<th>Date</th>
</tr>
</thead>
<tbody>
<tr ng-repeat="row in data">
<td>{{row.user}}</td>
<td class="same" ng-if="data[$index+1].date==row.date || data[$index-1].date==row.date">{{row.date}}</td>
<td class="differ" ng-if="data[$index+1].date!=row.date && data[$index-1].date!=row.date">{{row.date}}</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
You can add the class with an expression using the ngClass directive in the view:
(function() {
'use strict';
angular.module('app', []);
})();
(function() {
'use strict';
angular.module('app').controller('MainController', MainController);
MainController.$inject = ['$scope'];
function MainController($scope) {
var date = new Date();
$scope.rps = {
reports: [{
user: 'User A',
date: date.setDate(date.getDate() + 1),
count: 5
},
{
user: 'User B',
date: date.setDate(date.getDate() + 2),
count: 10
},
{
user: 'User C',
date: date.setDate(date.getDate()),
count: 8
},
{
user: 'User D',
date: date.setDate(date.getDate() + 2),
count: 6
},
{
user: 'User E',
date: date.setDate(date.getDate()),
count: 20
},
{
user: 'User F',
date: date.setDate(date.getDate() + 3),
count: 6
}
]
};
}
})();
.gridHeader,
.gridRow {
display: table-row;
}
.gridHeader>div,
.gridRow>div {
display: table-cell;
padding: 5px 10px;
}
.className {
background: #ff0000;
color: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.min.js"></script>
<div ng-app="app" ng-controller="MainController as MainCtrl">
<div class="gridHeader">
<div>User</div>
<div>Date</div>
<div>Count</div>
</div>
<div class="gridBody">
<div class="gridRow" ng-repeat="row in ::rps.reports track by $index" ng-class="::{'className': $index === 0 || $index > 0 && rps.reports[$index - 1].date !== row.date}">
<div>{{::row.user}}</div>
<div>{{::row.date | date: 'dd-MM-yyyy'}}</div>
<div>{{::row.count}}</div>
</div>
</div>
</div>
Or add a boolean in the controller that you can use to trigger the className using the ngClass directive:
(function() {
'use strict';
angular.module('app', []);
})();
(function() {
'use strict';
angular.module('app').controller('MainController', MainController);
MainController.$inject = ['$scope'];
function MainController($scope) {
var date = new Date();
$scope.rps = {
reports: [{
user: 'User A',
date: date.setDate(date.getDate() + 1),
count: 5
},
{
user: 'User B',
date: date.setDate(date.getDate() + 2),
count: 10
},
{
user: 'User C',
date: date.setDate(date.getDate()),
count: 8
},
{
user: 'User D',
date: date.setDate(date.getDate() + 2),
count: 6
},
{
user: 'User E',
date: date.setDate(date.getDate()),
count: 20
},
{
user: 'User F',
date: date.setDate(date.getDate() + 3),
count: 6
}
]
};
// add the classes to the reports
addClasses($scope.rps.reports);
/*
* #name addClasses
* #type function
*
* #description
* Adds a class to a report if the date is different to the previous
*
* #param {array} reports The reports to add classes to
* #return nothing.
*/
function addClasses(reports) {
// loop through the reports to check the dates
for (var i = 0, len = reports.length; i < len; i++) {
// if the previous report a different date then the current report will have a class
reports[i].hasClass = (i === 0 || reports[i - 1].date !== reports[i].date);
}
}
}
})();
.gridHeader,
.gridRow {
display: table-row;
}
.gridHeader>div,
.gridRow>div {
display: table-cell;
padding: 5px 10px;
}
.className {
background: #ff0000;
color: #fff;
}
<script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.6.3/angular.min.js"></script>
<div ng-app="app" ng-controller="MainController as MainCtrl">
<div class="gridHeader">
<div>User</div>
<div>Date</div>
<div>Count</div>
</div>
<div class="gridBody">
<div class="gridRow" ng-repeat="row in ::rps.reports track by $index" ng-class="::{'className': row.hasClass}">
<div>{{::row.user}}</div>
<div>{{::row.date | date: 'dd-MM-yyyy'}}</div>
<div>{{::row.count}}</div>
</div>
</div>
</div>

create a decimal star rating for a comment in angularjs

Here is a code which present star rating code in angularjs. In some point I need to have a average of all the rating in whole the system so instead of rate:2 , i will have 2.4 . In such case i am interesting to present 2 star which are complete fill and one which has only half filled. How can I change my code in order to add this functionality?
Moreover, initially I would like to don't specify any star filled. That's also need a modification which I am not sure how should be done?
<div ng-app="app" ng-controller="RatingCtrl" class="container">
<h1>Angular Star Rating Directive</h1>
<div star-rating ng-model="rating1" max="10" on-rating-selected="rateFunction(rating)"></div>
<star-rating ng-model="rating2" readonly="isReadonly"></star-rating>
<label>
<input type="checkbox" ng-model="isReadonly" /> Is Readonly
</label>
<div><strong>Rating 1:</strong> {{rating1}}</div>
<div><strong>Rating 2:</strong> {{rating2}}</div>
</div>
In my directive
angular.module("app", [])
.controller("RatingCtrl", function($scope) {
$scope.rating1 = 1;
$scope.rating2 = 2;
$scope.isReadonly = true;
$scope.rateFunction = function(rating) {
console.log("Rating selected: " + rating);
};
})
.directive("starRating", function() {
return {
restrict : "EA",
template : "<ul class='rating' ng-class='{readonly: readonly}'>" +
" <li ng-repeat='star in stars' ng-class='star' ng-click='toggle($index)'>" +
" <i class='fa fa-star'></i>" + //&#9733
" </li>" +
"</ul>",
scope : {
ratingValue : "=ngModel",
max : "=?", //optional: default is 5
onRatingSelected : "&?",
readonly: "=?"
},
link : function(scope, elem, attrs) {
if (scope.max == undefined) { scope.max = 5; }
function updateStars() {
scope.stars = [];
for (var i = 0; i < scope.max; i++) {
scope.stars.push({
filled : i < scope.ratingValue
});
}
};
scope.toggle = function(index) {
if (scope.readonly == undefined || scope.readonly == false){
scope.ratingValue = index + 1;
scope.onRatingSelected({
rating: index + 1
});
}
};
scope.$watch("ratingValue", function(oldVal, newVal) {
if (newVal) { updateStars(); }
});
}
};
});
and css
.rating {
margin: 0;
padding: 0;
display: inline-block;
}
.rating li {
padding: 1px;
color: #ddd;
font-size: 20px;
text-shadow: .05em .05em #aaa;
list-style-type: none;
display: inline-block;
cursor: pointer;
}
.rating li.filled {
color: #fd0;
}
.rating.readonly li.filled {
color: #666;
}
http://codepen.io/anon/pen/RPLJYW
Thank you for any help.
You could use two identical set of stars to achieve this, position absolute one on top of the other. One fills your background star shapes (gray) and the one position at the top will represent your fill.
The top set of stars are all filled but its container's width can be adjusted to the proportion of stars representing your rate.
var score = 2.4;
var maxStars = 5;
var starContainerMaxWidth = 100; //pixls
var filledInStarsContainerWidth = score / maxStars * starsMaxWidth;
A CSS overflow hidden will hide the portion of stars that are not turned on, in effect allowing you to show 2.4 stars filled.
Update:
I have bashed a quick example http://codepen.io/anon/pen/NqazVa , will need some tidy up and reshuffling but the average rate is calculated and displayed correctly.
Check the AngularUI Bootstrap Rating component.
http://angular-ui.github.io/bootstrap/#/rating

Resources