TreeList with inline edit and drag&drop

13 posts, 0 answers
  1. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 22 Jul 2018 Link to this post

    I have a question about TreeList wrapper component. I need exactly this component.

    https://www.telerik.com/kendo-react-ui/wrappers/treelist/editing/ 

    But is it possible to do drag&drop there? I need to move objects only in their own array, I dont need them to move to their parent / child.

    Another thing is that I want to active inline edit just by click on some cell, as you can see here: https://www.telerik.com/kendo-react-ui/components/grid/editing/editing-in-cell/.

     

    If those two requests arent possible in this TreeList component, is it somehow possible to manage that via react grid component since it supports inline edit and drag&drop, but problem for me is, that each detail grid creates his own table which I dont need.

     

  2. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 23 Jul 2018 Link to this post

    Hello, Martin,

    Regarding the requirements:

    1) The TreeList currently only supports drag and drop between parents/child rows as the main point for the drag and drop feature is to re-arrange hierarchical elements. In the Grid, this is achieved using the Sortable widget, but unfortunately, this widget is currently not available for the React suite.

    I can suggest commenting and voting for a similar feature request as this will increase its chances to be implemented sooner:

    http://kendoui-feedback.telerik.com/forums/127393-kendo-ui-feedback/suggestions/9269412-make-drag-drop-behaviour-on-treelist-same-as-on-tr

    2) The incell editing is planned for the next official release(expected around September).

    Let me know if you need additional details on this matter.

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  3. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 23 Jul 2018 in reply to Stefan Link to this post

    Thanks for this information, 

    anyway I have one more question. Is it possible to send my custom DOM into column ( in this case, its named field ). Is it possible to call "test" function, which returns JSX.Element, which has to be render in specific column? I know that function is being called, but the function result never appears in column. Here is my code below

    export default class TreeListContainer extends React.Component {
        dataSource: kendo.data.TreeListDataSource;
        columns: {
            field?: string;
            title?: string;
            command?: Array<any>;
        }[];
        data: Array<IDummyData>;
        constructor(props) {
            super(props);
            this.columns = [
                { field: "id" },
                { field: "name" },
                { field: "description" },
                { title: "isDefault" },
                { title: "Edits", command: ["edit"] }
            ]
            this.dataSource = new kendo.data.TreeListDataSource({
                change: this.onChange,
                data: dataTree,
                schema: {
                    model: {
                        id: "id",
                        expanded: true,
                        fields: {
                            id: { type: "string", editable: false, nullable: false },
                            name: { type: "string", editable: true, nullable: false },
                            description: { type: "string", editable: true, nullable: false },
                            isVisible: { type: "boolean", editable: true, nullable: false },
                            isDefault: this.test(),
                        },
                    }
                }
            });
        }
     
        private test = () => {
            console.log("test render");
            return <div>custom dom contains radio / checkbox</div>
        }
     
        private editCell = (event) => {
            console.log("editcell", event);
        }
     
        private onChange = (event) => {
            console.log("onChange",  event)
        }
     
        private drag = (event) => {
            console.log("drag",  event)
        }
     
        private dragStart = (event) => {
            console.log("drag",  event)
        }
     
        private dragEnd = (event) => {
            console.log("drag",  event)
        }
     
        render() {
            return (
                <div>
                    <TreeList height={600}
                        edit={this.editCell}
                        dragstart={this.dragStart}
                        dragend={this.dragEnd}
                        drag={this.drag}
                        editable={true}
                        columns={this.columns}
                        dataSource={this.dataSource}
                    />
                </div>
            );
        }
    }

     

  4. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 23 Jul 2018 in reply to Martin Link to this post

    So I found I can show checkbox in column like this. How I can bind this to react function then? 
    { title: "isDefault", template: "<input type='checkbox' onChange={this.test} data-bind='checked: checked' />"},
  5. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 24 Jul 2018 Link to this post

    Hello, Martin,

    Binding to React functions will require using jQuery as well. The main reason is that the wrapper is based on jQuery and the templates are not aware of React specific syntax as onChange, className etc.

    I can suggest attaching event listeners to the checkbox on the TreeList dataBound event:

    https://docs.telerik.com/kendo-ui/api/javascript/ui/treelist/events/databound

    I made an example demonstrating this:

    https://next.plnkr.co/edit/iP78i7PL4WyPWpcR

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  6. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 25 Jul 2018 in reply to Stefan Link to this post

    I managed to do that with different way, but it has serious performance issue, in my case i bind action events like below in my code. I tested it also with what you send in your previous post, but it was identical result. I have around 200 rows in tree list, every time I click on checkbox or radio, doesnt matter, it has like 0.3s delay because of event handlers binding. I had to put TreeListDataSource into render method, because when I load data from my api, when I had it in constructor, treelist component never showed any of them, it just message me there is nothing to display.

    But here is the issue, whenever my state is updated (click on checkbox etc..) it fires render method which creates new TreeListDataSource  object and all bindings has to be done again to my controls and this cause serious performance issue. Is there something I can do to avoid it?

     

    import React from 'react';
    import ReactDOM from 'react-dom';
    import $ from 'jquery';
    import '@progress/kendo-ui';
    import { RouteComponentProps, Link, Route } from "react-router-dom";
    import * as Routes from "routes";
    import { TreeList, TreeListColumn } from '@progress/kendo-treelist-react-wrapper';
    import ReactDOMServer from 'react-dom/server';
    import { Application } from "core";
    import { baseUrl } from 'domain-task';
    import { Button } from "@progress/kendo-react-buttons";
    import { Resource } from "resource";
     
     
    interface IDummyData {
        id: string;
        name: string;
        position?: number;
        parentId: string;
        isVisible?: boolean;
        isDefault?: boolean;
        collectionName: string;
    }
     
    interface IState {
        data: Array<any>;
        filter: Filter;
    }
     
    export default class TreeListContainer extends React.Component<{}, IState> {
        checkBoxArray: Array<string> = [];
        radioBoxArray: Array<string> = [];
        inputNameArray: Array<string> = [];
        dataSource = new kendo.data.TreeListDataSource();
        columns: {
            field?: string;
            title?: string;
            command?: Array<any>;
            render?: any;
            template?: any;
            onClick?: any;
        }[];
     
        constructor(props) {
            super(props);
            this.state = {
                data: [],
                filter: new Filter()       
            }
            this.columns = [
                { field: "name", template: this.renderInput },
                { title: "Is visible", template: this.renderVisibilityCheckBox },
                { title: "is default", template: this.renderIsDefaultRadioButton },
                { title: "Details", template: this.renderDetailButton }
            ]
            this.dataSource = new kendo.data.TreeListDataSource({
                change: this.onChange,
                data: this.state.data,
                schema: {
                    model: {
                        id: "id",
                        expanded: true,
                        fields: {
                            id: { type: "string", editable: false, nullable: false },
                            name: { type: "string", editable: true, nullable: false },
                            isDefault: {
                                type: "boolean", editable: true, nullable: false
                            }
                        },
                    }
                }
            });
        }
     
        private convertDataFromApi = (array: Array<any>) => {
            let testData: Array<IDummyData> = [];
     
            function recursiveDataBuilder(array: Array<any>) {
                for (var i = 0; i < array.length; i++) {
     
                    const dummy: IDummyData = {
                        id: array[i].key.id,
                        description: "",
                        isVisible: array[i].isVisible ? array[i].isVisible : false,
                        isDefault: array[i].isDefault ? array[i].isVisible : false,
                        name: array[i].name.translations[0].value,
                        parentId: array[i].parentKey ? array[i].parentKey.id : null,
                        position: 0,
                        collectionName: array[i].key.collectionName
                    }
                    testData.push(dummy);
     
                    if (array[i].nodes instanceof Array && array[i].nodes && array[i].nodes.length > 0) {
                        recursiveDataBuilder(array[i].nodes)
                    }
                }
            }
            recursiveDataBuilder(array);
            return testData;
        }
     
        private getData = () => {
            Application.modelTreeClient.readModelTree(this.state.filter)
                .then((data) => {
                    this.setState(prevState => ({
                        ...prevState,
                        data: this.convertDataFromApi(data)
                    }))
                }).catch(() => alert("failed to get data"));
        }
     
        componentWillMount() {
            this.getData();
        }
     
        componentDidMount() {
            this.initializeEventListeners();
        }
     
            // here i create binding via eventListeners
        private initializeEventListeners = () => {
            //for each checkbox add listener for events
            this.checkBoxArray.map((checkboxId: string) => {
                const element = document.getElementById(checkboxId);
                if (element) {
                    element.addEventListener('change', this.visibilityCheckEvent);
                }
            });
     
            //for each radio add listener for events
            this.radioBoxArray.map((radioId: string) => {
                const element = document.getElementById(radioId);
                if (element) {
                    element.addEventListener('change', this.isDefaultCheckEvent);
                }
            });
     
            //for each input add listener for events
            this.inputNameArray.map((id: string) => {
                const element = document.getElementById(id);
                if (element) {
                    element.addEventListener('click', this.isNameChangeEvent);
                    element.addEventListener('blur', this.saveInputChange);
                }
            });
        }
     
        componentDidUpdate() {
            const arrayOfDetailButtons = document.getElementsByTagName("button");
            for (var i = 0; i < arrayOfDetailButtons.length; i++) {
                arrayOfDetailButtons[i].addEventListener('click', this.goToDetail);
            }
            this.initializeEventListeners();
        }
     
        private renderInput = (item) => {
            const input = this.addNameInputIdToArray(item);
     
            const box = <input id={input}
                onClick={this.isNameChangeEvent}
                data-id={item.id}
                readOnly={true}
                contentEditable={false}
                value={item.name}
                className="k-treelist-input"
                onBlur={this.saveInputChange}
            />
            return ReactDOMServer.renderToString(box);
        }
     
        private renderDetailButton = (item) => {
            const button = <Button className="k-primary goto-Detail"
                onClick={this.goToDetail}
                data-contextid={item.id}
                data-parentid={item.parentId}
                data-contextname={item.collectionName}>
                EDIT
                    </Button>
            return ReactDOMServer.renderToString(button);
        }
     
        private renderVisibilityCheckBox = (item) => {
            const checkboxID = this.addVisibilityIdToArray(item);
     
            const box = <label className="sa-radio-option-wrapper">
                <input type="checkbox" id={checkboxID}
                    className="k-checkbox"
                    onChange={this.visibilityCheckEvent}
                    data-id={item.id}
                    checked={item.isVisible} />
                <label className="k-checkbox-label"
                    htmlFor={checkboxID}></label>
            </label>
            const dom = ReactDOMServer.renderToString(box);
            return dom;
        }
     
        private renderIsDefaultRadioButton = (item) => {
            const defaultId = this.addDefaultIdToArray(item);
            const box = <label className="sa-radio-option-wrapper">
                <input type="radio"
                    id={defaultId}
                    name={item.collectionName + item.parentId}
                    onChange={this.isDefaultCheckEvent}
                    data-id={item.id}
                    checked={item.isDefault}
                    className="k-radio" />
                <label className="k-radio-label" htmlFor={defaultId}></label>
            </label>
            return ReactDOMServer.renderToString(box)
        }
     
            // here i just hardcoded some array update to check if rest of the logic does not affect performance
        private visibilityCheckEvent = (event) => {
            const test = [...this.state.data];
            test[0].isVisible = false;
     
            this.setState(prevState => ({
                ...prevState,
                data: test
            }));
            console.log("updated");
        }
     
        /**
    * add element id of every checkbox from treelist into array for later usage
    */
        private addVisibilityIdToArray = (item: any): string => {
            const checkboxId = `visibility_${item.id}`;
            this.checkBoxArray.push(checkboxId);
            return checkboxId;
        }
     
        private addNameInputIdToArray = (item: any): string => {
            const checkboxId = `name_${item.id}`;
            this.inputNameArray.push(checkboxId);
            return checkboxId;
        }
        /**
         * add element id of every radio button from treelist into array for later usage
         */
        private addDefaultIdToArray = (item: any): string => {
            const defaultId = `default_${item.id}`;
            this.radioBoxArray.push(defaultId);
            return defaultId;
        }
     
        render() {
            const dataSource = new kendo.data.TreeListDataSource({
                change: this.onChange,
                data: this.state.data,
                schema: {
                    model: {
                        id: "id",
                        expanded: true,
                        fields: {
                            id: { type: "string", editable: false, nullable: false },
                            name: { type: "string", editable: true, nullable: false },
                            description: { type: "string", editable: true, nullable: false },
                            isDefault: {
                                type: "boolean", editable: true, nullable: false
                            }
                        },
                    }
                }
            });
            return (
                <div>
                    <h2>{Resource.TitleModelTree}</h2>
                    <TreeList height={600}
                        edit={this.editCell}
                        dragstart={this.dragStart}
                        dragend={this.dragEnd}
                        drag={this.drag}
                        columns={this.columns}
                        editable={true}
                        dataSource={dataSource}
                    />
                </div>
            );
        }
    }
  7. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 26 Jul 2018 Link to this post

    Hello, Martin,

    Thank you for the provided details and code.

    I modified the previous example and added 1000 rows, but the clicking of the checkbox is instant as expected:

    https://next.plnkr.co/edit/iP78i7PL4WyPWpcR

    If possible, please provide us an example reproducing the issue and we will be happy investigate it locally the make suggestion on how it can be optimized.

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  8. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 26 Jul 2018 in reply to Stefan Link to this post

    Hi,

    I checked your sample, there is one big difference, in your sample, checkbox does not affect state of component, but in my sample, it affects, so whenever I click on checkbox, my state is updated and it force component to re-render and this is why it slows so much, so re-render is big issue and cost a lot of time. 

    Best regards.

    Martin. 

  9. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 27 Jul 2018 Link to this post

    Hello, Martin,

    Thank you for the advice.

    I added the logic to change the state on the change events as well, and the performance was not changed noticeably:

    https://next.plnkr.co/edit/XDSxMmp6q1amEmyD

    As the issue could be caused by an unnoticed part of the code, if an example is provided we will be able to test different parts of the code and locate it.

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  10. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 27 Jul 2018 in reply to Stefan Link to this post

    I found an issue in my case, there was a problem that in my render function of class I all the time initialized dataSource object which is required for treelist component, because when I initialized it in constructor, it never had data, which come from api as asynchronous request. I solved it with initializing dataSource object in class constructor and passing data to it via data() function in render method.

    So something like this

    this.state.dataSource.data(this.state.data);

     

    After I solved this, I found one thing, which currently is problem for us. Whenever I click on some checkbox etc, it changes state and it force component to re-render and there is problem with expanded option property, lets say, I go deeper in tree, then I click on some checkbox and it force render function and whole tree is collapsed again and vice versa. Can I somehow specify, what should collapse and what not?

    Best regards.

    Martin

     

  11. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 30 Jul 2018 Link to this post

    Hello, Martin,

    I'm happy to hear that the initial issue when updating is resolved.

    As for the collapsed state. In general, setting the state should not change the collapsed and expanded state. I made a video demonstrating this:

    https://www.screencast.com/t/u90SMoQC60g6

    Still, if due to another action in the application the expanded collapsed state is lost a custom logic can be used to store the state in a variable or local storage.

    More details can be found in the following forum. The post is for the jQuery widgets, but the wrapper is based on the jQuery widget and the implementation is the same for the React wrapper:

    https://www.telerik.com/forums/restoring-treelist-expanded-state-after-refresh

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
  12. Martin
    Martin avatar
    11 posts
    Member since:
    May 2018

    Posted 16 Aug 2018 in reply to Stefan Link to this post

    Hi,

    sorry for late respond, I havent been here for a while. We have implemented our custom logic for it, since our component has complex logic

  13. Stefan
    Admin
    Stefan avatar
    2909 posts

    Posted 16 Aug 2018 Link to this post

    Hello, Martin,

    It is good to hear that the custom implementation is successfully able to achieve the desired result.

    If you have any further questions regarding the TreeView or any other of the React components we are here to assist.

    Regards,
    Stefan
    Progress Telerik
    Do you want to have your say when we set our development plans? Do you want to know when a feature you care about is added or when a bug fixed? Explore the Telerik Feedback Portal and vote to affect the priority of the items
Back to Top