I am trying to integrate Kendo UI into AngularJS. I've come across an issue which I can't seem to solve. I am fairly new to AngularJS and am trying to follow the guidelines as best as possible.
The issue at hand is that I am unable to obtain a reference to my Kendo UI grid. I've already gone through a couple of similar SO questions, but none seem to resolve the issue. The main difference in my code is that I'm using controllerAs instead of $scope.
Datagrid Controller:
'use strict';
angular
.module('ayotaWebFeApp.datagrid')
.controller('DataGridController', DataGridController);
DataGridController.$inject = ['$scope','$routeParams', 'datagriddataservice'];
function DataGridController($scope, $routeParams, dataservice) {
console.info('DataGridController instantiated.');
var vm = this;
vm.grid = {};
vm.dataTitle = $routeParams.datatype;
activate();
$scope.$on('kendoWidgetCreated', function(event, widget) {
console.log('kendowidgetcreated event: ', event);
console.log('kendowidgetcreated widget: ', widget);
$scope.$apply();
});
function activate() {
console.info('Activate was called.');
return getDataFields().then(function() {
console.info('Loaded data fields for data type :'+$routeParams.datatype);
});
}
function initGridOptions(columnSpec) {
var restURL = '/api/data';
if($routeParams.ups) {
restURL += '/UPS/' + $routeParams.ups;
} else {
restURL += '/adm';
}
restURL += '/' + $routeParams.historical;
restURL += '/' + $routeParams.datatype;
console.info('Data REST url : ', restURL);
console.info('fields: ', columnSpec);
vm.dataGridOptions = {
name: 'grid',
columns : columnSpec['columns'],
dataSource: {
type: 'json',
transport: {
read: 'http://localhost:57713'+restURL
},
schema: {
model: {
fields : columnSpec['fields']
}
}
},
sortable: true,
filterable: true,
scrollable: true,
reorderable: true,
resizable: true,
selectable: 'single row',
columnMenu: true,
pageable: {
refresh: true,
buttonCount: 5
},
dataBound: onDataBound,
height: '100%'
};
console.info('vm.dataGridOptions : ', vm.dataGridOptions);
console.info('scope after datagridoptionsloaded: ', $scope);
console.info('vm.grid: ', vm.grid);
console.info('scope.vm.grid: ', $scope.vm.grid);
}
function getDataFields() {
return dataservice.getDataFields($routeParams.datatype)
.then(function(data) {
initGridOptions(data);
});
}
function onDataBound(ev) {
console.log('DATABOUND !');
//vm.grid = ev.sender;
var gh = $('.datagrid-grid').find('.k-grid-header');
var gc = $('.datagrid-grid').find('.k-grid-content');
//var rightPaddingHeader = gh.css("padding-right");
gh.hide();
gh.css('padding-right', '0px');
gc.css('overflow-y', 'auto');
gh.show();
//resizeGrid();
//unbind event
vm.grid.unbind('dataBound');
}
function resizeGrid() {
console.log('vm.grid.dataSource: ', vm.grid.dataSource);
// trigger layout readjustment
vm.grid.resize();
// force layout readjustment
vm.grid.resize(true);
//set new pagesize according to content height to avoid inner scrollbars
var gridContentWidget = $('.datagrid-grid .k-grid-content');
console.info('gridContentWidget.height(): ', gridContentWidget.height());
var rowHeight = $('.datagrid-grid .k-grid-content tr').first().height();
var newPageSize = Math.floor(gridContentWidget.height() / rowHeight);
console.info('Setting dataSource to new pageSize: ', newPageSize);
if(newPageSize != vm.grid.dataSource.pageSize())
vm.grid.dataSource.pageSize(newPageSize);
console.info('DataSource pageSize after set: ', vm.grid.dataSource.pageSize);
}
}
And the corresponding html
<div class="datagrid-container">
<h2>{{vm.dataTitle}}</h2>
<div class="datagrid-grid">
<div kendo-grid="vm.grid"
k-options="vm.dataGridOptions"
k-ng-delay="vm.dataGridOptions"
datagrid-resize />
</div>
</div>
Only in the onDataBound function can I get a reference through the event sender.
Any suggestions are highly appreciated.
try this:
vm.gridReference = $('div[kendo-grid]').data('kendoGrid');
I haven't yet figured out a decent way of getting a grid reference without throwing JQuery around but this will do for a quick and dirty.
Related
I have a service that will return my some config options for an ng-grid. The getGridOptions function takes the name of the controller it's used on and returns the correct set of options (only one shown here for brevity).
service for ng-grid options:
angular.module('services').service('GridOptionsService',function(){
var documents = {
data: 'myData',
enablePaging: true,
showFooter:true,
totalServerItems: 'totalServerItems',
pagingOptions: {
pageSizes: [50,100,200],
pageSize: 50,
currentPage: 1
},
filterOptions: {
filterText: '',
useExternalFilter: false
},
enableCellEdit: false,
enableColumnReordering: true,
enablePinning: false,
showGroupPanel: false,
groupsCollapsedByDefault: true,
enableColumnResize: true,
showSelectionCheckbox: true,
selectWithCheckboxOnly: true,
columnDefs: [
{field:'docId', displayName:'Document ID', cellTemplate: NgGridDomUtil.toLink('#/documents/{{row.getProperty(col.field)}}')},
{field:'docTags', displayName:'Tags'},
{field:'lastSaveDate', displayName:'Last saved'},
{field:'documentVersion', displayName:'Version', width: 120},
{field:'busDocId', displayName:'Customer Doc ID'},
{field:'markedForDelete', displayName:'Deleted', width: 120, cellTemplate: NgGridDomUtil.toCheckbox('{{row.getProperty(col.field)}}')}]
};
var gridOptionManager = {
documents: documents
}
return {
getGridOptions: function(controllerName){
return gridOptionManager[controllerName];
}
}
})
The NgGridDomUtil class just makes it easier to style things on the grid:
var NgGridDomUtil = (function(){
var toLink = function(href){
var html = '<div class="ngCellText" ng-class="col.colIndex()"><a ng-href= "'+href+'" class="ngCellLink"><span ng-cell-text>{{row.getProperty(col.field)}}</span></a></div>'
return html;
}
var toCheckbox = function(_selected){
var html = '<div class="ngCellText" ng-class="col.colIndex()"><input type="checkbox" ng-change="console.log('+"TEST"+')" ng-model="COL_FIELD" ng-input="COL_FIELD"' + (_selected ? 'selected' : '') + ' /></div>'
return html
}
return {
toLink: toLink,
toCheckbox: toCheckbox
}
})();
My problem is what when I use the GridOptionsService to retrieve the data, the data is still presented to the grid correctly, but the text filtering no longer works and the paging is broken. However, the selectedFilterOption still works.
controller:
angular.module('controllers').controller('Repository', ['$scope', 'DataContext','GridOptionsService','$http', function($scope, DataContext,GridOptionsService,$http) {
$scope.filterOptions = {
filterText: '',
useExternalFilter: false
};
$scope.totalServerItems =0;
$scope.pagingOptions ={
pageSizes: [5,10,100],
pageSize: 5,
currentPage: 1
}
//filter!
$scope.dropdownOptions = [{
name: 'Show all'
},{
name: 'Show active'
},{
name: 'Show trash'
}];
//default choice for filtering is 'show active'
$scope.selectedFilterOption = $scope.dropdownOptions[1];
//three stage bool filter
$scope.customFilter = function(data){
var tempData = [];
angular.forEach(data,function(item){
if($scope.selectedFilterOption.name === 'Show all'){
tempData.push(item);
}
else if($scope.selectedFilterOption.name ==='Show active' && !item.markedForDelete){
tempData.push(item);
}
else if($scope.selectedFilterOption.name ==='Show trash' && item.markedForDelete){
tempData.push(item);
}
});
return tempData;
}
//grabbing data
$scope.getPagedDataAsync = function(pageSize, page, searchText){
var data;
if(searchText){
var ft = searchText.toLowerCase();
DataContext.getDocuments().success(function(largeLoad){
//filter the data when searching
data = $scope.customFilter(largeLoad).filter(function(item){
return JSON.stringify(item).toLowerCase().indexOf(ft) != -1;
})
$scope.setPagingData($scope.customFilter(data),page,pageSize);
})
}
else{
DataContext.getDocuments().success(function(largeLoad){
var testLargeLoad = $scope.customFilter(largeLoad);
//filter the data on initial page load when no search text has been entered
$scope.setPagingData(testLargeLoad,page,pageSize);
})
}
};
//paging
$scope.setPagingData = function(data, page, pageSize){
var pagedData = data.slice((page -1) * pageSize, page * pageSize);
//filter the data for paging
$scope.myData = $scope.customFilter(pagedData);
$scope.myData = pagedData;
$scope.totalServerItems = data.length;
// if(!$scope.$$phase){
// $scope.$apply();
// }
}
//watch for filter option change, set the data property of gridOptions to the newly filtered data
$scope.$watch('selectedFilterOption',function(){
var data = $scope.customFilter($scope.myData);
$scope.myData = data;
$scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage);
$scope.setPagingData($scope.myData,$scope.pagingOptions.currentPage,$scope.pagingOptions.pageSize);
})
$scope.$watch('pagingOptions',function(newVal, oldVal){
$scope.getPagedDataAsync($scope.pagingOptions.pageSize,$scope.pagingOptions.currentPage,$scope.filterOptions.filterText);
$scope.setPagingData($scope.myData,$scope.pagingOptions.currentPage,$scope.pagingOptions.pageSize);
},true)
$scope.message ="This is a message";
$scope.gridOptions = {
data: 'myData',
enablePaging: true,
showFooter:true,
totalServerItems: 'totalServerItems',
pagingOptions: $scope.pagingOptions,
filterOptions: $scope.filterOptions,
enableCellEdit: true,
enableColumnReordering: true,
enablePinning: true,
showGroupPanel: true,
groupsCollapsedByDefault: true,
enableColumnResize: true
}
$scope.gridOptions = GridOptionsService.getGridOptions('documents');
//get the data on page load
$scope.getPagedDataAsync($scope.pagingOptions.pageSize, $scope.pagingOptions.currentPage);
}]);
The grid options that I have hard coded into the controller are the same as the ones returned from the service. What I don't understand is why the grid renders, the dropdown filter works, but the paging is broken, only when the options for the grid come from a service? But it works as expected if it's hard coded into the controller.
EDIT: If someone can more eloquently state my problem, feel free to edit the title.
I don't really know how the ngGrid is implemented, but a common pitfall I do know that exist in many directives, is they expect their configurations to be ready as soon as they're initialized. Meaning that instead of watching the configuration object, they assume it exists and use it directly in the link\controller functions which runs as soon as they're created.
If this is indeed the case, a quick workaround to the problem is initializing the directive only when you have the configuration object. Let's say you pass on the configuration object through the variable 'options' on your scope, you'll then write something like:
<!-- If options exists on your scope, it means you fetched it from the server -->
<div ng-if="options">
<div ng-grid ng-grid-options="options"></div>
</div>
Again, I'm not familiar with ngGrid or its usage, this is just an educated guess, take the conclusions and apply them on the correct API.
I haven't tested this, but one possible issue is that you are overwriting an object that is on the $scope. This can break the two way binding. For a quick test try
$scope.grid = {
Options: {
data: 'myData',
enablePaging: true,
showFooter:true,
totalServerItems: 'totalServerItems',
pagingOptions: $scope.pagingOptions,
filterOptions: $scope.filterOptions,
enableCellEdit: true,
enableColumnReordering: true,
enablePinning: true,
showGroupPanel: true,
groupsCollapsedByDefault: true,
enableColumnResize: true
}
}
$scope.grid.Options = GridOptionsService.getGridOptions('documents');
You would need to update the grid options in the directives attribute as well of course.
The problem is that the controller functions for filtering and paging use the options defined on the controller $scope, but the ng-grid UI is not bound to those objects.
The controller methods for filtering and paging use $scope.pagingOptions as the data source. However, the ng-grid UI is bound to $scope.gridOptions.pagingOptions. When you create the $scope.gridOptions explicitly in the controller, $scope.gridOptions.pagingOptions refers to the same object as $scope.gridOptions, so making changes in the UI will change the value used in the controller functions.
However, when $scope.gridOptions is assigned using the service, the service is creating new pagingOptions, so there is no connection between $scope.gridOptions.pagingOptions and $scope.pagingOptions. Making changes in the UI will not change the values used in the controller functions.
The same is true for filterOptions.
One way to resolve the issue is
$scope.gridOptions = GridOptionsService.getGridOptions('documents');
$scope.pagingOptions = $scope.gridOptions.pagingOptions
$scope.filterOptions = $scope.gridOptions.filterOptions
I am building a website using MVC 4, Web API, and Kendo UI controls.
On my page I am using a Kendo UI Listview to filter my grid. I'm trying to add an "ALL" option as the first item in my listview.
Here is the listview:
var jobsfilter = $("#jobfilter").kendoListView({
selectable: "single",
loadOnDemand: false,
template: "<div class='pointercursor' id=${FilterId}>${FilterName}</div>",
dataSource: filterDataSource,
change: function (e) {
var itm = this.select().index(), dataItem = this.dataSource.view()[itm];
if (dataItem.FilterId !== 0) {
var $filter = new Array();
$filter.push({ field: "JobStatusId", operator: "eq", value: dataItem.FilterId });
jgrid.dataSource.filter($filter);
} else {
jgrid.dataSource.filter({});
}
}
});
Here is my datasource:
var filterDataSource = new kendo.data.DataSource({
transport: {
read: {
url: "api/Filter"
}
},
schema: {
model: { id: "FilterId" }
}
});
I have tried a few different methods to make this happen:
I can make it work if I attach it to a button - but I need it there
when the data loads.
If I add it to the dataBound event of the listview, it causes the
databound event to go into a loop and adds the item a bunch (IE) or kills the browser (firefox). Adding preventDefault did nothing.
I've read up on adding a function to the Read paramter of the
datasource, but I think that is simply not the correct place to do
it.
Based on what I've read, I think that I should be able to do it in the dataBound event of the listview and that my implementation is incorrect. Here is the listview with dataBound event added that crashes my browser (Firefox) - or adds about 50 "All" items to the listview (IE).
var jobsfilter = $("#jobfilter").kendoListView({
selectable: "single",
loadOnDemand: false,
template: "<div class='pointercursor' id=${FilterId}>${FilterName}</div>",
dataSource: {
transport: {
read: {
url: "api/Filter"
}
}
},
dataBound: function (e) {
var dsource = $("#jobfilter").data("kendoListView").dataSource;
dsource.insert(0, { FilterId: 0, FilterName: "All" });
e.preventDefault();
},
change: function (e) {
var itm = this.select().index(), dataItem = this.dataSource.view()[itm];
if (dataItem.FilterId !== 0) {
var $filter = new Array();
$filter.push({ field: "JobStatusId", operator: "eq", value: dataItem.FilterId });
jgrid.dataSource.filter($filter);
} else {
jgrid.dataSource.filter({});
}
}
});
Any help would be greatly appreciated.
Why don't you add it server-side?
Anyway, if you want to do it in dataBound, just check whether it exists and only add if it doesn't:
dataBound: function (e) {
var dsource = this.dataSource;
if (dsource.at(0).FilterName !== "All") {
dsource.insert(0, {
FilterId: 0,
FilterName: "All"
});
}
},
As an explanation to the problem you're seeing: you're creating an infinite loop since inserting an element in the data source will trigger the change event and the list view will refresh and bind again (and thus trigger dataBound).
You could also encapsulate this in a custom widget:
(function ($, kendo) {
var ui = kendo.ui,
ListView = ui.ListView;
var CustomListView = ListView.extend({
init: function (element, options) {
// base call to widget initialization
ListView.fn.init.call(this, element, options);
this.dataSource.insert(0, {
FilterId: 0,
FilterName: "All"
});
},
options: {
name: "CustomListView"
}
});
ui.plugin(CustomListView);
})(window.jQuery, window.kendo);
I can't figure out what is going on here. I'm trying to make a custom directive for grids and will use element attributes to customize a given instance. In doing so i've made two files
grid-controller.js
app.controller('gridController', ['$scope', function ($scope ) {
//Initilization code
$scope.gridOptions = {
//Setup options
};
$scope.detailOptions = function (e) {
console.log('winning');
return {
dataSource: {
transport: {
read: {
url: "/detail" + e.OrderNumber + ".json",
dataType: 'json'
}
},
error: function (e) {
console.log(e);
},
pageSize: true,
serverPaging: false,
serverFiltering: false,
serverSorting: false,
},
columns: [
{
field: "ItemCode",
label: "lblItemCode",
title: ""
}, {
field: "ItemDesc",
label: "lblItemDesc",
title: ""
}, {
field: "QuantityOrdered",
label: "lblQuantityOrdered",
title: ""
}
],
scrollable: false,
sortable: true
};
}
}]);
grid-directive.js
app.directive('grid', function () {
return {
// Restrict E for element
restrict: 'E',
// Here we setup the template for the code we want to replace our directive
template: "<div> \n\
<div kendo-grid='grid' \n\
k-options='gridOptions'\n\
k-data-source='dataSource'>\n\
</div> \n\
<script type='text/x-kendo-template'\n\
id='details'>\n\
<div kendo-grid >\n\
</div>\n\
</script>\n\
</div>",
replace: true,
scope: {
},
controller: "gridController",
link: function (scope, element, attrs) {
//Gather some attribute data and set it to the gridOptions object
if(scope.$eval(attrs.detailsGrid))
{
scope.gridOptions.detailTemplate = kendo.template($("#details").html());
scope.gridOptions.detailInit = scope.detailOptions;
}
//More customization code
scope.dataSource = new kendo.data.DataSource({
//Setup dataSource options for main grid
});
}
};
});
For sake of brevity i've excluded a lot of the extra code.
My problem is whenever I try to open the details of a row the row opens...closes...and the grid appears to refresh. It almost looks like something is crashing and the main grid is refreshing as a result.
Here is the associated plunkr with the commented portions fleshed out.
So the day after I posted the question angular-kendo released an update that addressed this issue. After updating the library and fixing up my code a bit the details grid works as expected!
There is a form on my page with textarea (CKEDITOR) and select field <select id="_photogalleries" multiple="multiple"></select>. I'd like options in RichCombo to depend on the options that are selected in select with id #_photogalleries. Is there any way to regenerate RichCombo dynamically?
Thanks in advance.
CKEDITOR.plugins.add('style_plugin', {
requires: ['richcombo'],
init: function(editor) {
var pluginName = 'style_plugin';
var config = editor.config,
lang = editor.lang.format;
editor.ui.addRichCombo('photogalleries', {
label: "Фоторепортаж",
title: "Фоторепортаж",
voiceLabel: "Фоторепортаж",
className: 'cke_format',
multiSelect: false,
icon: CKEDITOR.plugins.getPath('style_plugin') + 'photo-list-horizontal.png',
panel: {
css: [config.contentsCss, CKEDITOR.getUrl(editor.skinPath + 'editor.css')],
voiceLabel: lang.panelVoiceLabel
},
init: function () {
this.startGroup("Фоторепортаж");
var list=this;
$("#_photogalleries option:selected").each(function(index, value){
console.log(index, value);
list.add("#HORIZONTAL_GALLERY_"+ $(value).val()+"#", "(Г) " + $(value).text(), "(Г) " + $(value).text());
list.add("#VERTICAL_GALLERY_"+ $(value).val()+"#", "(В) " + $(value).text(), "(В) " + $(value).text());
});
},
onClick: function (value) {
editor.focus();
editor.fire('saveSnapshot');
editor.insertHtml(value);
editor.fire('saveSnapshot');
}
});
}
});
This works for me and you dont have to keep a global variable.
CKEDITOR.plugins.add('systemdata', {
init: function (editor) {
var fnData = editor.config.fnData;
if (!fnData || typeof (fnData) != 'function')
throw "You must provide a function to retrieve the list data.";
editor.ui.addRichCombo('systemDataCmb',
{
allowedContent: 'abbr[title]',
label: "System Data",
title: "System Data",
multiSelect: false,
init: function () {
var self = this;
var content = fnData();
$.each(content, function(index, value) {
// value, html, text
self.add(value.name, value.name, value.name)
});
}
}
Then to set the function to get the data put this somewhere where you setup the ckeditor
CKEDITOR.replaceAll(function(element, config) {
config.startupFocus = true;
config.fnData = function() {
var returnData = null;
$.ajax({
url: "/GetData",
async: false,
data: { id: 1 },
}).done(function(result) { returnData= result; });
return returnData;
};
});
It assumes you bring back a json response that has an array of items that have a value property, that can be easily changed though.
I guess I found a solution that worked for me. It was to keep a list object in a global variable and then modify it when onchange event fires in the external select.
I solved this trouble with a single line:
YOURCOMBO.createPanel(editor);
For example:
var comboTeam = editor.ui.get("team");
comboTeam.createPanel(editor);//This is important, if not, doesnt works
Now you can add items to the combo
comboTeam.add("name","name","name");
comboTeam.add("name2","name2","name2");
comboTeam.add("name3","name3","name3");
Trying to create an inline edit form.
I have a form that looks like this:
var editPic = "<img src='https://s3.amazonaws.com/bzimages/pencil.png' alt='edit' height='24' width='24' style='margin-left: 10px;'/>";
var submitPic = "<img id='submitPic' src='https://s3.amazonaws.com/bzimages/submitPic.png' alt='edit' height='24' width='24'/>";
Ext.define('BM.view.test.Edit', {
extend: 'Ext.form.Panel',
alias: 'widget.test-edit',
layout: 'anchor',
title: 'Edit Test',
defaultType: 'displayfield',
items: [
{name: 'id', hidden: true},
{
name: 'name',
fieldLabel: 'Name',
afterSubTpl: editPic,
cls: 'editable'
},
{
name: 'nameEdit',
fieldLabel: 'Name',
xtype: 'textfield',
hidden: true,
cls: 'editMode',
allowBlank: false,
afterSubTpl: submitPic
}
]
});
The controller looks like this (a lot of events):
init: function() {
this.control({
'test-edit > displayfield': {
afterrender: this.showEditable
},
'test-edit': {
afterrender: this.formRendered
},
'test-edit > field[cls=editMode]': {
specialkey: this.editField,
blur: this.outOfFocus
}
});
},
outOfFocus: function(field, event) {
console.log('Lost focus');
this.revertToDisplayField(field);
},
revertToDisplayField: function(field) {
field.previousNode().show();
field.hide();
},
formRendered: function(form) {
Ext.get('submitPic').on('click', function (event, object) {
var field = Ext.get(object).parent().parent().parent().parent();
var cmp = Ext.ComponentQuery.query('test-edit > field[cls=editMode]');
});
},
editField: function(field, e) {
var value = field.value;
if (e.getKey() === e.ENTER) {
if (!field.allowBlank && Ext.isEmpty(value)){
console.log('Not permitted!');
} else {
var record = Ext.ComponentQuery.query('test-edit')[0].getForm().getRecord();
Ext.Ajax.request({
url: '../webapp/tests/update',
method:'Post',
params: {
id: record.getId(),
fieldName: field.name,
fieldValue: field.value
},
store: record.store,
success: function(response, t){
field.previousNode().setValue(value);
t.store.reload();
var text = response.responseText;
// process server response here
console.log('Update successful!');
}
});
}
this.revertToDisplayField(field);
} else if (e.getKey() === e.ESC) {
console.log('gave up');
this.revertToDisplayField(field);
}
},
showEditable: function(df) {
df.getEl().on("click", handleClick, this, df);
function handleClick(e, t, df){
e.preventDefault();
var editable = df.nextNode();
editable.setValue(df.getValue());
editable.show();
editable.focus();
df.hide();
}
},
I'm using the 'afterSubTpl' config to add the edit icon, and the accept icon.
I have listeners set up to listen on click events concerning them, but after they are clicked, I only have the element created by Ext.get('submitPic'). Now I want to have access to the the Ext field and form that surround it. The parent method only brings back other DOM elements. How do I connect between them? You can see what I tried in formRendered.
I hope someone can clarify this little bit for me.
Walk up the DOM tree until you find a component for the element's id:
getCmpFromEl = function(el) {
var body = Ext.getBody();
var cmp;
do {
cmp = Ext.getCmp(el.id);
el = el.parentNode;
} while (!cmp && el !== body);
return cmp;
}
Ext.Component.from(el) does exactly this since ExtJS 6.5.0, as I just learnt. Doc
Source
You can get the component by id, but only if your component and its dom element have the same id (they usually do):
Ext.getCmp(yourelement.id)
But this is not exactly good practice -- it would be better to set up your listeners so that the handler methods already have a reference to the component. For example, in your 'submitPic' component, you could define the click listener like this:
var me = this;
me.on({
click: function(arguments){
var cmp = me;
...
});