Ok, I have a weird behavior issue on an mvc view that I am working on. The form is for employee onboarding, so it collects information about the new employee. I have a dropdown list of job titles that is strongly typed.
The weird part of this is that this does not work upon the first change of the Job Title dropdown. It does everything but show the checked box. It does work fine though for subsequent change events. I have tracked the firing events using alert boxes. When those alerts show, it all works. When I comment out the alerts, the weird behavior returns.
The logic is as follows:
A job title is selected; jquery detects the change event.
A call to the controller is made and the controller returns a partial view comprised of one field (SelectedAppsList).
Jquery function clears the Application(s) Checkboxes
Jquery is called to check checkboxes of Applications based upon the returned values in SelectedAppsList.
Wizard View:
$("#JobId").change(function () {
//alert($("#JobId").val());
// Check the Apps Based Upon the Job Title
var jobId = $("#JobId").val();
$("#dvApps").load('#(Url.Action("GetApps", "ESRWizards", null, Request.Url.Scheme))?jobId=' + jobId);
AutoCheckSelectedApps();
}));
function AutoCheckSelectedApps() {
ClearSelectedApps();
//alert("Selecting Apps...");
var apps = $("#SelectedAppsList").val().split('|');
for (var i = 0; i < apps.length; i++) {
//alert(apps[i]);
if (apps[i] != "") {
$('input[type="checkbox"][id="SelectedApps_' + apps[i] + '"]').attr('checked', true);
}
}
};
function ClearSelectedApps() {
//alert("Cleared...");
$('input[type="checkbox"][id^="SelectedApps_"]').attr('checked', false);
}
Wizard View Form Controls Involved:
`#Html.DropDownList("JobId", String.Empty)`
`<input type="checkbox" name="SelectedApps_#item.Id" id="SelectedApps_#item.Id" value="#item.Id" />`
and partial view below.
Controller:
[HttpGet]
[OutputCache(Duration = 0)]
public ActionResult GetApps(int jobId)
{
ViewBag.SelectedAppsList = null;
var AppList = db.JobApps.Where(m => m.JobId == jobId).OrderBy(v => v.AppId);
foreach (var item in AppList)
{
ViewBag.SelectedAppsList += item.AppId.ToString() + "|";
}
return PartialView("_Apps");
}
Partial View _Apps
#model ECSR.Models.ESRWizard
#if(ViewBag.SelectedAppsList != null)
{
#Html.TextBox("SelectedAppsList", (string)ViewBag.SelectedAppsList)
}
else
{
#Html.TextBoxFor(m => m.SelectedAppsList)
}
I am guessing that this is a possible refresh issue, so I tried various refresh commands on the checkboxes to no avail. This is a long form so I stripped out the non-related form fields. Any ideas?
In an AngularJS application, I have an ag-grid that uses virtual paging/infinite scrolling to lazy-load rows from a dataset that is too large to show at once. I have turned on check-box selection in the first column, so that the user should be able to select individual rows for arbitrary application-specific actions.
The AngularJS application uses ui-router to control multiple views. So, building on the virtual-paging example with "sorting & filtering", with constructed data about Olympic winners, from the ag-grid documentation, I've further extended the code a bit. From index.html:
<body ng-controller="MainController" class="container">
<div ui-view="contents"></div>
</body>
and the following ui-router states:
myapp.config(function($stateProvider, $urlRouterProvider) {
$urlRouterProvider.otherwise("example.page1")
$stateProvider
.state('example', {
abstract: true,
views: {
contents: {
template: '<div ui-view="example"></div>'
}
}
})
.state('example.page1', {
url: '/page1',
views: {
example: {
templateUrl: 'page1.html'
}
}
})
.state('example.page2', {
url: '/page2',
views: {
example: {
template: 'Go back to the <a ui-sref="example.page1">example grid</a>.'
}
}
});
});
where page1.html looks like the following:
<div ng-controller="GridController">
<div ag-grid="gridOptions" class="ag-fresh" style="height: 250px;"></div>
</div>
<div>
<h3>Selected rows:</h3>
<ul class="list-inline">
<li ng-repeat="row in currentSelection track by row.id">
<a ng-click="remove(row)">
<div class="badge">#{{ row.id }}, {{ row.athlete }}</div>
</a>
</li>
</ul>
</div>
<p>Go to <a ui-sref="example.page2">the other page</a>.</p>
What I want to accomplish:
That selections made in the ag-grid is remembered (sticky) when scrolling a (virtual) page out of view and back again, so that a user can select multiple rows on separate pages.
That the remembered selections are available outside the grid, and support adding and removing selections (as intended by the ng-click="remove(row)" in page1.html, shown above).
That the selections should be remembered when switching from the view with the ag-grid to another one, and back again.
(Optional) That the selections are remembered for the user's session.
How can I accomplish this?
I've created a working example of this can be implemented.
First of all, we'll write a AngularJS service, selectionService to keep track of the selections:
function _emptyArray(array) {
while (array.length) {
array.pop();
}
}
function _updateSharedArray(target, source) {
_emptyArray(target);
_.each(source, function _addActivity(activity) {
target.push(activity);
});
}
myapp.factory('selectionService', function ($rootScope, $window) {
var _collections = {},
_storage = $window.sessionStorage,
_prefix = 'selectionService';
angular.element($window).on('storage', _updateOnStorageChange);
function _persistCollection(collection, data) {
_storage.setItem(_prefix + ':' + collection, angular.toJson(data));
}
function _loadCollection(collection) {
var item = _storage.getItem(_prefix + ':' + collection);
return item !== null ? angular.fromJson(item) : item;
}
function _updateOnStorageChange(event) {
var item = event.originalEvent.newValue;
var keyParts = event.originalEvent.key.split(':');
if (keyParts.length < 2 || keyParts[0] !== _prefix) {
return;
}
var collection = keyParts[1];
_updateSharedArray(_getCollection(collection), angular.fromJson(item));
_broadcastUpdate(collection);
}
function _broadcastUpdate(collection) {
$rootScope.$emit(_service.getUpdatedSignal(collection));
}
function _afterUpdate(collection, selected) {
_persistCollection(collection, selected);
_broadcastUpdate(collection);
}
function _getCollection(collection) {
if (!_.has(_collections, collection)) {
var data = _loadCollection(collection);
// Holds reference to a shared array. Only mutate, don't replace it.
_collections[collection] = data !== null ? data : [];
}
return _collections[collection];
}
function _add(item, path, collection) {
// Add `item` to `collection` where item will be identified by `path`.
// For example, path could be 'id', 'row_id', 'data.athlete_id',
// whatever fits the row data being added.
var selected = _getCollection(collection);
if (!_.any(selected, path, _.get(item, path))) {
selected.push(item);
}
_afterUpdate(collection, selected);
}
function _remove(item, path, collection) {
// Remove `item` from `collection`, where item is identified by `path`,
// just like in _add().
var selected = _getCollection(collection);
_.remove(selected, path, _.get(item, path));
_afterUpdate(collection, selected);
}
function _getUpdatedSignal(collection) {
return 'selectionService:updated:' + collection;
}
function _updateInGridSelections(gridApi, path, collection) {
var selectedInGrid = gridApi.getSelectedNodes(),
currentlySelected = _getCollection(collection),
gridPath = 'data.' + path;
_.each(selectedInGrid, function (node) {
if (!_.any(currentlySelected, path, _.get(node, gridPath))) {
// The following suppressEvents=true flag is ignored for now, but a
// fixing pull request is waiting at ag-grid GitHub.
gridApi.deselectNode(node, true);
}
});
var selectedIdsInGrid = _.pluck(selectedInGrid, gridPath),
currentlySelectedIds = _.pluck(currentlySelected, path),
missingIdsInGrid = _.difference(currentlySelectedIds, selectedIdsInGrid);
if (missingIdsInGrid.length > 0) {
// We're trying to avoid the following loop, since it seems horrible to
// have to loop through all the nodes only to select some. I wish there
// was a way to select nodes/rows based on an id.
var i;
gridApi.forEachNode(function (node) {
i = _.indexOf(missingIdsInGrid, _.get(node, gridPath));
if (i >= 0) {
// multi=true, suppressEvents=true:
gridApi.selectNode(node, true, true);
missingIdsInGrid.splice(i, 1); // Reduce haystack.
if (!missingIdsInGrid.length) {
// I'd love for `forEachNode` to support breaking the loop here.
}
}
});
}
}
var _service = {
getCollection: _getCollection,
add: _add,
remove: _remove,
getUpdatedSignal: _getUpdatedSignal,
updateInGridSelections: _updateInGridSelections
};
return _service;
});
The selectionService service allows adding and removing arbitrary objects to separate collections, identified by collection, a name you find suitable. This way the same service can be used for remembering selections in multiple ag-grid instances. Each object will be identified using a path parameter. The path is used to retrieve the unique identifier using lodash's get function.
Furthermore, the service uses sessionStorage to persist the selections during the user's whole tab/browser session. This might be overkill; we could have just relied on the service to keep track of the selections since it will only get instantiated once. This can of course be modified to your needs.
Then there were the changes that had to be done to the GridController. First of all the columnDefs entry for the first column had to be changed slightly
var columnDefs = [
{
headerName: "#",
width: 60,
field: 'id', // <-- Now we use a generated row ID.
checkboxSelection: true,
suppressSorting: true,
suppressMenu: true
}, …
where the new, generated row ID is generated once the data has been retrieved from the remote server
// Add row ids.
for (var i = 0; i < allOfTheData.length; i++) {
var item = allOfTheData[i];
item.id = 'm' + i;
}
(The 'm' in the ID was included just to make sure I didn't confused that ID with other IDs used by ag-grid.)
Next, the necessary changes to gridOptions were to add
{
…,
onRowSelected: rowSelected,
onRowDeselected: rowDeselected,
onBeforeFilterChanged: clearSelections,
onBeforeSortChanged: clearSelections,
…
}
Were the different handlers are quite straight forward, communicating with the selectionService
function rowSelected(event) {
selectionService.add(event.node.data, 'id', 'page-1');
}
function rowDeselected(event) {
selectionService.remove(event.node.data, 'id', 'page-1');
}
function clearSelections(event) {
$scope.gridOptions.api.deselectAll();
}
Now, the GridController needs to handle updates signalled by the selectionService too
$scope.$on('$destroy',
$rootScope.$on(selectionService.getUpdatedSignal('page-1'),
updateSelections));
and
function updateSelections() {
selectionService.updateInGridSelections($scope.gridOptions.api, 'id', 'page-1');
}
calls selectionService.updateInGridSelections which will update the in-grid selections of the grid in question. That was the most cumbersome function to write. For example, if a selection has been added externally (outside the grid), then we'll have to perform a forEachNode run, even if we know all the necessary nodes have already been selected in-grid; there's no way to exit that loop early.
Finally, another crux was to clear and reapply the selections before and after, respectively, when the filters or sort orders are changed, or when new data is retrieved from the server (which is only simulated in the demo). The solution was to include a call to updateSelections after the params.successCallback inside the getRows handler
params.successCallback(rowsThisPage, lastRow);
updateSelections();
Now, the most puzzling findings during the implementation of this solution was that the ag-grid API grid options onAfterFilterChanged and onAfterSortChanged couldn't be used for reapplying the selections because they trigger before the (remote) data has finished loading.
I have a 2 selects handled by angularjs and the related controller.
The optins if the second select depends on the user selection in the first select.
I have this code, as follows:
<tr>
<td>
Stade<br/>
<select data-ng-options="s.displayName for s in stages"
data-ng-model="accidentSearchSelectedStage"
onChange="javascript:getAccidentsModel(this)"
>
</select>
</td>
</tr>
<tr>
<td>
Organe<br/>
<select data-ng-options="o.displayName for o in organs | filter:accidentsSearch(accidentSearchSelectedStage, null, null, accidentsDiagnosticsMenu)"
data-ng-model="accidentSearchSelectedOrgan"
onChange="javascript:getAccidentsModel(this)"
>
</select>
</td>
</tr>
EDIT 1: added filter code snippet
$scope.accidentsSearch = function( stage, organ, symptom, accidentsDiagnosticsMenu ) {
if (organ==null && symptom ==null) {
console.log("------>filter organs for criteria stage: "+stage.displayName+", "+organ+", "+symptom);
return function( organToCheck ) {
if (!accidentsDiagnosticsMenu) return false; // not yet prepared
organToCheck = organToCheck.displayName;
var pruned = accidentsDiagnosticsMenu[stage.displayName];
//console.log("Exploring pruned with "+stage.displayName+": "+JSON.stringify(pruned));
for (var o in pruned) {
// Find one with same organs?
var keep = (o == organToCheck);
if (keep) {
//console.log("Checking organs "+o+"=="+organToCheck+" for stage "+ stage.displayName+ ", so keep = "+keep);
return true;
}
}
return false;
};
}
else
if (symptom == null) {
console.log("------>filter for criteria stage/organ : "+stage.displayName+", "+organ.displayName+", "+symptom);
return function(symptomToCheck ) {
Q1) This works nice except it sometime selects an empty additional option in the second select, depending on the previous selected value it had.
How to fix this?
Q2) I need to execute legacy javascript code anytime a new selection is made. Is the 'onchange' attribute the correct way to do, or is there an angularjs way to do it?
EDIT 2: $watch path exploration
I explored a solution with a $watch on the model connected to each select as follows:
$scope.$watch('accidentSearchSelectedStage', function (newValue, oldValue) {
console.log("accidentSearchSelectedStage changed from "+oldValue.id+" to "+newValue.id);
});
$scope.$watch('accidentSearchSelectedOrgan', function (newValue, oldValue) {
console.log("accidentSearchSelectedOrgan changed from "+oldValue.id+" to "+newValue.id);
//$scope.accidentSearchSelectedSymptom = $scope.symptoms[0];
});
The logs are ok, but I'm stuck at this point for 2 reasons:
What is the time diagram between the selection of say stage, and
the execution of the filter?
Where and how to code to check the
value of organ and symptom are inbounds?
If the model is set to something other than an available option, the select directive will automatically create an option for that value. Otherwise it won't know which one should be selected. Theoretically it could be the first one, but I'm sure the Angular guys have a valid reason for doing this.
Anyway, you basically just need to bootstrap your data in your controller to make sure that if it doesn't have a valid option, just set it to the default one.
E.g.
var data = getDataFromSomeSource();
var validOptions = ['foo','bar','default'];
if(validOptions.indexOf(data.option) < 0) {
data.option = 'default';
}
RE: #2 - onchange is probably the easiest way to do that for now, no point in adding additional/unnecessary non-angular code if this is just temporary until you port everything over.
I'm currently facing a problem with the Kendo UI MultiSelect widget for selecting an option more than once. For example, in the image below I want to select Schindler's List again after selecting The Dark Knight, but unfortunately the MultiSelect widget behaves more like a set than an ordered list, i.e. repetitive selection is not allowed. Is there actually a proper way to achieve this? Any workarounds?
That's the intended behavior of the multi-select control and there is no simple way to make it do what you want using the available configuration options. Possible workarounds are ...
Creating a custom multi-select widget
Something like this should work (note that I haven't tested this much - it lets you add multiples and keeps the filter intact though):
(function ($) {
var ui = kendo.ui,
MultiSelect = ui.MultiSelect;
var originalRender = MultiSelect.fn._render;
var originalSelected = MultiSelect.fn._selected;
var CustomMultiSelect = MultiSelect.extend({
init: function (element, options) {
var that = this;
MultiSelect.fn.init.call(that, element, options);
},
options: {
name: 'CustomMultiSelect'
},
_select: function (li) {
var that = this,
values = that._values,
dataItem,
idx;
if (!that._allowSelection()) {
return;
}
if (!isNaN(li)) {
idx = li;
} else {
idx = li.data("idx");
}
that.element[0].children[idx].selected = true;
dataItem = that.dataSource.view()[idx];
that.tagList.append(that.tagTemplate(dataItem));
that._dataItems.push(dataItem);
values.push(that._dataValue(dataItem));
that.currentTag(null);
that._placeholder();
if (that._state === "filter") {
that._state = "accept";
}
console.log(this.dataSource.view());
},
_render: function (data) {
// swapping out this._selected keeps filtering functional
this._selected = dummy;
return originalRender.call(this, data);
this._selected = originalSelected;
}
});
function dummy() { return null; }
ui.plugin(CustomMultiSelect);
})(jQuery);
Demo here.
Using a dropdown list
Use a simple dropdown list (or ComboBox) and bind the select event to append to your list (which you have to create manually).
For example:
var mySelectedList = [];
$("#dropdownlist").kendoDropDownList({
select: function (e) {
var item = e.item;
var text = item.text();
// store your selected item in the list
mySelectedList.push({
text: text
});
// update the displayed list
$("#myOrderedList").append("<li>" + text + "</li>");
}
});
Then you could bind clicks on those list elements to remove elements from the list. The disadvantage of that is that it requires more work to make it look "pretty" (you have to create and combine your own HTML, css, images etc.).
So I have datatable setup using the YUI 2.0. And for one of my column definitions, I've set it up so when you click on a cell, a set of radio button options pops so you can modify that cell content.
I want whatever changes are made to be reflected in the database. So I subscribe to a radioClickEvent. Here is my code for that:
Ex.myDataTable.subscribe("radioClickEvent", function(oArgs){
// hold the change for now
YAHOO.util.Event.preventDefault(oArgs.event);
// block the user from doing anything
this.disable();
// Read all we need
var elCheckbox = oArgs.target,
newValue = elCheckbox.checked,
record = this.getRecord(elCheckbox),
column = this.getColumn(elCheckbox),
oldValue = record.getData(column.key),
recordIndex = this.getRecordIndex(record),
session_code = record.getData(\'session_code\');
alert(newValue);
// check against server
YAHOO.util.Connect.asyncRequest(
"GET",
"inlineeddit.php?sesscode=session_code&",
{
success:function(o) {
alert("good");
var r = YAHOO.lang.JSON.parse(o.responseText);
if (r.replyCode == 200) {
// If Ok, do the change
var data = record.getData();
data[column.key] = newValue;
this.updateRow(recordIndex,data);
} else {
alert(r.replyText);
}
// unblock the interface
this.undisable();
},
failure:function(o) {
alert("bad");
//alert(o.statusText);
this.undisable();
},
scope:this
}
);
});
Ex.myDataTable.subscribe("cellClickEvent", Ex.myDataTable.onEventShowCellEditor);
But when I run my code and I click on a cell and I click a radio button, nothing happens ever. I've been looking at this for some time and I have no idea what I'm doing wrong. I know you can also use asyncsubmitter within my column definition I believe and I tried that, but that also wasn't working for me.
Any ideas would be greatly appreciated.