How do I use a class variable in the function() method? - function

Class Variable Name: addPointY
"addPointY" Using Function:
setInterval(function () {
var y = Math.round(Math.random() * 100);
series.addPoint(this.addPointY, true, true);
}, 3000);
I have to find a way to use it.
This is a customer requirement and has not been resolved.
Please tell me another way.
The class variable must be used in any of its methods.
But I could not get the class variable.
Do not you have a smart developer who solved the same problem?
#Injectable()
export class HighChartService implements ChartService {
private addPointY: number = 0;
shiftAddPoint(data: number) {
this.addPointY = data;
console.log(this.addPointY);
}
/**
* #see DynamicChart start function
* #param obj chart Object
* #param title Top Title
* #param type ChartType
* #param yAxisTitle Left Title
* #param series Chart data
* #author jskang
* #since 2017/10/12
*/
dynamicInitOptions(title: string, type: string, yAxisTitle: string, series: Object[]) {
if (!type) { type = "line"; }
let obj = new Chart({
chart: {
type: type,
events: {
load: function () {
// set up the updating of the chart each second
var series = this.series[0];
setInterval(function () {
var y = Math.round(Math.random() * 100);
series.addPoint(this.addPointY, true, true);
}, 3000);
}
}
},
title: { text: title },
xAxis: {
categories: [0,1,2,3,4,5,6],
labels: {
formatter: function () {
let xAxis = "";
if(this.value % 7 == 0){ xAxis = "일"; }
else if(this.value % 7 == 1){ xAxis = "월"; }
else if(this.value % 7 == 2){ xAxis = "화"; }
else if(this.value % 7 == 3){ xAxis = "수"; }
else if(this.value % 7 == 4){ xAxis = "목"; }
else if(this.value % 7 == 5){ xAxis = "금"; }
else if(this.value % 7 == 6){ xAxis = "토"; }
return xAxis;
}
}
},
yAxis: {
title: {
text: yAxisTitle
},
labels: {
formatter: function () {
return this.value;
}
}
},
legend: {
layout: 'vertical',
align: 'right',
verticalAlign: 'middle'
},
series: series
});
return obj;
}
}

The this inside your callback function for setInterval does not point to the current class instance because when you use function () {} syntax it creates its own binding for this based on how it is called.
To fix this use arrow functions which preserves the context and you can access your class properties inside the callback:
load: () => { // Notice arrow function here
// set up the updating of the chart each second
var series = this.series[0];
setInterval(() => { // Notice arrow function here
var y = Math.round(Math.random() * 100);
series.addPoint(this.addPointY, true, true);
}, 3000);
}
Another way you can solve this is by using the that pattern where you capture your this where it points to your class instance and use it wherever you need to refer to your instance:
dynamicInitOptions(title: string, type: string, yAxisTitle: string, series: Object[]) {
if (!type) { type = "line"; }
let that = this; // Capture `this` here
let obj = new Chart({
chart: {
type: type,
events: {
load: function () {
// set up the updating of the chart each second
var series = this.series[0];
setInterval(function () {
var y = Math.round(Math.random() * 100);
series.addPoint(that.addPointY, true, true); // Use `that` instead of `this here
}, 3000);
}
}
}
// ...
});
}

Related

How to import and use a custom Chart.js plugin in Nuxt? (Chartjs-vuejs v2.9.4)

I am trying to import a custom plugin into my chart.
Got this plugin from my previous question: Question
Its a plugin so that I can use Grace in my version of Chart.js.
The version of Chart.js I am using is V2.9.4.
I am using vue-chartjs in Nuxt!.
Couldn't really find an answer anywhere else.
This is how it looks now
This is how I want it to look
Thanks in advance. :)
I made a Component called 'BarChart' in my Components folder.
I made a normal .vue file in my pages directory. In the <template> tag I added my <Barchart/> component.
In that same .vue file I added a script in the <script> tag.
The plugin code is included in the codes below, I didn't include it anywhere yet.
Barchart.vue (Component)
<script>
import {Bar} from "vue-chartjs";
export default {
extends: Bar,
props: {
data: {
type: String,
default: () => {},
},
options: {
type: Object,
default: () => {},
},
},
computed: {
Chart() {
return['data', 'options'];
},
},
mounted() {
this.renderChart(this.data, this.options);
},
};
</script>
.vue file (Include component)
<div class="chart">
<BarChart :data="barChartData" :options="barChartOptions" :height="200"/>
</div>
.vue file (script tags)
<script>
import BarChart from "~/components/plugins/BarChart";
export default {
components: {
BarChart,
},
data() {
return {
barChartData: {
labels: ["Verzonden", "Ontvangen", "Geopend", "Kliks"],
datasets: [
{
data: [25, 20, 20, 18],
backgroundColor: [
'#7782FF',
'#403DD3',
'#FFB930',
'#00E437',
],
barThickness : 50,
},
],
},
barChartOptions: {
responsive: true,
legend: {
display: false,
},
scales: {
xAxes: [
{
gridLines: {
display: false,
},
ticks: {
fontColor: "black",
fontSize: 14,
},
},
],
yAxes: [
{
ticks: {
beginAtZero: true,
min: 0,
stepSize: 5,
fontColor: '#ABACB3',
},
gridLines: {
display: true,
borderDash: [4, 4],
color: '#EEEDFB',
drawBorder: false,
},
},
],
},
},
};
},
};
</script>
Plugin code (where do I put this and how do I make it work?)
const plugin = {
id: "customScale",
beforeLayout: (chart, options, c) => {
let max = Number.MIN_VALUE;
let min = Number.MAX_VALUE
let grace = options.grace || 0
chart.data.datasets.forEach((dataset) => {
max = Math.max(max, Math.max(...dataset.data));
min = Math.min(min, Math.min(...dataset.data))
})
if (typeof grace === 'string' && grace.includes('%')) {
grace = Number(grace.replace('%', '')) / 100
chart.options.scales.yAxes[0].ticks.suggestedMax = max + (max * grace)
chart.options.scales.yAxes[0].ticks.suggestedMin = min - (min * grace)
} else if (typeof grace === 'number') {
chart.options.scales.yAxes[0].ticks.suggestedMax = max + grace
chart.options.scales.yAxes[0].ticks.suggestedMin = min - grace
}
}
}
According to the vue-chartjs documentation you can do this in 2 ways.
If you want the plugin to be available for all your charts you can use the global registration like so:
import Chart from 'chart.js'
Chart.pluginService.register({
id: "customScale",
beforeLayout: (chart, options, c) => {
let max = Number.MIN_VALUE;
let min = Number.MAX_VALUE
let grace = options.grace || 0
chart.data.datasets.forEach((dataset) => {
max = Math.max(max, Math.max(...dataset.data));
min = Math.min(min, Math.min(...dataset.data))
})
if (typeof grace === 'string' && grace.includes('%')) {
grace = Number(grace.replace('%', '')) / 100
chart.options.scales.yAxes[0].ticks.suggestedMax = max + (max * grace)
chart.options.scales.yAxes[0].ticks.suggestedMin = min - (min * grace)
} else if (typeof grace === 'number') {
chart.options.scales.yAxes[0].ticks.suggestedMax = max + grace
chart.options.scales.yAxes[0].ticks.suggestedMin = min - grace
}
}
});
This way of importing and registering should work from anywhere in your app.
The second way is an inline plugin. This needs to be done in your BarChart.vue and goes like this:
mounted() {
this.addPlugin(
Chart.pluginService.register({
id: "customScale",
beforeLayout: (chart, options, c) => {
let max = Number.MIN_VALUE;
let min = Number.MAX_VALUE
let grace = options.grace || 0
chart.data.datasets.forEach((dataset) => {
max = Math.max(max, Math.max(...dataset.data));
min = Math.min(min, Math.min(...dataset.data))
})
if (typeof grace === 'string' && grace.includes('%')) {
grace = Number(grace.replace('%', '')) / 100
chart.options.scales.yAxes[0].ticks.suggestedMax = max + (max * grace)
chart.options.scales.yAxes[0].ticks.suggestedMin = min - (min * grace)
} else if (typeof grace === 'number') {
chart.options.scales.yAxes[0].ticks.suggestedMax = max + grace
chart.options.scales.yAxes[0].ticks.suggestedMin = min - grace
}
}
});
)
}

how to add AngularJS DHTML directive?

dhtml syntax help
this is the syntax used
I do not exhaust the complete dhtmlXGrid API here...
however configure and dataLoaded callbacks let user
add any additional configuration they desire
"use strict";
angular.module('dhxDirectives')
.directive('dhxGrid', function factory(DhxUtils) {
return {
restrict: 'E',
require: 'dhxGrid',
controller: function () {
},
scope: {
/**
* Grid will be accessible in controller via this scope entry
* after it's initialized.
* NOTE: For better design and testability you should use instead the
* configure and dataLoaded callbacks.
*/
dhxObj: '=',
/** Mandatory in current implementation! */
dhxMaxHeight: '=',
/** Optional. Default is 100%. */
dhxMaxWidth: '=',
/**
* Data is given here as an object. Not a filename! Must conform to the
* specified or default dataFormat
*/
dhxData: '=',
/**
* View possible formats here: http://docs.dhtmlx.com/grid__data_formats.html
* Currently supported:
* ['Basic JSON', 'Native JSON'] // 'Basic JSON' is default value
*/
dhxDataFormat: '=',
/** Optional! Recommended! http://docs.dhtmlx.com/api__dhtmlxgrid_setheader.html */
dhxHeader: '=',
/** Optional! http://docs.dhtmlx.com/api__dhtmlxgrid_setcoltypes.html */
dhxColTypes: '=',
/** Optional! http://docs.dhtmlx.com/api__dhtmlxgrid_setcolsorting.html */
dhxColSorting: '=',
/** Optional! http://docs.dhtmlx.com/api__dhtmlxgrid_setcolalign.html */
dhxColAlign: '=',
/** Optional! http://docs.dhtmlx.com/api__dhtmlxgrid_setinitwidthsp.html */
dhxInitWidths: '=',
/** Optional! http://docs.dhtmlx.com/api__dhtmlxgrid_setinitwidths.html */
dhxInitWidthsP: '=',
/**
* preLoad and postLoad callbacks to controller for additional
* customization power.
*/
dhxConfigureFunc: '=',
dhxOnDataLoaded: '=',
/**
* [{type: <handlerType>, handler: <handlerFunc>}]
* where type is 'onSomeEvent'
* Events can be seen at: http://docs.dhtmlx.com/api__refs__dhtmlxgrid_events.html
* Optional
*/
dhxHandlers: '=',
dhxVersionId: '=',
dhxContextMenu: '='
},
compile: function compile(/*tElement, tAttrs, transclude*/) {
return function (scope, element/*, attrs*/) {
var loadStructure = function () {
$(element).empty();
$('<div></div>').appendTo(element[0]);
var rootElem = element.children().first();
var width = scope.dhxMaxWidth ? (scope.dhxMaxWidth + 'px') : '100%';
var height = scope.dhxMaxHeight ? (scope.dhxMaxHeight + 'px') : '100%';
rootElem.css('width', width);
rootElem.css('height', height);
//noinspection JSPotentiallyInvalidConstructorUsage
if (scope.dhxObj) {
DhxUtils.dhxDestroy(scope.dhxObj);
}
scope.dhxObj = new dhtmlXGridObject(rootElem[0]);
var grid = scope.dhxObj;
grid.setImagePath(DhxUtils.getImagePath());
grid.enableAutoHeight(!!scope.dhxMaxHeight, scope.dhxMaxHeight, true);
grid.enableAutoWidth(!!scope.dhxMaxWidth, scope.dhxMaxWidth, true);
scope.dhxContextMenu ? grid.enableContextMenu(scope.dhxContextMenu) : '';
scope.$watch(
"dhxContextMenu",
function handle( newValue, oldValue ) {
grid.enableContextMenu(newValue);
}
);
scope.dhxHeader ? grid.setHeader(scope.dhxHeader): '';
scope.dhxColTypes ? grid.setColTypes(scope.dhxColTypes): '';
scope.dhxColSorting ? grid.setColSorting(scope.dhxColSorting): '';
scope.dhxColAlign ? grid.setColAlign(scope.dhxColAlign): '';
scope.dhxInitWidths ? grid.setInitWidths(scope.dhxInitWidths): '';
scope.dhxInitWidthsP ? grid.setInitWidthsP(scope.dhxInitWidthsP): '';
// Letting controller add configurations before data is parsed
if (scope.dhxConfigureFunc) {
scope.dhxConfigureFunc(grid);
}
grid.init();
// Finally parsing data
var dhxDataFormat = scope.dhxDataFormat || 'Basic JSON';
switch (dhxDataFormat) {
case 'Basic JSON':
grid.parse(scope.dhxData, 'json');
break;
case 'Native JSON':
grid.load(scope.dhxData, 'js');
break;
}
// Letting controller do data manipulation after data has been loaded
if (scope.dhxOnDataLoaded) {
scope.dhxOnDataLoaded(grid);
}
DhxUtils.attachDhxHandlers(grid, scope.dhxHandlers);
DhxUtils.dhxUnloadOnScopeDestroy(scope, grid);
};
scope.$watch('dhxVersionId', function (/*newVal, oldVal*/) {
console.log('rebuilding...');
loadStructure();
});
}
}
};
});
© 2020 GitHub, Inc.
I do not exhaust the complete dhtmlXGrid API here...
however configure and dataLoaded callbacks let user
add any additional configuration they desire
<dhx-grid
dhx-obj="grid.obj"
style="height: 100%"
dhx-data="gridData"
dhx-col-sorting="'str,str,int'"
dhx-header="'Title,Author,Copies sold'"
dhx-context-menu="contextMenu"
dhx-handlers="grid.handlers"></dhx-grid>
angular.module('myApp')
.controller('GridController', ['$scope' ,function ($scope) {
$scope.grid = {
obj: {},
handlers: [
{type: "onRowSelect", handler: function (id) {
$scope.grid.obj.deleteRow(id);
}}
]
};
$scope.alert = function alert(event_name) {
switch (event_name) {
case "refreshsize":
$scope.grid.obj.setSizes();
}
};
$scope.contextMenu = {};
$scope.gridData = {
rows:[
{ id:1, data: ["Click a row", "John Grasham", "100"]},
{ id:2, data: ["to have it", "Stephen Pink", "2000"]},
{ id:3, data: ["deleted", "Terry Brattchet", "3000"]},
{ id:4, data: ["La la la", "Isaac Zimov", "4000"]},
{ id:5, data: ["La la la", "Sax Pear", "5000"]}
]
};
}]);
"use strict";
/**
* Created by Emanuil on 01/02/2016.
*/
angular.module('dhxDirectives')
.factory('DhxUtils', [function () {
var _imgPath = "bower_components/dhtmlx/imgs/";
/**
* #param dhxObject
* #param dhxHandlers
*/
var attachDhxHandlers = function (dhxObject, dhxHandlers) {
(dhxHandlers || [])
.forEach(function (info) {
dhxObject.attachEvent(info.type, info.handler);
});
};
var getImagePath = function () {
return _imgPath;
};
var setImagePath = function (imgPath) {
_imgPath = imgPath;
};
/**
* I hope to never resort to using that
*/
var createCounter = function () {
var current = -1;
return function () {
current++;
return current;
};
};
var removeUndefinedProps = function(obj) {
for (var prop in obj) {
if (obj.hasOwnProperty(prop) && obj[prop] === undefined) {
delete obj[prop];
}
}
};
var dhxDestroy = function (dhxObj) {
var destructorName =
'destructor' in dhxObj
? 'destructor'
:
('unload' in dhxObj
? 'unload'
: null);
if (destructorName === null) {
console.error('Dhtmlx object does not have a destructor or unload method! Failed to register with scope destructor!');
return;
}
dhxObj[destructorName]();
};
var dhxUnloadOnScopeDestroy = function (scope, dhxObj) {
var destructorName =
'destructor' in dhxObj
? 'destructor'
:
('unload' in dhxObj
? 'unload'
: null);
if (destructorName === null) {
console.error('Dhtmlx object does not have a destructor or unload method! Failed to register with scope destructor!');
return;
}
scope.$on(
"$destroy",
function (/*event*/) {
dhxObj[destructorName]();
}
);
};
return {
attachDhxHandlers: attachDhxHandlers,
getImagePath: getImagePath,
setImagePath: setImagePath,
createCounter: createCounter,
removeUndefinedProps: removeUndefinedProps,
dhxUnloadOnScopeDestroy: dhxUnloadOnScopeDestroy,
dhxDestroy: dhxDestroy
};
}]);

Directive changes color and text on a fly

This is a directive that should change the color and text of the element depending on the incoming data
function colorStatus() {
return {
restrict: 'AE',
scope: {
status: '#'
},
link: function (scope, element) {
let status = +scope.status;
switch (status) {
case 0:
element.text(' ');
element.css('color', '#FFFFFF');
break;
case 1:
element.text('Correct!');
element.css('color', '#4CAF50');
break;
case 2:
element.text('Error!');
element.css('color', '#F44336');
break;
case 3:
element.text('Waiting...');
element.css('color', '#FF9800');
break;
}
}
};
}
Initially, it receives resolved data from the controller.
Here is HTML:
<color-status status="{{vm.status}}"></color-status>
<button ng-click="vm.changeStatus()"><button>
Here is function from controller:
vm.changeStatus = changeStatus;
vm.status = chosenTask.status; // It equals 0 in the received data
function changeStatus() {
vm.status = 1;
}
I expect that the text and color of the directive will change, but this does not happen. Where is my mistake?
Post link is only called once
The problem you're having is that you set your element's text and color in your link function. This means that when your directive instantiates and goes through initialisation, the link function will be executed, but it will get executed exactly once. When the value of status changes, you're not handling those changes to reflect the, on your element. Therefore you should add $onChanges() function to your directive and handle those changes.
function StatusController($element) {
this.$element = $element;
this.status = 0;
}
StatusController.mapper = [
{ text: ' ', color: '#FFFFFF' },
{ text: 'Correct!', color: '#4CAF50' },
{ text: 'Error!', color: '#F44336' },
{ text: 'Waiting...', color: '#FF9800' },
}];
StatusController.prototype.setStatus = function(status) {
var statusObj = StatusController.mapper[+status];
this.$element
.text(statusObj.text)
.css('color', statusObj.color);
}
StatusController.prototype.$onInit = function() {
// this.status is now populated by the supplied attribute value
this.setStatus(this.status);
}
StatusController.prototype.$onChanges = function(changes) {
if (changes.status && !changes.status.isFirstChange()) {
this.setStatus(this.status);
}
}
var StatusDirective = {
restrict: 'AE',
controller: StatusController,
scope: true,
bindToController: {
status: '#'
}
};
angular.module('someModule')
.directive('colorStatus', function() { return StatusDirective; });
But apart from this, I also suggest you set element's text by using ng-bind or {{...}} to put that value in. Directive could populate its public properties instead and use those in HTML along with CSS. It's always wiser to not manipulate DOM elements from within AngularJS code if possible.
function StatusController($element) {
this.$element = $element;
this.status = 0;
this.text = '';
this.name = '';
}
StatusController.mapper = [
{ text: ' ', name: '' },
{ text: 'Correct!', name: 'correct' },
{ text: 'Error!', name: '#error' },
{ text: 'Waiting...', name: 'pending' },
}];
StatusController.prototype.setStatus = function(status) {
var statusObj = StatusController.mapper[+status];
this.text = statusObj.text;
this.name = statusObj.name;
}
StatusController.prototype.$onInit = function() {
// this.status is now populated by the supplied attribute value
this.setStatus(this.status);
}
StatusController.prototype.$onChanges = function(changes) {
if (changes.status && !changes.status.isFirstChange()) {
this.setStatus(this.status);
}
}
var StatusDirective = {
restrict: 'AE',
controller: StatusController,
controllerAs: 'colorStatus',
scope: true,
bindToController: {
status: '#'
}
};
angular.module('someModule')
.directive('colorStatus', function() { return StatusDirective; });
And then in your template write use it this way:
<color-status status="{{vm.status}}" ng-class="colorStatus.name" ng-bind="colorStatus.text"></color-status>
This will give you a lot more flexibility in templates. Instead of setting text in the controller you could get away with just class name and use pseudo classes to add text to the element however you please to do, so each instance of your <color-status> directive could then display differently for the same status value.

How to restrict Google Maps search results to only one country properly?

I am developing an application, in Ionic, where you can plan a trip with a start address and and end address. However I want to limit this feature to only one country. Before writing I have been searching for solutions on the internet, but none of them worked for me.
Have tried these suggestions:
https://stackoverflow.com/a/8282093/8130808
https://stackoverflow.com/a/10170421/8130808
Here is how I have tried to approach it:
//Places markers and displays a route, so the user can accept the current placing
newRoutePlaceMarks(place: any): void {
var googleDiplay = new google.maps.DirectionsRenderer({ draggable: true });
var route = this.directionsDisplay;
//A bit of a hack, sadly Typescript and google maps arent the best of buddies
this.directionsService.route({
origin: this.routeStart,
destination: this.routeEnd,
travelMode: 'DRIVING',
}, function (response, status) {
if (status === 'OK') {
console.log("status is OK trying to put directions up");
//The reason why I've set the addListener before the actual route is so it gets triggered
//on the creation of the route. Had some problem with figuring out how to actually handle
//the data when on the route creation, as this response function is in strict mode, and outside
google.maps.event.addListener(route, "directions_changed", function () {
console.log("Route changed");
this.global = ShareService.getInstance();
this.directions = route.getDirections();
this.metersToDist = this.directions.routes[0].legs[0].distance.value;
this.timeToDist = this.directions.routes[0].legs[0].duration.value;
this.startAddress = this.directions.routes[0].legs[0].start_address;
this.startCord = this.directions.routes[0].legs[0].start_location;
this.endAddress = this.directions.routes[0].legs[0].end_address;
this.endCord = this.directions.routes[0].legs[0].end_location;
this.global.setMetersToDist(this.metersToDist);
this.global.setTimeToDist(this.timeToDist);
this.global.setStartAddress(this.startAddress);
this.global.setStartCord(this.startCord);
this.global.setEndAddress(this.endAddress);
this.global.setEndCord(this.endCord);
var options = {
types: ['geocode'],
componentRestrictions: { country: "dk" }
};
google.maps.places.Autocomplete(this.startAddress, options);
});
//The actual initialiser for the route
route.setDirections(response);
} else {
alert('Could not display route ' + status);
}
});
}
My problem is that the input is HTTPELEMENT, I get the input from an alert dialog
newRouteInput() {
let alert = this.alertCtrl.create({
title: 'New route',
inputs: [
{
name: 'routeStart',
placeholder: 'Start of route'
},
{
name: 'routeEnd',
placeholder: 'End of route'
}
],
buttons: [
{
text: 'Cancel',
role: 'cancel',
handler: data => {
console.log('Cancel clicked');
}
},
{
text: 'Debug start and end',
handler: data => {
if (data.username !== "undefined") {
console.log(data.routeStart + " " + data.routeEnd);
this.routeStart = "Brøndby Strand";
this.routeEnd = "Hvidovre";
this.newRoutePlaceMarks(this.map);
this.routeButton = false;
} else {
return false;
}
}
},
{
text: 'Place route markers',
handler: data => {
if (data.username !== "undefined") {
this.routeStart = data.routeStart;
this.routeEnd = data.routeEnd;
this.newRoutePlaceMarks(this.map);
this.routeButton = false;
} else {
console.log(data.routeStart + " " + data.routeEnd);
return false;
}
}
}
]
});
alert.present();
}
When I run this I get an error because of this.startAddress. It's not null, it contains the start address:
InvalidValueError: not an instance of HTMLInputElement

Transform Request to Autoquery friendly

We are working with a 3rd party grid (telerik kendo) that has paging/sorting/filtering built in. It will send the requests in a certain way when making the GET call and I'm trying to determine if there is a way to translate these requests to AutoQuery friendly requests.
Query string params
Sort Pattern:
sort[{0}][field] and sort[{0}][dir]
Filtering:
filter[filters][{0}][field]
filter[filters][{0}][operator]
filter[filters][{0}][value]
So this which is populated in the querystring:
filter[filters][0][field]
filter[filters][0][operator]
filter[filters][0][value]
would need to be translated to.
FieldName=1 // filter[filters][0][field]+filter[filters][0][operator]+filter[filters][0][value] in a nutshell (not exactly true)
Should I manipulate the querystring object in a plugin by removing the filters (or just adding the ones I need) ? Is there a better option here?
I'm not sure there is a clean way to do this on the kendo side either.
I will explain the two routes I'm going down, I hope to see a better answer.
First, I tried to modify the querystring in a request filter, but could not. I ended up having to run the autoqueries manually by getting the params and modifying them before calling AutoQuery.Execute. Something like this:
var requestparams = Request.ToAutoQueryParams();
var q = AutoQueryDb.CreateQuery(requestobject, requestparams);
AutoQueryDb.Execute(requestobject, q);
I wish there was a more global way to do this. The extension method just loops over all the querystring params and adds the ones that I need.
After doing the above work, I wasn't very happy with the result so I investigated doing it differently and ended up with the following:
Register the Kendo grid filter operations to their equivalent Service Stack auto query ones:
var aq = new AutoQueryFeature { MaxLimit = 100, EnableAutoQueryViewer=true };
aq.ImplicitConventions.Add("%neq", aq.ImplicitConventions["%NotEqualTo"]);
aq.ImplicitConventions.Add("%eq", "{Field} = {Value}");
Next, on the grid's read operation, we need to reformat the the querystring:
read: {
url: "/api/stuff?format=json&isGrid=true",
data: function (options) {
if (options.sort && options.sort.length > 0) {
options.OrderBy = (options.sort[0].dir == "desc" ? "-" : "") + options.sort[0].field;
}
if (options.filter && options.filter.filters.length > 0) {
for (var i = 0; i < options.filter.filters.length; i++) {
var f = options.filter.filters[i];
console.log(f);
options[f.field + f.operator] = f.value;
}
}
}
Now, the grid will send the operations in a Autoquery friendly manner.
I created an AutoQueryDataSource ts class that you may or may not find useful.
It's usage is along the lines of:
this.gridDataSource = AutoQueryKendoDataSource.getDefaultInstance<dtos.QueryDbSubclass, dtos.ListDefinition>('/api/autoQueryRoute', { orderByDesc: 'createdOn' });
export default class AutoQueryKendoDataSource<queryT extends dtos.QueryDb_1<T>, T> extends kendo.data.DataSource {
private constructor(options: kendo.data.DataSourceOptions = {}, public route?: string, public request?: queryT) {
super(options)
}
defer: ng.IDeferred<any>;
static exportToExcel(columns: kendo.ui.GridColumn[], dataSource: kendo.data.DataSource, filename: string) {
let rows = [{ cells: columns.map(d => { return { value: d.field }; }) }];
dataSource.fetch(function () {
var data = this.data();
for (var i = 0; i < data.length; i++) {
//push single row for every record
rows.push({
cells: _.map(columns, d => { return { value: data[i][d.field] } })
})
}
var workbook = new kendo.ooxml.Workbook({
sheets: [
{
columns: _.map(columns, d => { return { autoWidth: true } }),
// Title of the sheet
title: filename,
// Rows of the sheet
rows: rows
}
]
});
//save the file as Excel file with extension xlsx
kendo.saveAs({ dataURI: workbook.toDataURL(), fileName: filename });
})
}
static getDefaultInstance<queryT extends dtos.QueryDb_1<T>, T>(route: string, request: queryT, $q?: ng.IQService, model?: any) {
let sortInfo: {
orderBy?: string,
orderByDesc?: string,
skip?: number
} = {
};
let opts = {
transport: {
read: {
url: route,
dataType: 'json',
data: request
},
parameterMap: (data, type) => {
if (type == 'read') {
if (data.sort) {
data.sort.forEach((s: any) => {
if (s.field.indexOf('.') > -1) {
var arr = _.split(s.field, '.')
s.field = arr[arr.length - 1];
}
})
}//for autoquery to work, need only field names not entity names.
sortInfo = {
orderByDesc: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'desc'), 'field'), ','),
orderBy: _.join(_.map(_.filter(data.sort, (s: any) => s.dir == 'asc'), 'field'), ','),
skip: 0
}
if (data.page)
sortInfo.skip = (data.page - 1) * data.pageSize,
_.extend(data, request);
//override sorting if done via grid
if (sortInfo.orderByDesc) {
(<any>data).orderByDesc = sortInfo.orderByDesc;
(<any>data).orderBy = null;
}
if (sortInfo.orderBy) {
(<any>data).orderBy = sortInfo.orderBy;
(<any>data).orderByDesc = null;
}
(<any>data).skip = sortInfo.skip;
return data;
}
return data;
},
},
requestStart: (e: kendo.data.DataSourceRequestStartEvent) => {
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if ($q)
ds.defer = $q.defer();
},
requestEnd: (e: kendo.data.DataSourceRequestEndEvent) => {
new DatesToStringsService().convert(e.response);
let ds = <AutoQueryKendoDataSource<queryT, T>>e.sender;
if (ds.defer)
ds.defer.resolve();
},
schema: {
data: (response: dtos.QueryResponse<T>) => {
return response.results;
},
type: 'json',
total: 'total',
model: model
},
pageSize: request.take || 40,
page: 1,
serverPaging: true,
serverSorting: true
}
let ds = new AutoQueryKendoDataSource<queryT, T>(opts, route, request);
return ds;
}
}