Kendo Sortable integrated with a grouped Grid - asp.net-mvc

Here's my View, in which I have a Sortable that's integrated with a Grid. This works fine, but the problem is that the Grid is grouped. And, I want each group to have its own Sortable functionality, meaning that the user should not be able to drag and drop rows from one group to another. How do I do that? Do I have to have separate Sortables for each group?
#(Html.Kendo().Grid<QRMT.ViewModels.SubsystemViewModel>()
.Name("subsystems")
.ToolBar(toolbar => toolbar.Create().Text("Add new Subsystem"))
.Columns(columns =>
{
columns.ForeignKey(c => c.SystemId, new SelectList(ViewBag.Systems, "Value", "Text")).Hidden();
columns.Bound(c => c.SubsystemCode);
columns.Bound(c => c.SubsystemDesc);
columns.Command(c => { c.Edit(); c.Destroy(); }).Width(200);
})
.Editable(e => e.Mode(GridEditMode.PopUp).Window(window => window.Width(500)))
.DataSource(dataSource => dataSource
.Ajax()
.Events(events => events.Sync("onSync").Error("onError"))
.Model(model =>
{
model.Id(m => m.SubsystemId);
})
.Group(group => group.Add(m => m.SystemId))
.Create(create => create.Action("Add", "Subsystems"))
.Read(read => read.Action("Read", "Subsystems"))
.Update(update => update.Action("Update", "Subsystems"))
.Destroy(destroy => destroy.Action("Delete", "Subsystems"))
)
.Events(events => events.Edit("onEdit"))
)
#(Html.Kendo().Sortable()
.For("#subsystems")
.Filter("table > tbody > tr:not(.k-grouping-row)")
.Cursor("move")
.HintHandler("noHint")
.PlaceholderHandler("placeholder")
.ContainerSelector("#subsystems tbody")
.Events(events => events.Change("onChange"))
)
<script type="text/javascript">
var noHint = $.noop;
function placeholder(element) {
return element.clone().addClass("k-state-hover").css("opacity", 0.65);
}
function onEdit(e) {
if (e.model.isNew()) {
$('.k-window-title').text("Add");
}
}
function onSync(e) {
this.read();
}
function onError(e) {
alert(e.errors);
}
function onChange(e) {
var grid = $("#subsystems").data("kendoGrid"),
skip = grid.dataSource.skip(),
oldIndex = e.oldIndex + skip,
newIndex = e.newIndex + skip,
data = grid.dataSource.data(),
dataItem = grid.dataSource.getByUid(e.item.data("uid"));
grid.dataSource.remove(dataItem);
grid.dataSource.insert(newIndex, dataItem);
}
</script>
<style>
.k-grid tbody tr:not(.k-grouping-row) {
cursor: move;
}
</style>

Kendo Sortable widget does not work with grouped Grid.
This is written in the known limitations section of Sortable-Grid integration help topic.

Kendo says using KendoSortable is impossible on grouped grids, I did however find a possible solution! in your question you limit yourself to only being able to drag within the same group. If you have a field to store the order and sort correctly, this does not matter.
My solution posted below, was made for an angular implementation, but the onchange event is both javascript so I think this can be reworked.
(tr filter [data-uid] will ensure no grouping-row is 'selected')
first number all rows correctly, possibly needed to initiate the SortOrder value
then set the new index on the moved row
then change where needed
then resort the grid. Possibly hardcoded fieldname is needed here instead of 'primarySortField'
$scope.initMySortOrderGrouped = function(grid, myDatasource, primarySortField) {
grid.table.kendoSortable({
filter: ">tbody>tr[data-uid]",
hint: $.noop,
cursor: "move",
placeholder: function(element) {
return element.clone().addClass("k-state-hover").css("opacity", 0.65);
},
container: "#" + grid.element[0].id + " tbody",
change: function(e) {
/*Needed on grid for sorting to work conditionally*/
if (grid.canWrite) {
var skip = 0,
oldIndex = e.oldIndex + skip,
newIndex = e.newIndex + skip,
view = myDatasource.view(),
dataItem = myDatasource.getByUid(e.item.data("uid"));
/*retrieve the moved dataItem*/
/*set initial values where needed*/
var countRows = 0;
for (var i = 0; i < view.length; i++) {
for (var j = 0; j < view[i].items.length; j++) {
if (view[i].items[j].SortOrder != countRows + j) {
view[i].items[j].SortOrder = countRows + j;
view[i].items[j].dirty = true;
}
}
countRows += view[i].items.length
}
dataItem.SortOrder = newIndex; /*update the order*/
dataItem.dirty = true;
countRows = 0;
/*shift the order of the records*/
for (var i = 0; i < view.length; i++) {
for (var j = 0; j < view[i].items.length; j++) {
if (oldIndex < newIndex) {
if (countRows + j > oldIndex && countRows + j <= newIndex) {
view[i].items[j].SortOrder--;
view[i].items[j].dirty = true;
}
} else {
if (countRows + j >= newIndex && countRows + j < oldIndex) {
view[i].items[j].SortOrder++;
view[i].items[j].dirty = true;
}
}
}
countRows += view[i].items.length
}
myDatasource.sync();
/*submit the changes through the update transport and refresh the Grid*/
}
myDatasource.sort([{
field: primarySortField,
dir: "asc"
}, {
field: "SortOrder",
dir: "asc"
}]);
}
});
};
This will allow you to drag items over it's group, the number will be far higher or lower than it could be in the group, but the resulting sort will show correctly!
I hate it when programmers say something is impossible...

Related

React Select chained options based on another dropdown selected value

Using React Select Async what are the best practices to chain options.
What I mean: I have 3 dropdowns the first one is populated from default with option values and the next 2 dropdowns are disabled.
Selecting the first dropdown value should populate the second dropdown options based on it's value and so on with the next dropdown.
so what I've been trying
import React from "react";
import Select from "react-select";
import AsyncSelect from "react-select/async";
import classnames from "classnames";
import Requests from "../services/requests";
const filterOptions = (inputValue, options) => {
return options.filter(i =>
i.label.toLowerCase().includes(inputValue.toLowerCase())
);
};
class FieldsRenderer extends React.Component {
constructor(props) {
super(props);
this.state = {
fields: props.fields,
containerClass: props.containerClass,
stepSnapshot: null,
selectOptions: {}
};
this.props.fields.map( (f) => {
if(f.type === 'select' && typeof f.dependsOn !== 'undefined') {
this.state.selectOptions[f.name] = null;
}
})
}
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.fields !== prevState.fields) {
return {
fields: nextProps.fields,
containerClass: nextProps.containerClass
};
}
return null;
}
componentDidUpdate(prevProps, nextProps) {
if (prevProps !== this.props) {
this.setState({
fields: nextProps.fields,
containerClass: nextProps.containerClass
});
this.props.fields.map(f => {
if (typeof f.dependsOn != "undefined") {
this.state.selectOptions[f.name] = null;
}
});
}
}
handleInputChange = (index, e) => {
console.log(e.target.value);
console.log(index);
};
handleSelectChange = (selectedOption, item) => {
this.setState({
stepSnapshot: {
[item.name]: {
value: selectedOption.value,
label: selectedOption.label
}
}
});
let childField = this.props.fields.filter(t => {
if (t.type === "select" && typeof t.dependsOn !== "undefined") {
return t.dependsOn === item.name;
}
});
if (childField) {
this.loadChildOptions(childField[0], selectedOption);
}
};
//load child slect options
loadChildOptions(target, parentValue) {
Requests.get(
process.env.REACT_APP_API_BASE_URL +
target.source +
"/" +
parentValue.value,
(status, data) => {
//data will be set but will be shown just the previous state
this.state.selectOptions[target.name] = data;
}
);
}
render() {
let containerClass = "";
let fields = this.state.fields.map((field, i) => {
const fieldType = field.type;
let fieldStyle;
if (
typeof this.state.containerClass !== "undefined" &&
this.state.containerClass !== ""
) {
containerClass = this.state.containerClass;
}
if (typeof field.width !== "undefined" && field.width !== "") {
fieldStyle = {
width: "calc(" + field.width + " - 5px)"
};
}
switch (fieldType) {
case "select": {
const selectCustomStyles = {
control: (base, state) => ({
...base,
boxShadow: state.isFocused ? 0 : 0,
borderWidth: 2,
height: 45,
borderColor: state.isFocused ? "#707070" : base.borderColor,
"&:hover": {
borderColor: state.isFocused ? "#707070" : base.borderColor
}
}),
option: (provided, state) => ({
...provided,
backgroundColor: state.isSelected ? "#46B428" : "initial"
})
};
if (
typeof field.async !== "undefined" &&
typeof field.dependsOn === "undefined"
) {
return (
<div key={i} className={"field-wrapper"}>
<AsyncSelect
loadOptions={(inputValue, callback) => {
Requests.get(
process.env.REACT_APP_API_BASE_URL + field.source,
(status, data) => {
callback(data);
}
);
}}
styles={selectCustomStyles}
defaultOptions
name={field.name}
placeholder={field.label}
onChange={this.handleSelectChange}
/>
</div>
);
} else if(typeof field.dependsOn !== "undefined") {
return(<div key={i} className={"field-wrapper"}>
<AsyncSelect
styles={selectCustomStyles}
placeholder={field.label}
defaultOptions={this.state.selectOptions[field.name]}
loadOptions={this.state.selectOptions[field.name]}
/>
</div>)
} else {
const disabled =
typeof field.dependsOn !== "undefined" && field.dependsOn !== ""
? this.state.selectOptions[field.name] != null
? false
: true
: false;
return (
<div key={i} className={"field-wrapper"}>
<Select
styles={selectCustomStyles}
placeholder={field.label}
//isLoading={this.state.selectOptions[field.name].length ? true : false}
isDisabled={disabled}
name={field.name}
options={this.state.selectOptions[field.name]}
/>
</div>
);
}
}
case "input":
{
let suffix;
let inputAppendClass;
if (typeof field.suffix !== "undefined" && field.suffix !== "") {
inputAppendClass = "input-has-append";
suffix = <span className={"input-append"}>{field.suffix}</span>;
}
return (
<div
key={i}
className={classnames("field-wrapper input", inputAppendClass)}
style={fieldStyle}
>
<input
placeholder={field.label}
type="text"
className={"input-field"}
onChange={event => this.handleInputChange(field.name, event)}
/>
{suffix}
</div>
);
}
break;
case "checkbox":
{
containerClass = "checkbox-fields";
let radios = field.options.map((option, b) => {
return (
<div key={i + b} className={"field-wrapper checkbox-button"}>
<input
placeholder={option.label}
id={option.name + "_" + i + b}
type={"checkbox"}
className={"input-field"}
/>
<label htmlFor={option.name + "_" + i + b}>
<div className={"label-name"}>{option.label}</div>
<span className={"info-icon"}></span>
<div className={"hint"}>{option.hint}</div>
</label>
</div>
);
});
return radios;
}
break;
case "radio":
{
let radios = field.options.map((option, k) => {
return (
<div key={i + k} className={"field-wrapper radio-button"}>
<input
name={option.name}
id={option.name + "_" + i + k}
placeholder={option.label}
type={"radio"}
className={"input-field"}
/>
<label htmlFor={option.name + "_" + i + k}>
<div className={"label-name"}>{option.label}</div>
<div className={"hint"}>{option.hint}</div>
</label>
</div>
);
});
return radios;
}
break;
default:
break;
}
});
return (
<div className={classnames("fields-group", containerClass)}>{fields}</div>
);
}
}
export default FieldsRenderer;
For example I has react-select Async field. I use for manage form formik. At first you create field:
<AsyncSelect
name="first"
...
onChange={(name, value) => {
// you can write what you want but here small example what I do for other
// two fields
setFieldValue('second', null);
setFieldValue('third', null);
return setFieldValue(name, value);
}}
/>
And second field:
<AsyncSelect
name="second"
key={!!values.first && !!values.first.id ? values.first.id : null}
...
onChange={(name, value) => {
setFieldValue('third', null);
return setFieldValue(name, value);
}}
/>
There is i give key and change key value on changes first field. Because if you don't do it second field don't know when first field changes value. And if you give uniq changeable key second can load from remote data which depends from first field.
And third field:
<AsyncSelect
name="third"
key={!!values.third && !!values.third.id ? values.third.id : null}
...
onChange={setFieldValue}
/>
This is easy way to manage depended three or more fields. I think this you understand this logic.

select2 v4 dataAdapter.query not firing

I have an input to which I wish to bind a dataAdapter for a custom query as described in https://select2.org/upgrading/migrating-from-35#removed-the-requirement-of-initselection
<input name="pickup_point">
My script:
Application.prototype.init = function() {
this.alloc('$pickupLocations',this.$el.find('[name="pickup_point"]'));
var that = this;
$.fn.select2.amd.require([
'select2/data/array',
'select2/utils'
], function (ArrayData, Utils) {
var CustomData = function($element, options) {
CustomData.__super__.constructor.call(this, $element, options);
};Utils.Extend(CustomData, ArrayData);
CustomData.prototype.query = function (params, callback) {
var data = {
results: []
};
console.log("xxx");
for (var i = 1; i < 5; i++) {
var s = "";
for (var j = 0; j < i; j++) {
s = s + params.term;
}
data.results.push({
id: params.term + i,
text: s
});
}
callback(data);
};
that.$pickupLocations.select2({
minimumInputLength: 2,
language: translations[that.options.lang],
tags: [],
dataAdapter: CustomData
});
});
}
But when I type the in the select2 search box the xxx i'm logging for testing doesn't appear in my console.
How can I fix this?
I found the solution on
https://github.com/select2/select2/issues/4153#issuecomment-182258515
The problem is that I tried to initialize the select2 on an input field.
By changing the type to a select, everything works fine.
<select name="pickup_point">
</select>

How to change the background of a cell dynamically in a Kendo MVC UI Grid?

I need to change the backgroundcolor of a kendo grid.
My code looks as follows :
#(Html.Kendo().Grid<TegelCheckerModel>()
.Name("Grid")
.CellAction(cell =>
{
if (cell.Column.Title.Equals("TegelZones"))
{
if (cell.DataItem.TegelZones.Contains("Extern"))
{
cell.HtmlAttributes["style"] = "background-color: red";
}
}
})
.Columns(columns =>
{
columns.Bound(p => p.TegelNaam);
columns.Bound(p => p.TegelActief).HeaderHtmlAttributes(new { style = "background: #800000;" }).ClientTemplate("#if(TegelActief){# ja #}else{# nee #}#");
columns.Bound(p => p.TegelZones).HeaderHtmlAttributes(new { style = "background: #800000;" }); })
.AutoBind(true)
.Pageable()
.Sortable()
.Filterable()
.DataSource(dataSource => dataSource
.Ajax() //Or .Server()
.Read(read => read.Action("GetTegels", "TegelChecker")
.Data("getAlvNummerAndVoorWie"))
)
)
This code doesn't change the background-color of the cell.
What am I missing?
I succeeded partly. This means I was able to color some part of the cell using the following code :
if (tegelTitel == "TegelActie") {
if ($("#tegelcheckeroverzicht_klantControl_Klantddl").klantenControl().getKlant().alvNummer == "") {
html = kendo.format("<span>" + data.TegelActie + "</span>");
}
else {
if (data.TegelActieAllowed) {
if (data.TegelActie == null) {
html = kendo.format("<span></span>");
}
else {
html = kendo.format("<span>" + data.TegelActie + "</span>");
}
}
else {
html = kendo.format("<span STYLE='background: red; color: white;font-weight:bold'>" + data.TegelActie + "</span>");
}
}
}
But that means that only the background of the span is colored and not the entire cell.
Ok I found a solution :
function gridDataBound() {
var data = this._data;
for (var x = 0; x < data.length; x++) {
var dataItem = data[x];
var tr = $("#Grid").find("[data-uid='" + dataItem.uid + "']");
if (dataItem.Selected) {
$("input:first", tr).attr("checked", "checked");
}
var alvNummer = $("#tegelcheckeroverzicht_klantControl_Klantddl").klantenControl().getKlant().alvNummer;
if (alvNummer == "")
{ }
else
{
var cell = $("td:nth-child(6)", tr); // Zone
if (dataItem.HasCorrectZone) {
}
else {
cell.addClass("myerror");
}
cell = $("td:nth-child(7)", tr); //Actie
if (!dataItem.TegelActieAllowed) {
cell.addClass("myerror");
}
cell = $("td:nth-child(8)", tr); //Rollen
if (!dataItem.RolcodeVoldaan) {
cell.addClass("myerror");
}
}
}
}
And in the grid I added :
.Events(e => e.DataBound("gridDataBound "))

Kendo UI Grid - Show row number

How do I show the row number in a Kendo UI Grid? The code I have is not working. The page displays the column but it's empty.
#{int counter = 1;}
#(Html.Kendo().Grid<QueueViewModel>()
.Name("Queue")
.Columns(columns =>
{
columns.Template(#<text><span>#counter #{ counter++; }</span></text>).Title("#");
})
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(10)
.Read(read => read.Action("GetOpenQueue", "DataSource", new { GeneralQueue = true })
))
Do this:
#{
int counter = 1;
}
#(Html.Kendo().Grid<QueueViewModel>()
.Name("Queue")
.Columns(columns =>
{
columns.Template(#<text><span>#(counter++)</span></text>).Title("#");
})
Or, if your DataSource is set to Ajax (client-side), do this:
<script>
var counter = 1;
function onDataBound(e) {
counter = 1;
}
function renderNumber(data) {
return counter++;
}
</script>
#(Html.Kendo().Grid()
.Name("Queue")
.Columns(columns => {
columns.Template(t => { }).ClientTemplate("#= renderNumber(data) #").Title("#");
})
.Events(ev => ev.DataBound("onDataBound"))
)
Column ClientTemplate is client-side functionality. You cannot use server-side variables in it. You should define Javascript variable:
<script>
var i = 1;
</script>
Then, inside the grid use this:
columns.Template(t => { }).ClientTemplate(#=i++#).Title("#");
Updated: it should be ClientTemplate instead of Template
Try this way In javascript, the code will support the paging also
<script type="text/javascript">
var CountIt = 0
function GetCountIt() {
var page = $("#YourGrid").data("kendoGrid").dataSource.page();
var pageSize = $("#YourGrid").data("kendoGrid").dataSource.pageSize();
CountIt++;
return (page * pageSize) - pageSize + CountIt
}
function YourGrid_DataBound() {
CountIt = 0; $('#YourGrid').data('kendoGrid').pager.unbind("change").bind('change', function (e) {
CountIt = 0
})
}
</script>
then add to your kindo grid
.Events(events =>
{
events.DataBound("YourGrid_DataBound");
})
.Columns(columns =>
{
columns.Bound("").ClientTemplate("#=GetCountIt()#").Title("Sr.").Width(50);
...

Columns in Kendo Grid MVC Multiline

I have kendo grid MVC like this:
#(Html.NFSGrid<dynamic>("PortfolioGrid")
.Name("PortfolioGrid")
.EnableCustomBinding(true)
//.Selectable()
.BindTo(Model)
.DataSource(dataSource => dataSource
.Ajax()
.PageSize(countpaging)
.Model(m =>
{
foreach (var Allcoulms in (List<HtmlHelperGridBuilder.GridCol>)ViewData["ViewDataGridfildes"])
{
if (Allcoulms.ColumnName == "Id")
{
m.Id(Allcoulms.ColumnName);
}
else
{
m.Field(Allcoulms.ColumnName, Type.GetType("System.String")).Editable(true);
}
}
})
.ServerOperation(true)
.Read(read => read.Action("Read", "Portfolio").Data("portFolioNameSpace.additionalInfo")
)
)
.HtmlAttributes(new { style = "width:2000;" })
.Columns(columns =>
{
columns.Template(p => { }).ClientTemplate("<input name='selectedIds' type='checkbox' value=\"#=Id#\" class='check_row' onchange='portFolioNameSpace.changeChk(event,this.checked,this);'/>")
.HeaderTemplate("<div style='background=#C7CA21 ;width= 40%'><input type='checkbox' style='outline: 2px solid #cfbe62' class='selectAll' onclick='portFolioNameSpace.buttonclick(event)'/></div>")
.HeaderHtmlAttributes(new { style = "text-align:center;" })
.Width(30);
columns.Template(#<text></text>).Title(T("روند").ToString()).Width(30).ClientTemplate("<a onclick='portFolioNameSpace.onclickFlowFPortfolio(event)'><i class='iconmain-showall'></i></a>");
columns.Template(#<text></text>).Title(T("اصل سند").ToString()).Width(50).ClientTemplate("<a onclick='portFolioNameSpace.GetFormData(event)'><i class='iconmain-Accepted'></i></a>");
foreach (var Allcoulms in (List<HtmlHelperGridBuilder.GridCol>)ViewData["ViewDataGridfildes"])
{
if (Allcoulms.ColumnName == "Id")
{
columns.Bound(Allcoulms.ColumnName).Visible(false);
}
else if (Allcoulms.ColumnName == "Subject")
{
columns.Bound(Allcoulms.ColumnName).Width(Allcoulms.ColumnWidth).Title(T(Allcoulms.ColumnTitle).ToString()).HtmlAttributes(new { style = "text-align:center;" });
}
else if (Allcoulms.ColumnName == "Comment")
{
columns.Bound(Allcoulms.ColumnName).Width(200).Title(T(Allcoulms.ColumnTitle).ToString()).HtmlAttributes(new { style = "text-align:center;" }).ClientTemplate("<input type=\"text\" id=\"#=Id#\" value=\"#=Comment#\"/>");
}
else if (Allcoulms.ColumnName == "notViewdRows")
{
}
else
{
columns.Bound(Allcoulms.ColumnName).Width(Allcoulms.ColumnWidth).Title(T(Allcoulms.ColumnTitle).ToString()).HtmlAttributes(new { style = "text-align:center;" }).HeaderHtmlAttributes(new { style = "text-align:center;" });
}
}
})
.Pageable(pager => pager.Enabled(true))
.Scrollable()
.Filterable()
.Resizable(resize => resize.Columns((true)))
.Reorderable(reorder => reorder.Columns(true))
.Events(e => e
.DataBound("portFolioNameSpace.gridDataBound")
)
)
so the problem is when the lengh of a coulmn is more than what i set in width it makes 2line like this picture so how can i make it 1ine without set specific width?
Add the CSS attributes overflow: hidden; white-space: nowrap; to the column definition, something like this:
columns.Bound(Allcoulms.ColumnName).Width(Allcoulms.ColumnWidth).Title(T(Allcoulms.ColumnTitle).ToString()).HtmlAttributes(new { style = "text-align:center; overflow: hidden; white-space: nowrap;" }).HeaderHtmlAttributes(new { style = "text-align:center;" });
I tested and it works, see if it works for you too.
EDIT
Since you're using column templates, you also have the option of adding the CSS properties directly in your CSS file or even inline (although the latter isn't a good practice)

Resources