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 am trying to "translate" this AngularJS slide left / right example to an AngularJS Material one.
The latter link consists of the following code snippets:
HTML code:
<div ng-controller="ExampleController" ng-app="switchExample">
<!--<select ng-model="slide" ng-options="item as item.name for item in slides">
</select>-->
<code>slide={{slide}}</code>
<code>moveToLeft={{mtl}}</code>
<md-button ng-click="prev()"><</md-button>
<md-button ng-click="next()">></md-button>
<div class="">
<div class="ngSwitchItem" ng-if="slide.name == 'first'" ng-class="{'moveToLeft' : mtl}">
<div class="firstPage page" md-swipe-left="selectPage(1)">
first
</div>
</div>
<div class="ngSwitchItem" ng-if="slide.name == 'second'" ng-class="{'moveToLeft' : mtl}">
<div class="secondPage page" md-swipe-right="selectPage(0)" md-swipe-left="selectPage(2)">
second
</div>
</div>
<div class="ngSwitchItem" ng-if="slide.name == 'third'" ng-class="{'moveToLeft' : mtl}">
<div class="thirdPage page" md-swipe-right="selectPage(1)" md-swipe-left="selectPage(3)">
third
</div>
</div>
<div class="ngSwitchItem" ng-if="slide.name == 'fourth'" ng-class="{'moveToLeft' : mtl}">
<div class="fourthPage page" md-swipe-right="selectPage(2)">
fourth
</div>
</div>
</div>
</div>
JS code
(function(angular) {
'use strict';
angular.module('switchExample', ['ngMaterial', 'ngAnimate'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.slides = [
{ index: 0, name: 'first' },
{ index: 1, name: 'second' },
{ index: 2, name: 'third' },
{ index: 3, name: 'fourth' }
];
$scope.selectPage = selectPage;
/**
* Initialize with the first page opened
*/
$scope.slide = $scope.slides[0];
$scope.prev = () => {
if ($scope.slide.index > 0) {
selectPage($scope.slide.index - 1);
}
}
$scope.next = () => {
if ($scope.slide.index < 3) {
selectPage($scope.slide.index + 1);
}
}
/**
* #name selectPage
* #desc The function that includes the page of the indexSelected
* #param indexSelected the index of the page to be included
*/
function selectPage(indexSelected) {
if ($scope.slides[indexSelected].index > $scope.slide.index) {
$scope.mtl = false;
} else {
$scope.mtl = true;
}
$scope.slide = $scope.slides[indexSelected];
}
}]);
})(window.angular);
CSS code
body {
overflow-x: hidden;
}
.ngSwitchItem {
position: absolute;
top: 50px;
bottom: 0;
right: 0;
left: 0;
animation-duration: 10.30s;
animation-timing-function: ease-in-out;
-webkit-animation-duration: 10.30s;
-webkit-animation-timing-function: ease-in-out;
}
.page {
position: inherit;
top: 0;
right: inherit;
bottom: inherit;
left: inherit;
}
.firstPage {
background-color: blue;
}
.secondPage {
background-color: red;
}
.thirdPage {
background-color: green;
}
.fourthPage {
background-color: yellow;
}
/* When the page enters, slide it from the right */
.ngSwitchItem.ng-enter {
animation-name: slideFromRight;
-webkit-animation-name: slideFromRight;
}
/* When the page enters and moveToLeft is true, slide it from the left(out of the user view) to the right (left corner) */
.ngSwitchItem.moveToLeft.ng-enter {
animation-name: slideFromLeft;
-webkit-animation-name: slideFromLeft;
}
/* When the page leaves, slide it to left(out of the user view) from the left corner,
in other words slide it from the left(out of the view) to the left corner but in reverse order */
.ngSwitchItem.ng-leave {
animation-name: slideFromLeft;
animation-direction: reverse;
-webkit-animation-name: slideFromLeft;
-webkit-animation-direction: reverse;
}
/* When the page leaves, slide it to the right(out of the user view) from the the left corner,
in other words, slide it from the right but in reverse order */
.ngSwitchItem.moveToLeft.ng-leave {
animation-name: slideFromRight;
animation-direction: reverse;
-webkit-animation-name: slideFromRight;
-webkit-animation-direction: reverse;
}
#keyframes slideFromRight {
0% {
transform: translateX(100%);
}
100% {
transform: translateX(0);
}
}
#keyframes slideFromLeft {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(0);
}
}
#-webkit-keyframes slideFromRight {
0% {
-webkit-transform: translateX(100%);
}
100% {
-webkit-transform: translateX(0);
}
}
#-webkit-keyframes slideFromLeft {
0% {
-webkit-transform: translateX(-100%);
}
100% {
-webkit-transform: translateX(0);
}
}
As seen, however, the second one doesn't behave as the first one, WHEN slide direction has changed.
For instance:
I slide to left the first one --> second slide loads with the correct animation
Then, I slide to right the second one --> it is supposed the first slide to start appearance from the left side, while the second one to start disappearance to to the right side. Instead, as you may see, the second one start to disappear to the left and from the right side a white slide is shown. At some point, the first slide starts its appearance from the middle of the content.
Please note, I deliberately delay the animations on the second example, just to see the undesired side effect mode clearly.
Actually, after a few more research hours, I found where the problem was buried - it seems, I have to move scope variable change for the next tick, to give time ng-class change to make its "magic".
Long story short - adding the following is what made the thing work:
$timeout(() => {
$scope.slide = $scope.slides[indexSelected];
}, 0)
Here is the updated example and the code snippet below:
JS code
(function(angular) {
'use strict';
angular.module('switchExample', ['ngMaterial', 'ngAnimate'])
.controller('ExampleController', ['$scope', '$timeout', function($scope, $timeout) {
$scope.slides = [
{ index: 0, name: 'first' },
{ index: 1, name: 'second' },
{ index: 2, name: 'third' },
{ index: 3, name: 'fourth' }
];
$scope.selectPage = selectPage;
/**
* Initialize with the first page opened
*/
$scope.slide = $scope.slides[0];
$scope.prev = () => {
if ($scope.slide.index > 0) {
selectPage($scope.slide.index - 1);
}
}
$scope.next = () => {
if ($scope.slide.index < 3) {
selectPage($scope.slide.index + 1);
}
}
/**
* #name selectPage
* #desc The function that includes the page of the indexSelected
* #param indexSelected the index of the page to be included
*/
function selectPage(indexSelected) {
if ($scope.slides[indexSelected].index > $scope.slide.index) {
$scope.mtl = false;
} else {
$scope.mtl = true;
}
// this will move a scope variable change to the next tick,
// hence will give time $scope.mtl to be handled by ng-class
$timeout(() => {
$scope.slide = $scope.slides[indexSelected];
}, 0)
}
}]);
})(window.angular);
I want to swipe Ionic list items to both sides. (i.e left-right AND right-left). It works perfectly for right-left swipe but I am not able to swipe list item to left side.
I used $ionicGesture for left-right swipe, and it also gives me an alert when i use swiperight event: event($ionicGesture.on('swiperight', scope.reportEvent, elem)), but I am not able to let it show the ion-option-button at the left side.
Here is my directive and controller code:
.directive('onSwipeRight', function($ionicGesture) {
return {
restrict : 'A',
link : function(scope, elem, attrs) {
var gestureType = attrs.gestureType;
switch(gestureType) {
case 'swipeRight':
$ionicGesture.on('swiperight', scope.reportEvent, elem);
break;
case 'swipeleft':
$ionicGesture.on('swipeleft', scope.reportEvent, elem);
break;
case 'doubletap':
$ionicGesture.on('doubletap', scope.reportEvent, elem);
break;
case 'tap':
$ionicGesture.on('tap', scope.reportEvent, elem);
break;
}
}
}
})
.controller('ChatsCtrl', function($scope, Chats) {
// With the new view caching in Ionic, Controllers are only called
// when they are recreated or on app start, instead of every page change.
// To listen for when this page is active (for example, to refresh data),
// listen for the $ionicView.enter event:
//
//$scope.$on('$ionicView.enter', function(e) {
//});
$scope.chats = Chats.all();
$scope.remove = function(chat) {
Chats.remove(chat);
}
$scope.reportEvent = function (event) {
alert("hi");
console.log('Reporting : ' + event.type);
event.preventDefault();
};
})
Here is my html code.
<ion-view view-title="Chats">
<ion-content>
<ion-list can-swipe="true">
<ion-item gesture-type="swipeRight" on-swipe-right="swipeRight()" class="item-remove-animate item-avatar item-icon-right" ng-repeat="chat in chats" type="item-text-wrap" href="#/tab/chats/{{chat.id}}">
<img ng-src="{{chat.face}}">
<h2>{{chat.name}}</h2>
<p>{{chat.lastText}}</p>
<i class="icon ion-chevron-right icon-accessory"></i>
<ion-option-button class="button-assertive" ng-click="share(item)" side="left">
Share
</ion-option-button>
<ion-option-button class="button-assertive" ng-click="remove(chat)" side="right">
Delete
</ion-option-button>
</ion-item>
</ion-list>
</ion-content>
</ion-view>
So I want to display share button at left side and delete button at right side.
Can anybody provide me specific solution for it?
I've edited ionic lib to do something like that. But i couldn't do a JSFiddle or a Code Pen i Will give you the link to my modified ionic.css and ionic.bundle.js!
TL;DR
https://gist.githubusercontent.com/booris/847f044d2ef2a05101ce/raw/2274365384f5eed3e4538b269f3a7d7998eb22ed/ionic.css
https://gist.githubusercontent.com/booris/847f044d2ef2a05101ce/raw/2274365384f5eed3e4538b269f3a7d7998eb22ed/ionic.bundle.js
Just replace it with yours, start an ionic project blank. And put this HTML in it:
<body ng-app="starter">
<ion-pane>
<ion-header-bar class="bar-stable">
<h1 class="title">Ionic Blank Starter</h1>
</ion-header-bar>
<ion-content>
<ion-list show-delete="false" can-swipe="true" swipe-direction="both">
<ion-item href="#">
Item 1
<ion-option-button side="right" class="button-light icon ion-heart"></ion-option-button>
<ion-option-button side="right" class="button-light icon ion-email"></ion-option-button>
<ion-option-button side="left" class="button-assertive icon ion-trash-a"></ion-option-button>
</ion-item>
<ion-item href="#">
Item 2
<ion-option-button class="button-light icon ion-heart"></ion-option-button>
<ion-option-button class="button-light icon ion-email"></ion-option-button>
<ion-option-button class="button-assertive icon ion-trash-a"></ion-option-button>
</ion-item>
</ion-list>
</ion-content>
</ion-pane>
</body>
You can specify the wipe direction with left, right or both.
And in the ion-options-button you can give it a side.
Hope it helps, anything you need just ask! I will try to comment my changes in the code later on!
EDIT:
I will try to explain what i did.
First change the ionOptionButton directive to create to div for the button, one left and one right
//added second div with class item-options-left for the left buttons
var ITEM_TPL_OPTION_BUTTONS =
'<div class="item-options invisible">' +
'</div>' + '<div class="item-options-left invisible">' +
'</div>';
IonicModule.directive('ionOptionButton', [function () {
function stopPropagation(e) {
e.stopPropagation();
}
return {
restrict: 'E',
require: '^ionItem',
priority: Number.MAX_VALUE,
compile: function ($element, $attr) {
$attr.$set('class', ($attr['class'] || '') + ' button', true);
return function ($scope, $element, $attr, itemCtrl) {
if (!itemCtrl.optionsContainer) {
itemCtrl.optionsContainer = jqLite(ITEM_TPL_OPTION_BUTTONS);
itemCtrl.$element.append(itemCtrl.optionsContainer);
}
//[NEW] if it as an attribute side = 'left' put the button in the left container
if ($attr.side === 'left') {
angular.element(itemCtrl.optionsContainer[1]).append($element);
itemCtrl.$element.addClass('item-left-editable');
} else{
angular.element(itemCtrl.optionsContainer[0]).append($element);
itemCtrl.$element.addClass('item-right-editable');
}
//Don't bubble click up to main .item
$element.on('click', stopPropagation);
};
}
};
}]);
Add CSS to left buttons in ionic.css file
.item-options-left {
position: absolute;
top: 0;
left: 0;
z-index: 1;
height: 100%; }
.item-options-left .button {
height: 100%;
border: none;
border-radius: 0;
display: -webkit-inline-box;
display: -webkit-inline-flex;
display: -moz-inline-flex;
display: -ms-inline-flexbox;
display: inline-flex;
-webkit-box-align: center;
-ms-flex-align: center;
-webkit-align-items: center;
-moz-align-items: center;
align-items: center; }
.item-options .button:before {
margin: 0 auto; }
Now change the ion-list controller to accept swipe directions attribute
.controller('$ionicList', [
'$scope',
'$attrs',
'$ionicListDelegate',
'$ionicHistory',
function ($scope, $attrs, $ionicListDelegate, $ionicHistory) {
var self = this;
//[NEW] object with can-swipe attr and swipe-direction side attr, default direction is left
var swipe = {
isSwipeable: true,
side: 'left'
};
var isReorderShown = false;
var isDeleteShown = false;
var deregisterInstance = $ionicListDelegate._registerInstance(
self, $attrs.delegateHandle,
function () {
return $ionicHistory.isActiveScope($scope);
}
);
$scope.$on('$destroy', deregisterInstance);
self.showReorder = function (show) {
if (arguments.length) {
isReorderShown = !!show;
}
return isReorderShown;
};
self.showDelete = function (show) {
if (arguments.length) {
isDeleteShown = !!show;
}
return isDeleteShown;
};
//[NEW] get swipe direction attribute and store it in a variable to access in other function
self.canSwipeItems = function (can) {
if (arguments.length) {
swipe.isSwipeable = !!can;
swipe.side = $attrs.swipeDirection;
}
return swipe;
};
self.closeOptionButtons = function () {
self.listView && self.listView.clearDragEffects();
};
}]);
To end, you should replace slideDrag function with this one, just search for it in ionic.bundle.js
//[NEW] add this var to the others in the function
var ITEM_OPTIONS_CLASS_RIGHT = 'item-options-left';
var SlideDrag = function (opts) {
this.dragThresholdX = opts.dragThresholdX || 10;
this.el = opts.el;
this.item = opts.item;
this.canSwipe = opts.canSwipe;
};
SlideDrag.prototype = new DragOp();
SlideDrag.prototype.start = function (e) {
var content, buttonsLeft, buttonsRight, offsetX, buttonsLeftWidth, buttonsRightWidth;
if (!this.canSwipe().isSwipeable) {
return;
}
if (e.target.classList.contains(ITEM_CONTENT_CLASS)) {
content = e.target;
} else if (e.target.classList.contains(ITEM_CLASS)) {
content = e.target.querySelector('.' + ITEM_CONTENT_CLASS);
} else {
content = ionic.DomUtil.getParentWithClass(e.target, ITEM_CONTENT_CLASS);
}
// If we don't have a content area as one of our children (or ourselves), skip
if (!content) {
return;
}
// Make sure we aren't animating as we slide
content.classList.remove(ITEM_SLIDING_CLASS);
// Grab the starting X point for the item (for example, so we can tell whether it is open or closed to start)
offsetX = parseFloat(content.style[ionic.CSS.TRANSFORM].replace('translate3d(', '').split(',')[0]) || 0;
// Grab the buttons
buttonsLeft = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS);
if (!buttonsLeft) {
return;
}
//[NEW] get the Right buttons
buttonsRight = content.parentNode.querySelector('.' + ITEM_OPTIONS_CLASS_RIGHT);
if (!buttonsRight) {
return;
}
// [NEW] added the same functionality to both sides, to make buttons visible when dragged
if(e.gesture.direction === "left")
buttonsLeft.classList.remove('invisible');
else
buttonsRight.classList.remove('invisible');
//[NEW] added buttonRight and buttonLeft properties to currentDrag
buttonsLeftWidth = buttonsLeft.offsetWidth;
buttonsRightWidth = buttonsRight.offsetWidth;
this._currentDrag = {
buttonsLeft: buttonsLeft,
buttonsRight: buttonsRight,
buttonsLeftWidth: buttonsLeftWidth,
buttonsRightWidth: buttonsRightWidth,
content: content,
startOffsetX: offsetX
};
};
/**
* Check if this is the same item that was previously dragged.
*/
SlideDrag.prototype.isSameItem = function (op) {
if (op._lastDrag && this._currentDrag) {
return this._currentDrag.content == op._lastDrag.content;
}
return false;
};
SlideDrag.prototype.clean = function (isInstant) {
var lastDrag = this._lastDrag;
if (!lastDrag || !lastDrag.content) return;
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
lastDrag.content.style[ionic.CSS.TRANSFORM] = '';
if (isInstant) {
lastDrag.content.style[ionic.CSS.TRANSITION] = 'none';
makeInvisible();
ionic.requestAnimationFrame(function () {
lastDrag.content.style[ionic.CSS.TRANSITION] = '';
});
} else {
ionic.requestAnimationFrame(function () {
setTimeout(makeInvisible, 250);
});
}
function makeInvisible() {
lastDrag.buttonsLeft && lastDrag.buttonsLeft.classList.add('invisible');
lastDrag.buttonsRight && lastDrag.buttonsRight.classList.add('invisible');
}
};
SlideDrag.prototype.drag = ionic.animationFrameThrottle(function (e) {
var buttonsLeftWidth;
var buttonsRightWidth;
// We really aren't dragging
if (!this._currentDrag) {
return;
}
// Check if we should start dragging. Check if we've dragged past the threshold,
// or we are starting from the open state.
if (!this._isDragging &&
((Math.abs(e.gesture.deltaX) > this.dragThresholdX) ||
(Math.abs(this._currentDrag.startOffsetX) > 0))) {
this._isDragging = true;
}
if (this._isDragging) {
buttonsLeftWidth = this._currentDrag.buttonsLeftWidth;
buttonsRightWidth = this._currentDrag.buttonsRightWidth;
// Grab the new X point, capping it at zero
//[NEW] added right swipe new position
if (this.canSwipe().side === 'left' || (this.canSwipe().side === 'both' && e.gesture.direction === 'left'))
var newX = Math.min(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
else if (this.canSwipe().side === 'right' || (this.canSwipe().side === 'both' && e.gesture.direction === 'right'))
var newX = Math.max(0, this._currentDrag.startOffsetX + e.gesture.deltaX);
var buttonsWidth = 0;
if (e.gesture.direction === 'right')
buttonsWidth = buttonsRightWidth;
else
buttonsWidth = buttonsLeftWidth;
// If the new X position is past the buttons, we need to slow down the drag (rubber band style)
if (newX < -buttonsWidth) {
// Calculate the new X position, capped at the top of the buttons
newX = Math.min(-buttonsWidth, -buttonsWidth + (((e.gesture.deltaX + buttonsWidth) * 0.4)));
}
this._currentDrag.content.$$ionicOptionsOpen = newX !== 0;
this._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + newX + 'px, 0, 0)';
this._currentDrag.content.style[ionic.CSS.TRANSITION] = 'none';
}
});
SlideDrag.prototype.end = function (e, doneCallback) {
var self = this;
// There is no drag, just end immediately
if (!self._currentDrag) {
doneCallback && doneCallback();
return;
}
// If we are currently dragging, we want to snap back into place
// The final resting point X will be the width of the exposed buttons
var restingPoint;
if (e.gesture.direction === 'left' && (this.canSwipe().side === 'left' || this.canSwipe().side === 'both'))
restingPoint = -self._currentDrag.buttonsLeftWidth;
if (e.gesture.direction === 'right' && (this.canSwipe().side === 'right' || this.canSwipe().side === 'both'))
restingPoint = self._currentDrag.buttonsRightWidth;
// Check if the drag didn't clear the buttons mid-point
// and we aren't moving fast enough to swipe open
var buttonsWidth = 0;
if (e.gesture.direction === 'right')
buttonsWidth = self._currentDrag.buttonsRightWidth;
else
buttonsWidth = self._currentDrag.buttonsLeftWidth;
if (e.gesture.deltaX > -(buttonsWidth / 2)) {
// If we are going left or right but too slow, or going right, go back to resting
if ((e.gesture.direction == "left" || e.gesture.direction == "right") && Math.abs(e.gesture.velocityX) < 0.3) {
restingPoint = 0;
}
}
ionic.requestAnimationFrame(function () {
if (restingPoint === 0) {
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = '';
var buttonsLeft = self._currentDrag.buttonsLeft;
var buttonsRight = self._currentDrag.buttonsRight;
setTimeout(function () {
buttonsLeft && buttonsLeft.classList.add('invisible');
buttonsRight && buttonsRight.classList.add('invisible');
}, 250);
} else {
self._currentDrag.content.style[ionic.CSS.TRANSFORM] = 'translate3d(' + restingPoint + 'px,0,0)';
}
self._currentDrag.content.style[ionic.CSS.TRANSITION] = '';
// Kill the current drag
if (!self._lastDrag) {
self._lastDrag = {};
}
ionic.extend(self._lastDrag, self._currentDrag);
if (self._currentDrag) {
self._currentDrag.buttons = null;
self._currentDrag.content = null;
}
self._currentDrag = null;
// We are done, notify caller
doneCallback && doneCallback();
});
};
My solution is not perfect, but it works. and there are others ways of doing this, i did it this way to understand better how Ionic works and how they do Ionic directives.
Any feedback is welcome, and with this you can try to make your own or improve this one.
I made item-swipe-pane directive which creates a container inside a ion-item, which is visible when the item is swiped to the left or to the right.
var ITEM_SWIPE_PANE_TPL = '<div class="item-options invisible item-swipe-pane"></div>';
var DIRECTION_RIGHT_CLASS = 'direction-right';
module.directive( 'itemSwipePane' , function() {
return {
restrict: 'E',
require: '^ionItem',
link: function (scope, $element, attrs, itemCtrl) {
var container;
var direction = 'left';
// Set direction
if (attrs['direction'] && attrs['direction'] === 'right'){
direction = 'right';
} else if (attrs['direction'] && attrs['direction'] === 'left'){
direction = 'left';
}
if (direction === 'left'){
if (!itemCtrl.itemSwipeLeft){
itemCtrl.itemSwipeLeft = angular.element(ITEM_SWIPE_PANE_TPL);
itemCtrl.$element.append(itemCtrl.itemSwipeLeft);
}
container = itemCtrl.itemSwipeLeft;
} else if (direction === 'right'){
if (!itemCtrl.itemSwipeRight){
itemCtrl.itemSwipeRight = angular.element(ITEM_SWIPE_PANE_TPL);
// If direction is right, move position of item options
// to the left - override inherited right:0;
itemCtrl.itemSwipeRight.css({right: 'auto'});
// "direction-right" is container selector.
itemCtrl.itemSwipeRight.addClass(DIRECTION_RIGHT_CLASS);
itemCtrl.$element.append(itemCtrl.itemSwipeRight);
}
container = itemCtrl.itemSwipeRight;
}
container.append($element);
// Animation to slowly close opened item.
itemCtrl.$element.addClass('item-right-editable');
} // link
}; // return
}); // item-swipe-pane
Attribute direction controls swipe direction. Possible values are left or right. Default direction is left.
You can place any content it the directive, button, text, image, icon, avatar, background image, etc.
The container is quite raw in a sense that everything you place in it has to be formatted by CSS or by other means.
item-swipe-pane is compatible with ion-option-button, ion-delete-button and ion-reorder-button directives.
It is possible to combine two item-swipe-panes on the same ion-item. Each one with different swipe direction.
Example with two item-swipe-panes, one is on the left and one on the right:
<ion-item>
Two item-swipe-panes. One on the left, one on the right.
<item-swipe-pane direction="right">
<button class="button button-balanced ion-arrow-right-c"></button>
<button class="button button-positive">Update</button>
<button class="button button-royal">Add</button>
</item-swipe-pane>
<item-swipe-pane class="left-pane">
<button class="button button-assertive">Delete</button>
<button class="button button-calm">Share</button>
<button class="button button-balanced ion-arrow-left-c"></button>
</item-swipe-pane>
</ion-item>
More item-swipe-pane examples are on Codepen.
Important note:
Unfortunately Ionic Framework does not allow right swipe (from left to right) of a list item, so I had to make few modifications to the Ionic library. Here is summary of modifications to Ionic Framework.
Links:
Modified Ionic library download.
item-swipe-pane directive on Github.
here is an sample code using that u can achieve it
swipe-pane.html
<div class="mk-swipe-pane">
<div class="col-xs-4 swipe-actions-padding" ng-repeat="action in swipeActions">
<div ng-click="currentActionClick(action.actionName)"
ng-class="[action.actionCssclass]">
<div class="icon-font-size">
<i ng-class="[action.actionIcon]"></i>
</div>
{{action.actionName}}
</div>
</div>
</div>
swipe-pane.js
angular.module('mk.Directives')
.directive('mkSwipePane', function ($swipe) {
return {
templateUrl: "lib/mobikon/directives/notifications/swipe-pane/swipe-pane.html",
restrict: 'E',
scope: {
swipeActions: "="
},
replace: true,
link: function ($scope, element) {
var MAX_VERTICAL_DISTANCE = 75,
MAX_VERTICAL_RATIO = 0.3,
MIN_HORIZONTAL_DISTANCE = 30,
startCoords,
valid,
elWidth = $(element).width(),
direction = 1,
pointerTypes = ['touch'],
delayForAnimation = 70;
$scope.currentActionClick = function (actionName) {
$scope.$emit('currentActionName', actionName);
};
function validSwipe(coords) {
if (!startCoords) return false;
var deltaY = Math.abs(coords.y - startCoords.y);
var deltaX = (coords.x - startCoords.x) * direction;
return valid && // Short circuit for already-invalidated swipes.
deltaY < MAX_VERTICAL_DISTANCE &&
deltaX > 0 &&
deltaX > MIN_HORIZONTAL_DISTANCE &&
deltaY / deltaX < MAX_VERTICAL_RATIO;
}
$swipe.bind(element, {
'start': function (coords, event) {
startCoords = coords;
valid = true;
},
'move': function (coords, event) {
var diffX = coords.x - startCoords.x;
if (diffX < 0) {
direction = -1; // For left swipe
} else {
direction = 1; // For right swipe
}
if (validSwipe(coords)) {
var marginLeft = parseInt($(element).css("marginLeft"));
if (direction === -1 && Math.abs(diffX) <= elWidth / 2) {
$(element).prev().css({"margin-left": diffX});
} else if (direction === 1 && (marginLeft + diffX) <= 0) {
$(element).prev().css({"margin-left": marginLeft + diffX});
}
}
},
'cancel': function (event) {
valid = false;
},
'end': function (coords, event) {
if (validSwipe(coords)) {
if (direction === -1) {
$(element).prev().animate({"margin-left": "-50%"}, delayForAnimation);
$scope.$emit('isCurrentRowClickable', {isSwiped: false});
} else {
$(element).prev().animate({"margin-left": "0%"}, delayForAnimation);
$scope.$emit('isCurrentRowClickable', {isSwiped: true});
}
}
}
}, pointerTypes);
}
}
});
require("./swipe-pane.html");
require("./swipe-pane.scss");
swipe-pane.scss
#import "../../../../../views/mixins";
[mk-swipe-pane], .mk-swipe-pane {
display: inline-block;
width: 50%;
$icon-outline-color: $mk-pure-white;
$icon-font-size: 35px;
$icon-text-font-size: 16px;
$icon-margin-top:-10px;
$icon-padding-top:35%;
$icon-padding-bottom:5px;
$icon-container-height:120px;
$icon-width:20px;
#media screen and (max-width: 768px) {
.swipe-actions-padding {
padding-left: 0px;
padding-right: 0px;
float: none;
display: inline-block;
}
.icon-font-size {
font-size: $icon-font-size;
}
.email-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-1-icon-background-orange;
color: $icon-outline-color;
}
.sms-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-2-icon-background-blue;
color: $icon-outline-color;
}
.call-icon {
text-align: center;
font-size: $icon-text-font-size;
margin-top: $icon-margin-top;
padding-top: $icon-padding-top;
height: $icon-container-height;
vertical-align: middle;
background-color: $mk-swipe-action-3-icon-background-green;
color: $icon-outline-color;
}
.disabled {
background-color: $mk-background-gray !important;
}
}
}
Unlike other answers, i've created an angular wrapper for swiper (that seems to be the slider lib used in ionic 2) focused on ionic v1 instead of editing the framework itself.
My wrapper is avaliable here, and there's an demo here.
You can use npm install ionic-swiper to install it, and import like instructed on README.md:
In javascript with webpack (you can import the whole bundle too like a normal js):
import {moduleName as ionicSwiperModule} from 'ionic-swiper';
angular.module('yourModule',[ionicSwiperModule]);
Edit:
I've made some changes since i wrote this answer, so here's a more correct way to use my lib:
<ionic-swiper ng-repeat="i in [1,2,3]"
center-on-disable="{{ true || 'disable default center on disable behavior'}}"
is-swipable="{{ true || 'some prop to watch' }}"
left-swiper="{{:: true || 'or any prop that evaluate to a boolean' }}"
right-swiper="{{:: true || 'or any prop that evaluate to a boolean' }}">
<!-- containerId is available inside this context -->
<!-- Left transclude is optional -->
<left-swiper class="side-item">
Left
</left-swiper>
<!-- Central transclude is required -->
<central-swiper class="central-item">
Central {{:: containerId}}
</central-swiper>
<!-- Right transclude is optional -->
<right-swiper class="side-item">
Right
</right-swiper>
</ionic-swiper>
And here's the original answer usage example:
In HTML (you will need to adjust some css too):
<ionic-list>
<div
swiper-container="true"
class="swiper-container"
ng-repeat="item in [1,2,3,4,5,6]">
<!-- containerId is available inside this context -->
<div class="swiper-wrapper">
<ion-item swiper-slide="center">
This swiper container id is {{:: containerId }}
</ion-item>
<ion-item swiper-slide="right">
Right Button
</ion-item>
<ion-item swiper-slide="left">
Left Button
</ion-item>
</div>
</div>
</ionic-list>
Here's an gif from the demo (i've recorded this in a touchpad, that's why it seems 'sticky')
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>" + //★
" </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
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.