ng-dropdown-multiselect, does max items break UI - angularjs

Using this very cool AngularJS control but having a problem with limiting the maximum number of items that can be selected.
http://dotansimha.github.io/angularjs-dropdown-multiselect/docs.
The documentation is vague at best although there are a lot of properties and features described. But after several hours of trying combinations of settings I almost got it to work as desired.
I want the dropdown button to display a custom label and show the count of the selected items. The problem is I can't seem to find any way to have this functionally and also limit the number of selected items the user can click on.
This said, I have not tried to see if it is possible to capture the click event and force the behavior I desire because the control describes the "smartButtonMaxItems" as a property that is intended to do just this, limit the number of selected items.
The JS
app.controller('AlphabeticController', ['$scope', '$window', function ($scope, $window){
var _this = this; // insure correct scope within callBacks to this controller
this.name = "AlphabeticController";
_this.names = [
{ 'pk': 1, 'userName': 'Priscila Gail Hane' },
{ 'pk': 2, 'userName': 'Milford Frank-Duell' },
{ 'pk': 3, 'userName': 'Wilson Albanese' },
{ 'pk': 4, 'userName': 'Aileen Hudec' }
];
// init the selected names collection
_this.oSelectedNames = [];
// create default alphabetic list of names
this.init = function () {
// sort names with local characgters (accent inorged)
_this.oSortedNames = angular.copy(_this.names);
_this.oSortedNames.sort(function (a, b) {
return a.userName.strength - b.userName.strength || a.userName.localeCompare(b.userName);
});
};
this.projectSettings = {
displayProp: 'userName',
idProp: 'pk',
externalIdProp: '',
styleActive: true,
showCheckAll: true,
showUncheckAll: true,
scrollable: true,
enableSearch: true,
keyboardControls: true,
dynamicTitle: true,
smartButtonMaxItems: 0,
smartButtonTextConverter: function (itemText, originalItem) {
return _this.oSelectedNames.length;
}
};
this.projectText = {
buttonDefaultText: _this.oSelectedNames.length + ' Selected Names',
selectionCount: 0,
selectionOf: 0,
searchPlaceholder: 'enter name',
dynamicButtonTextSuffix: ' Selected Names'
};
}]);
The HTML
<div ng-controller="AlphabeticController as AC" ng-init="AC.init()">
<div ng-dropdown-multiselect=""
options="AC.oSortedNames"
selected-model="AC.oSelectedNames"
translation-texts="AC.projectText"
extra-settings="AC.projectSettings"
search-filter="">
</div>
</div>
With the value of zero (0) set on the "smartButtonMaxItems" property the dropdown button works correctly but there is no upper limit to the number of items that can be selected. Whereas, with the "smartButtonMaxItems" set to a value smaller than the names array the control limits the number of items that can be selected by the button no longer shows the count and rather shows the names them selves.
Demo on Fiddler

To limit the selection i have added selectionLimit: 2 to this.projectSettings .
Also i have customized the selection message in the below fiddler . Hope this helps you
Here's Updated fiddler

Related

Dropdown opening issue inside of ui-grid

I get a new row on dropdown Click and am using enableCellEditOnFocus and when I try to open the dropdown it is not opening and alternate dropdowns are working properly. To avoid confusion I altered the code and I am showing that bit of code that is giving me the error and here is my plunker.
This is my ui-grid object
$scope.gridOptions = {
enableCellEditOnFocus: true
};
$scope.gridOptions.columnDefs = [
{ name: 'gender', displayName: 'Gender', editableCellTemplate: 'ui-grid/dropdownEditor', width: '20%',
editDropdownValueLabel: 'gender', editDropdownOptionsArray: [
{ id: 1, gender: 'male' },
{ id: 2, gender: 'female' }
] } ];
I add new row by using this code:
$scope.addNewRow = function () {
$scope.gridOptions.data.push({
"gender": "male"});
};
//adding new row inside of uigrid
$scope.newRow = function (row, columnIndex) {
tempIndexSave = "";
tempIndexSave = _.indexOf($scope.gridOptions.data, row);
var length = $scope.gridOptions.data.length;
if (length - 1 == tempIndexSave) {
$scope.addNewRow();
}
};
The problem is due to these lines where you can see there's a hack for screen readers.
This hack may bring issues similar to yours because it depends on beginCellEdit and afterCellEdit events to be fired evenly and with a fixed pattern:
beginCellEdit
afterCellEdit
beginCellEdit
afterCellEdit
beginCellEdit
...
Adding a new row while already editing a row breaks this pattern since, for the new created row, you are missing a beginCellEdit, so the preventMouseDown function will always fire, preventing user interaction.
I put up a workaround, but I don't advise you using this if you want to keep compatibility with screen readers (see comments before the lines in the file I linked to), because the workaround might break the hack.
The workaround consists of raising a fake beginCellEdit while adding the row, as you can see in this updated plunkr.
...
if (length - 1 == tempIndexSave) {
$scope.addNewRow();
$scope.gridApi.edit.raise.beginCellEdit();
}
...

mwl-calendar does not render calendar - just outputs text

I resolved it - it was a css issue
I am trying to integrate https://github.com/mattlewis92/angular-bootstrap-calendar. There are no errors, but instead of rendering the calendar as per the demo page, it just prints out the days etc on the screen.
Here is my html:
div ng-controller="calendarCtrl">
<mwl-calendar
view="calendarView"
current-day="calendarDay"
events="events"
view-title="calendarTitle"
auto-open="true">
</mwl-calendar>
</div>
and here is the controller:
app.controller('calendarCtrl', function($scope, $log, userData){
var invoiceData = userData.getActivityData();
$scope.calendarView = 'month';
$scope.calendarDay = new Date();
$scope.calendarTitle = "BirdsEyeView";
if( invoiceData[0]){
//$log.debug("in cal array:"+$scope.calendarDay);
}
else {
$log.debug("No invoice data");
}
$scope.invoiceData = invoiceData;
$scope.events = [
{
title: 'My event title', // The title of the event
type: 'info', // The type of the event (determines its color). Can be important, warning, info, inverse, success or special
startsAt: new Date(2015,6,10,1), // A javascript date object for when the event starts
endsAt: new Date(2015,6,10,15), // Optional - a javascript date object for when the event ends
editable: false, // If edit-event-html is set and this field is explicitly set to false then dont make it editable. If set to false will also prevent the event from being dragged and dropped.
deletable: false, // If delete-event-html is set and this field is explicitly set to false then dont make it deleteable
incrementsBadgeTotal: true, //If set to false then will not count towards the badge total amount on the month and year view
recursOn: 'year' // If set the event will recur on the given period. Valid values are year or month
}
];
});
this was a CSS issue - the download from npm did not have the css. So did a manual download.

How to have a checkbox in an ng-grid grouping

Is it possible to have a check box in an ng-grid grouping header?
When that grouping check box is clicked, all the checkboxes of the rows under that particular group should be selected.
The other group rows should remain un-selected.
Right now, when I have my gridOptions as follows, I see checkboxes for all rows, and in the table header. When a column header is dragged to use it for grouping, the grouped rows-header do not have checkboxes.
Can anybody help?
$scope.gridOptions = {
data: 'myData',
showGroupPanel: true,
showSelectionCheckbox: true,
jqueryUITheme: true,
enableCellSelection: true,
enableRowSelection: true,
enableCellEdit: false,
pinSelectionCheckbox: false,
selectWithCheckboxOnly: true
};
In order to make it, you have first to override the default aggregateTemplate :
$scope.gridOptions = {
data: 'myData',
showGroupPanel: true,
showSelectionCheckbox: true,
jqueryUITheme: true,
enableRowSelection: true,
aggregateTemplate: "<div ng-init=\"initAggregateGroup(row)\" ng-style=\"rowStyle(row)\" style=\"top: 0px; height: 48px; left: 0px; cursor:pointer\" class=\"ngAggregate\">" +
" <span class=\"ngAggregateText\" ng-click=\"expandGroupChilds($event, row)\">{{row.label CUSTOM_FILTERS}} ({{row.totalChildren()}} {{AggItemsLabel}})</span>" +
"<input class=\"ngSelectionHeader\" type=\"checkbox\" ng-show=\"multiSelect\" ng-checked=\"row.status.isChecked\" ng-model=\"row.status.isChecked\" ng-change=\"toggleGroupSelectedChilds(row)\" /></div>"
};
Then you can add those functions in your controller:
$scope.expandAll = function($event){
$scope.isAllCollapsed = !$scope.isAllCollapsed;
_.each($scope.ngGridOptions.ngGrid.rowFactory.aggCache, function(row){
row.toggleExpand();
});
};
$scope.initAggregateGroup = function(group){
group.status = {
isChecked: false
};
};
$scope.toggleGroupSelectedChilds = function(group){
_.each(group.children, function(entry){
$scope.ngGridOptions.selectItem(entry.rowIndex, group.status.isChecked);
});
};
$scope.expandGroupChilds = function($event, group){
$event.stopPropagation();
$event.preventDefault();
group.toggleExpand();
};
I found this question while desperately seeking an answer to the same problem, and while Eric's answer pointed me in the right direction, it wasn't working as expected*.
Here's what I came to in the end, after a couple of days of playing around with the contents of the ngRow and ngAggregate objects.
My aggregateTemplate is pretty similar to Eric's, but there are a few differences. First, I've added the expand/collapse icon thing back in (span class="{{row.aggClass()}}></span>"). I've also moved the ng-click="expandGroupChildren($event, row)" to the containing div, and added ng-click="$event.stopPropagation()" to the checkbox. This means that clicking anywhere but the checkbox will expand/collapse the group. Finally, I've renamed some functions.
var aggregateTemplate = '<div ng-init="initAggregateGroup(row)" ng-style="rowStyle(row)" style="top: 0; height: 48px; left: 0; cursor:pointer" class="ngAggregate" ng-click="expandGroupChildren($event, row)">' +
'<span class="{{row.aggClass()}}"></span>' +
'<input class="ngSelectionHeader" type="checkbox" ng-show="multiSelect" ng-checked="row.status.isChecked" ng-model="row.status.isChecked" ng-change="setGroupSelection(row)" ng-click="$event.stopPropagation()" />' +
'<span class="ngAggregateText">{{row.label CUSTOM_FILTERS}} ({{row.totalChildren()}} {{AggItemsLabel}})</span>' +
'</div>';
But the function code is where I really diverge.
First, this just doesn't work for me:
$scope.gridOptions.selectItem(entry.rowIndex, group.status.isChecked)
Instead, in this version we call this:
row.selectionProvider.setSelection (row, group.status.isChecked)
Since it takes the row itself instead of an index, it just works, no matter how tangled the indices might get.
This version also works with nested groups. When you group by, for instance, City > Age, and you get a group Minnesota containing a group 53, clicking on the group header for 53 will select all 53-year-olds who live in Minesota, but not 53-year-olds in other cities or Minnesotans of other ages. Clicking the group header for Minnesota, on the other hand, will select everyone in Minnesota, and the group header checkboxes for every sub-group. Likewise, if we only have 53-year-olds in the the Minnesota group, then clicking the 53 checkbox will also tick the Minnesota checkbox.
And that brings me to the final change. With a watcher per group (I don't generally like watchers and try to avoid them, but sometimes they're a necessary evil), we keep track of the selection within each group and automatically tick the box in the group header when every row is selected. Just like the select-all checkbox at the top of the grid.
So here's the code:
angular.extend ($scope, {
initAggregateGroup : initAggregateGroup,
expandGroupChildren : expandGroupChildren,
setGroupSelection : setGroupSelection
});
function initAggregateGroup (group) {
group.status = {
isChecked: getGroupSelection (group)
};
$scope.$watch (
function () {
return getGroupSelection (group)
},
function (isSelected) {
setGroupSelection (group, isSelected);
}
)
}
function expandGroupChildren ($event, group) {
$event.stopPropagation ();
$event.preventDefault ();
group.toggleExpand ();
}
function setGroupSelection (group, isSelected) {
// Sets the field when called by the watcher (in which case the field needs to be updated)
// or during recursion (in which case the objects inside aggChildren don't have it set at all)
if (_.isBoolean (isSelected)) {
group.status = { isChecked : isSelected }
}
// Protects against infinite digest loops caused by the watcher above
if (group.status.isChecked === getGroupSelection (group)) { return }
_.forEach (group.children, function (row) {
// children: ngRow objects that represent actual data rows
row.selectionProvider.setSelection (row, group.status.isChecked);
});
_.forEach (group.aggChildren, function (subGroup) {
// aggChildren: ngAggregate objects that represent groups
setGroupSelection (subGroup, group.status.isChecked);
});
}
function getGroupSelection (group) {
if (group.children.length > 0) {
return _.every (group.children, 'selected');
}
if (group.aggChildren.length > 0) {
return _.every (group.aggChildren, getGroupSelection);
}
return false;
}
*Clicking the checkbox in the aggregateTemplate would select a seemingly random collection of rows from all across the grid (seemingly random, because it was consistent throughout separate sessions, for the same data).
I think the problem (at least for me in ngGrid 2.0.12) was that ngGrid wasn't properly mapping the rowIndex field to the right row in its model. I think this was because the rows were rearranged for the grouping as well as the sorting, and the internal mapping hadn't kept up.

How do I set a Ext Grid Filter Default?

I have a working sort-able grid using the ext 3.4 grid filter plugin. I would like to default the active column to filter true values. User who needs the inactive records could remove the filter. How do I specify a default filter column and value?
Thanks in advance!
colModel: new Ext.grid.ColumnModel({
defaults: {
sortable: true
// How do I specify a default filter value
//
// Only show active records unless the user changes the filter...
},
columns: [{
dataIndex:'f_uid',
id:'f_uid',
header:'ID',
hidden:true
}, {
dataIndex:'f_name',
id:'f_name',
header:'Name',
}, {
xtype:'booleancolumn',
dataIndex:'f_active',
id:'f_active',
header:'Active',
filterable:true,
trueText:'Active',
falseText:'Inactive'
}]
I realise this is an old question but it took me a while to find a solution, therefore I thought I would share.
1) The filter can be set using the value property in the filter.
filter: {
type: 'LIST',
value: ['VALUE TO FILTER']
}
2) In order to initially filter the data use the filterBy() method in the store. This could be defined in the onRender event handler.
this.getStore().load({
scope:this,
callback: function() {
// filter the store
this.getStore().filterBy(function(record, id) {
// true will display the record, false will not
return record.data.DATA_TO_FILTER == 'VALUE TO FILTER ';
});
}
});
The answer was in the Filter.js source code. The filter object within the column definition can be used to configure the default behavior.
}, {
xtype:'booleancolumn',
dataIndex:'f_active',
id:'f_active',
header:'Active',
trueText:'Active',
falseText:'Inactive',
filterable:true,
filter: {
value:1, // 0 is false, 1 is true
active:true // turn on the filter
}
}
I have encountered the same problem and I found that #John's answer is right, I can make it work with the sample http://dev.sencha.com/deploy/ext-4.0.0/examples/grid-filtering/grid-filter-local.html, for the grid-filter-local.js, just add the code like:
grid.getStore().load({
scope:this,
callback: function() {
// filter the store
grid.getStore().filterBy(function(record, id) {
// true will display the record, false will not
return record.data.size === 'small';
});
}
});
before the original code store.load(), and wipe off the store.load().
Then it will only show the record with size equals 'small' at the first load of the web page. Cheers!
I've made a universal helper class that allows you to set any default values in column definition.
https://gist.github.com/Eccenux/ea7332159d5c54823ad7
This should work with both remote and static stores. Note that this also works with filterbar plugin.
So your column item is something like:
{
header: 'Filename',
dataIndex: 'fileName',
filter: {
type: 'string',
// filename that starts with current year
value: Ext.Date.format(new Date(), 'Y'),
active:true
}
},
And then in your window component you just add something like:
initComponent: function() {
this.callParent();
// apply default filters from grid to store
var grid = this.down('grid');
var defaultFilters = Ext.create('Ext.ux.grid.DefaultFilters');
defaultFilters.apply(grid);
},

Apply grid filter programmatically from function

Using Ext.ux.grid.FiltersFeature, I have remote filters and I am trying to write a function to apply a date filter on a grid column programmatically (rather than clicking on the filter drop down menu in the column header). The first time I run the function the grid store gets reloaded without the filter. When I run the function a second time (and every time thereafter) it works totally fine, the store reloads with the filters. Here is the gist of the function I have:
// a filter object for testing
aFilter = {type: 'date', field: 'a_date_field', comparison: 'gt', value: '2012-03-08 00:00:00'}
var grid = Ext.create('Ext.grid.Panel', {
store: store,
features: [{
ftype: 'filters',
}],
columns[{
header: 'ID',
dataIndex: 'id',
itemId: 'id',
width: 40,
}, {
xtype: 'datecolumn',
header: 'Date',
dataIndex: 'a_date_field',
itemId: 'a_date_field',
width: 75,
format:'j-M-Y',
filterable: true
}],
listeners: {
'afterrender': function() {
// Need to create the filters as soon as the grid renders
// rather than waiting for the user to click on the header
grid.filters.createFilters();
}
},
bbar: [{
text: 'Do a filter',
handler: function() {
// get the filter that is attached to the grid
var gridFilter = grid.filters.getFilter(aFilter.field);
// have to do this to create a menu for this filter
gridFilter.init({dataIndex: aFilter.field, type: aFilter.type, active: true});
// if this column is a date filter column
if (gridFilter.type == 'date') {
var dateValue = Ext.Date.parse(aFilter.value, 'Y-m-d H:i:s');
if (filter.comparison == 'gt') {
gridFilter.setValue({after: dateValue});
} else {
gridFilter.setValue({before: dateValue});
}
}
}
}
});
I also found that this function works the first time if I click on any grid header menu before I run the function.
I've been trying to find out what changes are made to the grid which make the filter work after the first attempt fails or what clicking on any grid header does to make it work. But nothing I add seems to fix it so it will run the first time. Has anyone implemented this successfully?
I have workaround:
bbar: [{
text: 'Do a filter',
handler: function() {
var grid = this.up('grid');
var dateValue = Ext.Date.parse(aFilter.value, 'Y-m-d H:i:s');
var value = aFilter.comparison == 'gt' ? {after: dateValue} : {before: dateValue};
var gridFilter = grid.filters.getFilter(aFilter.field);
if (!gridFilter) {
gridFilter = grid.filters.addFilter({
active: true,
type: aFilter.type,
dataIndex: aFilter.dataIndex,
});
gridFilter.menu.show();
gridFilter.setValue(value);
gridFilter.menu.hide();
} else {
gridFilter.setActive(true);
}
Ext.Function.defer(function(){
gridFilter = grid.filters.getFilter(aFilter.field);
gridFilter.setValue(value);
}, 10);
}
}]
As you can see I actually apply filter 2 times.
As an update, I expanded this function and modified it to work with ExtJS 4.1.1
Here is an example of the function to set grid filters dynamically (without the user needing to click on the menu items). Afterwards, the filtered items will be visible to the user in the grid column header menus as if he clicked on them and set them manually.
The "grid" argument is a grid with FiltersFeature that you want to filter. The other argument is an array of "filter" objects (I'll show an example below), the function simply applies all the passed "filter" objects to the grid.
doGridFilter: function(grid, filters) {
// for each filter object in the array
Ext.each(filters, function(filter) {
var gridFilter = grid.filters.getFilter(filter.field);
gridFilter.setActive(true);
switch(filter.data.type) {
case 'date':
var dateValue = Ext.Date.parse(filter.data.value, 'm/d/Y'),
value;
switch (filter.data.comparison) {
case 'gt' :
value = {after: dateValue};
break;
case 'lt' :
value = {before: dateValue};
break;
case 'eq' :
value = {on: dateValue};
break;
}
gridFilter = log.filters.getFilter(filter.field);
gridFilter.setValue(value);
gridFilter.setActive(true);
break;
case 'numeric':
var value;
switch (filter.data.comparison) {
case 'gt' :
value = {gt: filter.data.value};
break;
case 'lt' :
value = {lt: filter.data.value};
break;
case 'eq' :
value = {eq: filter.data.value};
break;
}
gridFilter = log.filters.getFilter(filter.field);
gridFilter.setValue(value);
gridFilter.setActive(true);
break;
case 'list':
gridFilter = log.filters.getFilter(filter.field);
gridFilter.menu.setSelected(gridFilter.menu.selected, false);
gridFilter.menu.setSelected(filter.data.value.split(','), true);
break;
default :
gridFilter = log.filters.getFilter(filter.field);
gridFilter.setValue(filter.data.value);
break;
}
});
}
Here's an example of a "filter" object array.
// an example of a "filters" argument
[{
field: 'some_list_column_data_index',
data: {
type: 'list',
value: 'item1,item2,item3,item4,item5,item6,item7'
}
}, {
field: 'some_date_column_data_index',
data: {
type: 'date',
comparison: 'gt',
value: '07/07/2007'
}
}]
One caveat, you need to "create" the filters manually before using this function. Normally FiltersFeature grid filters are "created" the first time a user clicks on one of them, that may not happen if the user just wants to apply one of these predefined filters.
That can be handled easily by including this afterrender listener in the gridpanel.
listeners: {
// must create the filters after grid is rendered
afterrender: function(grid) {
grid.filters.createFilters();
}
}
Just add
filter: true
to grid columns description like this:
me.columns = [
{header:"Name", dataIndex:"name", editor:"textfield", filter: true},
];
if you want to get the filter work after the first attempt, first instance create.
Here is something that may be worth looking into. It seems that the filters plugin is listening for menucreate event to initialize the filters. I wonder if menu create event is deferred until necessary and hence the filters don't get initialized?
/**
* #private Handle creation of the grid's header menu. Initializes the filters and listens
* for the menu being shown.
*/
onMenuCreate: function(headerCt, menu) {
var me = this;
me.createFilters(); //<------
menu.on('beforeshow', me.onMenuBeforeShow, me);
},
Do you want to apply grid filter or may be store.filter() capability would suit you better? In this case just filter the store, and grid will display filtered records.
I discovered another way to implement this. It appears that grid features are only bound to the grid after the grid is rendered. This means that any setup of the filter will not take effect until after the grid is rendered. The initial load of the store appears to be initiated before the grid is rendered.
I solved my problem by creating my store with a memory proxy containing no data.
me.store = Ext.create('Ext.data.Store', {
model: 'SummaryData',
data: [],
proxy: {
type: 'memory',
reader: 'array'
},
remoteSort: true,
remoteFilter: true
});
Then set up an afterrender handler on the grid to poke in the correct proxy and initiate a load of the store.
afterrender: function () {
var me = this;
me.store.setProxy({
type: 'ajax',
url : '/print_unallocated/change_site__data',
reader: {
type: 'json',
root: 'rows'
},
listeners: {
exception: function (proxy, response) {
Max.reportException(response);
}
}
});
me.filters.createFilters();
me.store.load();
},
In the source, you can see a comment related to this.
// Call getMenu() to ensure the menu is created, and so, also are the filters. We cannot call
// createFilters() withouth having a menu because it will cause in a recursion to applyState()
// that ends up to clear all the filter values. This is likely to happen when we reorder a column
// and then add a new filter before the menu is recreated.
me.view.headerCt.getMenu();
You can test whether the menu has been created before applying your filter. If it hasn't, do it yourself.
if(!grid.getView().headerCt.menu){
grid.getView().headerCt.getMenu();
}

Resources