Related
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!
I want to display a drop-down list of buttons in the left panel, one button for one "need".
Later, the user will be able to add a new need-button to the list.
I use a panel with "itemArray" binding.
But the button is not displayed when sentence: addNeed("My new need"); is executed.
I checked with the "dynamicPorts sample but I can't understand why it doesn't work.
<!DOCTYPE html>
<html>
<head>
<meta name="minimumCode" content="width=device-width, initial-scale=1">
<title>minimumCode</title>
<meta name="description" content="Iso prototype Leon Levy" />
<!-- Copyright 1998-2017 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="https://unpkg.com/gojs/release/go-debug.js"></script>
<span id="diagramEventsMsg" style="color: red"></span>
<script id="code">
var ellipseStrokeWidth=3;
var ellipseWidth = 80;
var ellipseHeight = 25;
var myFont = "16px sans-serif";
var myFontMedium = "23px sans-serif";
var myFontLarge = "30px sans-serif";
var needWidth = 170;
var needHeight = 20;
var needStrokeWidth = 0;
var needColor = 'purple';
var portSize = new go.Size(8, 8);
function init() {
var $ = go.GraphObject.make; //for conciseness in defining node templates
myDiagram =
$(go.Diagram, "myDiagramDiv",
{
"undoManager.isEnabled": true
});
myBannerNeeds =
$(go.Diagram, "myBannerNeedsDiv",
{ layout: $(go.GridLayout, { wrappingColumn: 1, alignment: go.GridLayout.Position
})}
);
myBannerNeeds.nodeTemplate =
$(go.Node,
$(go.Panel, "Vertical", //needs list buttons
{ alignment: new go.Spot(0, 0), row: 0, column: 0},
new go.Binding("itemArray", "needsDataArray"),
{ itemTemplate:
$(go.Panel,
$(go.Shape, "Rectangle",
{ stroke: "red", strokeWidth: 2,
height: 30, width: 30}
),
$(go.TextBlock, { text: "...", stroke: "gray" },
new go.Binding("text", "key"))
) // end itemTemplate
}
) // end Vertical Panel
);
// Add a button to the needs panel.
function addNeed(newNeedName) {
myDiagram.startTransaction("addNeed");
var button = $('Button',
$(go.Shape, "Rectangle",
{ width: needWidth, height: needHeight, margin: 4, fill: "white",
stroke: "rgb(227, 18, 18)", strokeWidth: needStrokeWidth}),
$(go.TextBlock, newNeedName, // the content is just the text label
{stroke: needColor, font: myFont }),
{click: function(e, obj) { needSelected(newNeedName); } }
);
var needsNode = needsDataArray; //document.getElementById("ForNeeds");
if (needsNode) { showMessage("needsNode is true; " + button)}
else {showMessage("needsNode is false")};
myDiagram.model.insertArrayItem(needsNode, -1, button);
myDiagram.commitTransaction("addNeed");
}// end function addNeed
var needsDataArray = [];
var linksNeedsDataArray = []; // always empty
myBannerNeeds.model = new go.GraphLinksModel( needsDataArray, linksNeedsDataArray);
myDiagram.grid.visible = true;
myDiagram.model.copiesArrays = true;
myDiagram.model.copiesArrayObjects = true;
addNeed("My new need");
function needSelected(e,obj) {
alert("e:" + e + "; obj:" + obj + ' selected')
}; //end function flowTypeSelected
function showMessage(s) {
document.getElementById("diagramEventsMsg").textContent = s;
}
}// end function init
</script>
</head>
<body onload="init()">
<div id="container" style= "display: grid; grid-template-columns: 1fr 5fr; margin:0 ; height: 800px; width:1080px; font-size:0; position: relative; ">
<div id="ForNeeds">
<div id="myBannerNeedsDiv" style="display: inline-block; width: 200px; min-height: 400px; background: whitesmoke; margin-right: 0px; border: solid 1px purple;">
</div>
</div>
<div id="myDiagramDiv" style="flex-grow: 1; width: 804px;height: 100%; border: solid 1px black;">
</div>
</div>
</body>
</html>
Here's a basic demonstration of what I think you are asking for:
<!DOCTYPE html>
<html>
<head>
<title>Minimal GoJS Sample</title>
<!-- Copyright 1998-2019 by Northwoods Software Corporation. -->
<meta charset="UTF-8">
<script src="go.js"></script>
<script id="code">
function init() {
var $ = go.GraphObject.make;
myDiagram =
$(go.Diagram, "myDiagramDiv",
{ "undoManager.isEnabled": true });
myDiagram.nodeTemplate =
$(go.Node, "Auto",
$(go.Shape,
{ fill: "white" }),
$(go.Panel, "Vertical",
$(go.TextBlock,
{ margin: 4 },
new go.Binding("text")),
$(go.Panel, "Vertical",
new go.Binding("itemArray", "buttons"),
{
itemTemplate:
$("Button",
$(go.TextBlock, new go.Binding("text", "")),
{
click: function(e, button) {
alert(button.data);
}
}
)
}
)
)
);
myDiagram.model = new go.GraphLinksModel(
[
{ key: 1, text: "Alpha", buttons: ["one", "two"] },
{ key: 2, text: "Beta", buttons: ["one"] }
],
[
{ from: 1, to: 2 }
]);
}
function test() {
myDiagram.commit(function(diag) {
diag.selection.each(function(n) {
if (n instanceof go.Node) {
diag.model.addArrayItem(n.data.buttons, "another");
}
})
})
}
</script>
</head>
<body onload="init()">
<div id="myDiagramDiv" style="border: solid 1px black; width:100%; height:600px"></div>
<button onclick="test()">Test</button>
</body>
</html>
Select a Node and then click the HTML "Test" Button. It will add an item to the node's data.buttons Array, which causes a copy of the Panel.itemTemplate to be added to that Panel. In this case, that item template is just a GoJS "Button" which when clicked calls alert with the value of the item, a string.
Note how the value added to the JavaScript Array in the data is just a simple object -- in this case just a string, although it is commonplace to have each Array item be a JavaScript Object with various properties. I think your problem is that you are trying to add GraphObjects to the Array. That's a no-no -- you should not be mixing the Diagram's GraphObjects with the Model data.
I'm using https://github.com/HsuanXyz/ion2-calendar to generate a calendar in ionic. I can't make color changes to dates using dateconfig's cssClass.
Below is the code i'm using
` daysConfig() {
let _daysConfig = [
{
date:new Date(2018,0,1),
subTitle:'New Year\'s',
marked:false,
cssClass: 'my-cal'
},
{
date:new Date(2017,1,14),
subTitle:'Valentine\'s',
disable:true
},
{
date:new Date(2017,3,1),
subTitle:'April Fools',
marked:true
},
{
date:new Date(2017,3,7),
subTitle:'World Health',
marked:true
},
{
date:new Date(2017,4,31),
subTitle:'No-Smoking',
marked:true
},
{
date:new Date(2017,5,1),
subTitle:'Children\'s',
marked:true
}
];
_daysConfig.push(...this.days);
this.calendarCtrl.openCalendar({
from: new Date(2017,0,1),
to : new Date(2018,11.1),
daysConfig:_daysConfig
})
.then( (res:any) => { console.log(res) })
.catch( () => {} )
}`
css Class
.my-cal {
color: yellow
}
After a long RnD of almost 5 hour could solve this issue.
Please find the below solution in ionic
Define the below class in global.scss
.my-cal {
background-color: red !important;
}
and remember not to define this class anywhere else.
now use this in you days config as below:
let _daysConfig: DayConfig[] = [];
for (let i = 0; i < 31; i++) {
_daysConfig.push({
date: new Date(2020, 4, i + 1),
marked: false,
subTitle: `$${i + 1}`,
cssClass: 'my-cal'
})
}
Hope it will help someone :)
I just encountered the same problem, and I solved it by adding the follow style in the .scss file:
button.days-btn.my-cal {
p, small {
color: yellow;
text-decoration: underline;
font-weight: bold;
}
}
The cssClass is only added to the button that the <p> element containing the text is in.
So to properly change the color, you'll have to reference the <p> element first and assign the color to that.
Example
.ts
cssClass: 'my-cal'
.scss
.my-cal {
p {
color: green;
}
}
One of the solutions would be to add this code to your global scss/css style
ion-calendar { .my-cal{ background-color: #6E6B6B !important; p{ color: white !important; } }}
Later add "my-cal" class to wanted day.
I am using : https://github.com/angular-ui/ui-grid.info/tree/gh-pages/release/3.0.0-RC.18
<div ui-grid="gridOptions" style="height:765px"></div>
When I hard code the value, as shown above, the grid spreads out and everything works as expected.
However, if I do the following...
$scope.gridStyle = 'height:'+numRows*rowHeight+'px' //(765px);
<div ui-grid="gridOptions" style="{{gridStyle}}"></div>
The height is printed in the div and div widens but the content itself widens to only around 340px. The space that is left is blank, so instead of 25 rows I see only 8. I have to scroll down, while there is a whole 400px free in the grid. The ui-grid-viewport and ui-grid-canvas are both not using this space...
Why can't the ui-grid-viewport use that space?
I use ui-grid - v3.0.0-rc.20 because a scrolling issue is fixed when you go full height of container. Use the ui.grid.autoResize module will dynamically auto resize the grid to fit your data. To calculate the height of your grid use the function below. The ui-if is optional to wait until your data is set before rendering.
angular.module('app',['ui.grid','ui.grid.autoResize']).controller('AppController', ['uiGridConstants', function(uiGridConstants) {
...
$scope.gridData = {
rowHeight: 30, // set row height, this is default size
...
};
...
$scope.getTableHeight = function() {
var rowHeight = 30; // your row height
var headerHeight = 30; // your header height
return {
height: ($scope.gridData.data.length * rowHeight + headerHeight) + "px"
};
};
...
<div ui-if="gridData.data.length>0" id="grid1" ui-grid="gridData" class="grid" ui-grid-auto-resize ng-style="getTableHeight()"></div>
A simpler approach is set use css combined with setting the minRowsToShow and virtualizationThreshold value dynamically.
In stylesheet:
.ui-grid, .ui-grid-viewport {
height: auto !important;
}
In code, call the below function every time you change your data in gridOptions. maxRowToShow is the value you pre-defined, for my use case, I set it to 25.
ES5:
setMinRowsToShow(){
//if data length is smaller, we shrink. otherwise we can do pagination.
$scope.gridOptions.minRowsToShow = Math.min($scope.gridOptions.data.length, $scope.maxRowToShow);
$scope.gridOptions.virtualizationThreshold = $scope.gridOptions.minRowsToShow ;
}
.ui-grid, .ui-grid-viewport,.ui-grid-contents-wrapper, .ui-grid-canvas {
height: auto !important;
}
UPDATE:
The HTML was requested so I've pasted it below.
<div ui-grid="gridOptions" class="my-grid"></div>
ORIGINAL:
We were able to adequately solve this problem by using responsive CSS (#media) that sets the height and width based on screen real estate. Something like (and clearly you can add more based on your needs):
#media (min-width: 1024px) {
.my-grid {
width: 772px;
}
}
#media (min-width: 1280px) {
.my-grid {
width: 972px;
}
}
#media (min-height: 768px) {
.my-grid {
height: 480px;
}
}
#media (min-height: 900px) {
.my-grid {
height: 615px;
}
}
The best part about this solution is that we need no resize event handling to monitor for grid size changes. It just works.
I like Tony approach. It works, but I decided to implement in different way. Here my comments:
1) I did some tests and when using ng-style, Angular evaluates ng-style content, I mean getTableHeight() function more than once. I put a breakpoint into getTableHeight() function to analyze this.
By the way, ui-if was removed. Now you have ng-if build-in.
2) I prefer to write a service like this:
angular.module('angularStart.services').factory('uiGridService', function ($http, $rootScope) {
var factory = {};
factory.getGridHeight = function(gridOptions) {
var length = gridOptions.data.length;
var rowHeight = 30; // your row height
var headerHeight = 40; // your header height
var filterHeight = 40; // your filter height
return length * rowHeight + headerHeight + filterHeight + "px";
}
factory.removeUnit = function(value, unit) {
return value.replace(unit, '');
}
return factory;
});
And then in the controller write the following:
angular.module('app',['ui.grid']).controller('AppController', ['uiGridConstants', function(uiGridConstants) {
...
// Execute this when you have $scope.gridData loaded...
$scope.gridHeight = uiGridService.getGridHeight($scope.gridData);
And at the HTML file:
<div id="grid1" ui-grid="gridData" class="grid" ui-grid-auto-resize style="height: {{gridHeight}}"></div>
When angular applies the style, it only has to look in the $scope.gridHeight variable and not to evaluate a complete function.
3) If you want to calculate dynamically the height of an expandable grid, it is more complicated. In this case, you can set expandableRowHeight property. This fixes the reserved height for each subgrid.
$scope.gridData = {
enableSorting: true,
multiSelect: false,
enableRowSelection: true,
showFooter: false,
enableFiltering: true,
enableSelectAll: false,
enableRowHeaderSelection: false,
enableGridMenu: true,
noUnselect: true,
expandableRowTemplate: 'subGrid.html',
expandableRowHeight: 380, // 10 rows * 30px + 40px (header) + 40px (filters)
onRegisterApi: function(gridApi) {
gridApi.expandable.on.rowExpandedStateChanged($scope, function(row){
var height = parseInt(uiGridService.removeUnit($scope.jdeNewUserConflictsGridHeight,'px'));
var changedRowHeight = parseInt(uiGridService.getGridHeight(row.entity.subGridNewUserConflictsGrid, true));
if (row.isExpanded)
{
height += changedRowHeight;
}
else
{
height -= changedRowHeight;
}
$scope.jdeNewUserConflictsGridHeight = height + 'px';
});
},
columnDefs : [
{ field: 'GridField1', name: 'GridField1', enableFiltering: true }
]
}
tony's approach does work for me but when do a console.log, the function getTableHeight get called too many time(sort, menu click...)
I modify it so the height is recalculated only when i add/remove rows. Note: tableData is the array of rows
$scope.getTableHeight = function() {
var rowHeight = 30; // your row height
var headerHeight = 30; // your header height
return {
height: ($scope.gridData.data.length * rowHeight + headerHeight) + "px"
};
};
$scope.$watchCollection('tableData', function (newValue, oldValue) {
angular.element(element[0].querySelector('.grid')).css($scope.getTableHeight());
});
Html
<div id="grid1" ui-grid="gridData" class="grid" ui-grid-auto-resize"></div>
I am late to the game but I found a nice solution. I created a custom attribute directive all you need to do is pass in the gridApi and it will automatically calculate the height. It also subscribes to the pagination change event so if the user changes page size it will resize.
class UIGridAutoResize implements ng.IDirective {
link: (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => void;
scope: { gridApi: "=" };
restrict = "A";
private previousValue: string;
private isValid: boolean = true;
private watch: any;
constructor($timeout: ng.ITimeoutService) {
UIGridAutoResize.prototype.link = (scope: ng.IScope, element: ng.IAugmentedJQuery, attrs: ng.IAttributes) => {
const gridOptions = scope.$eval(attrs.uiGrid) as any;
const gridApi = scope.$eval(attrs.gridResize) as any;
gridApi.core.on.rowsRendered(scope, () => {
$timeout(() => {
this.autoSizeGrid(element, attrs, gridOptions, gridApi, false);
}, 100);
});
gridApi.core.on.filterChanged(scope, () => {
this.autoSizeGrid(element, attrs, gridOptions, gridApi, false);
});
if (attrs.uiGridPagination === "") {
gridApi.pagination.on.paginationChanged(null, () => {
this.autoSizeGrid(element, attrs, gridOptions, gridApi, true);
});
}
angular.element(window).resize(() => {
$timeout(() => {
this.autoSizeGrid(element, attrs, gridOptions, gridApi, false);
}, 100);
});
};
}
static Factory(): ng.IDirectiveFactory {
const directive = ($timeout: ng.ITimeoutService) => {
return new UIGridAutoResize($timeout);
};
directive["$inject"] = ["$timeout"];
return directive;
}
private autoSizeGrid(element: ng.IAugmentedJQuery, attrs: ng.IAttributes, gridOptions: any, gridApi: any, isPaginationChanged: boolean) {
gridApi.core.handleWindowResize();
// Clear empty grid message
angular.element(element.parent()).find("#emptyGridMessage").remove();
element.find(".ui-grid-viewport").css("display", "");
if (attrs.hidePageSize === "") {
element.find(".ui-grid-pager-row-count-picker").css("display", "none");
}
let rowCount = gridApi.core.getVisibleRows().length;
const headerElements = element.find(".ui-grid-header");
let headerHeight = 2;
if (headerElements.length > 1) { // If we have more than one header element the grid is using grouping
const headerElement = angular.element(headerElements[1]);
headerHeight += headerElement.height();
} else {
headerHeight += headerElements.height();
}
if (attrs.uiGridPagination === "") {
if (rowCount < 1) {
gridOptions.enablePagination = false;
gridOptions.enablePaginationControls = false;
element.css("height", (rowCount * 30) + headerHeight - 2);
element.find(".ui-grid-viewport").css("display", "none");
angular.element("<div id='emptyGridMessage' style='font-size: 1em; width: 100%; background-color: white; border: 1px solid #d4d4d4; padding: 7px 12px; color: #707070;'><span style='opacity: 0.95;'>There are no records.</span></div>").insertAfter(element);
} else if (gridApi.core.getVisibleRows().length < gridOptions.paginationPageSize && !isPaginationChanged) {
gridOptions.enablePagination = false;
gridOptions.enablePaginationControls = false;
element.css("height", (rowCount * 30) + headerHeight);
} else {
gridOptions.enablePagination = true;
gridOptions.enablePaginationControls = true;
element.css("height", (rowCount * 30) + headerHeight);
}
} else {
if (rowCount < 1) {
element.css("height", (rowCount * 30) + headerHeight - 2);
element.find(".ui-grid-viewport").css("display", "none");
angular.element("<div id='emptyGridMessage' style='font-size: 1em; width: 100%; background-color: white; border: 1px solid #d4d4d4; padding: 7px 12px; color: #707070;'><span style='opacity: 0.95;'>There are no records.</span></div>").insertAfter(element);
} else {
element.css("height", (rowCount * 30) + headerHeight);
}
}
// Add extra margin to prevent scroll bar and pager from overlapping content underneath
const pagerHeight = element.find(".ui-grid-pager-panel").height();
if (rowCount > 0) {
if (pagerHeight > 0)
element.css("margin-bottom", pagerHeight);
else
element.css("margin-bottom", 10);
} else {
if (pagerHeight > 0)
angular.element(element.parent()).find("#emptyGridMessage").css("margin-bottom", pagerHeight);
else
angular.element(element.parent()).find("#emptyGridMessage").css("margin-bottom", 10);
}
if (rowCount > gridOptions.paginationPageSize) // Sometimes paging shows all rows this fixes that
gridApi.core.refresh();
}
}
<div ui-grid="vm.gridOptions" grid-resize="vm.gridApi" ui-grid-resize-columns ui-grid-pagination></div>
following #tony's approach, changed the getTableHeight() function to
<div id="grid1" ui-grid="$ctrl.gridOptions" class="grid" ui-grid-auto-resize style="{{$ctrl.getTableHeight()}}"></div>
getTableHeight() {
var offsetValue = 365;
return "height: " + parseInt(window.innerHeight - offsetValue ) + "px!important";
}
the grid would have a dynamic height with regards to window height as well.
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>