lukasbach / react-complex-tree

Unopinionated Accessible Tree Component with Multi-Select and Drag-And-Drop
https://rct.lukasbach.com
MIT License
944 stars 74 forks source link

need export EventEmitter for customised data provider #255

Closed tsingson closed 1 year ago

tsingson commented 1 year ago

example data provider:


import {
   EventEmitter  // ----------------------- this need export

  ExplicitDataSource,
  TreeDataProvider,
  TreeItem,
  TreeItemIndex,
} from "react-complex-tree";
//  this is static demo data
import {itemsData} from "../data";

export interface DataX {
  recordIdStr: string;
  itemTitle: string;
  itemIdStr?: string;
  categoryId: string;
  type: string;
  sequence: number;
}

export interface ItemX extends TreeItem<DataX> {
  index: TreeItemIndex;
  children?: Array<TreeItemIndex>;
  isFolder?: boolean;
  canMove?: boolean;
  canRename?: boolean;
  data: DataX;
}

export interface ExplicitDataSourceX extends ExplicitDataSource {
  items: Record<TreeItemIndex, TreeItem<DataX>>;
}

export class DataProvider<DataX> implements TreeDataProvider {

  private data: ExplicitDataSourceX;    // this for demo only,  replace with local cache in product
  private onDidChangeTreeDataEmitter = new EventEmitter<TreeItemIndex[]>();

  constructor() {
    // this is demo only, replace with remote API or indexedDB 
    this.data = {items: itemsData};
  }

  public async getTreeItem(itemId: TreeItemIndex): Promise<TreeItem> {
    // get one item from local database like indexedDB
    // or from server

    return this.data.items[itemId];
  }

  public async getTreeItems(itemIds: TreeItemIndex[]): Promise<TreeItem[]> {
    // get   item list from local database like indexedDB
    // or from server
    //    api like this:    GET http://xxxxx:8080/gettreeitembyid/(itemId)

    return itemIds.map((itemId) => this.data.items[itemId]);
  }

  // reorder children item and store to server via API
  public async onChangeItemChildren(
      itemId: TreeItemIndex, newChildren: TreeItemIndex[]): Promise<void> {

    let i = 1;
    if (newChildren.length > 0) {

      newChildren.forEach((childId) => {

        this.data.items[childId].data.sequence = i;
        if (typeof itemId === "string") {
          this.data.items[childId].data.categoryId = itemId;
        }
        // log.debug("-- item -- ", this.data.items[childId].data);
        i++;
      });
    }
    this.data.items[itemId].children = newChildren;
    this.data.items[itemId].isFolder = true;

    // store into local database like indexedDB
    // or into server

    await this.onDidChangeTreeDataEmitter.emit([itemId]);
  }

  public async onRenameItem(
      item: TreeItem<any>, newName: string): Promise<void> {

    if ((item.canRename) && (newName.length > 0)) {
      item.data.itemTitle = newName;
      this.data.items[item.index] = item;
      // then , store into local database like indexedDB
      // or into server
    }
  }
}

example static data for demo only:

import {ItemX} from "./store/DataProvider";

export const itemsData: Record<string, ItemX> = {
  "category": {
    index: "category",
    children: ["2222", "3333", "4444"],
    isFolder: true,
    data: {
      recordIdStr: "1111",
      itemTitle: "1111",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "2222": {
    index: "2222",
    isFolder: true,
    children: ["4411", "4455"],
    data: {
      recordIdStr: "1111",
      itemTitle: "2222",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "3333": {
    index: "3333",
    isFolder: false,
    data: {
      recordIdStr: "1111",
      itemTitle: "3333",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "4444": {
    index: "4444",
    isFolder: false,
    data: {
      recordIdStr: "1111",
      itemTitle: "4444",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "4411": {
    index: "4411",
    isFolder: false,
    data: {
      recordIdStr: "1111",
      itemTitle: "4411",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "4455": {
    index: "4455",
    isFolder: false,
    data: {
      recordIdStr: "1111",
      itemTitle: "4455",
      categoryId: "0",
      type: "category",
      sequence: 0,
    },
  },
  "channel": {
    index: "channel",
    children: ["item6666", "item7777", "item8888"],
    isFolder: true,
    data: {
      recordIdStr: "channel",
      itemTitle: "channel",
      categoryId: "0",
      type: "channel",
      sequence: 0,
    },
  },
  item6666: {
    index: "item6666",
    isFolder: false,
    data: {
      recordIdStr: "item6666",
      itemTitle: "item6666",
      categoryId: "0",
      type: "channel",
      sequence: 0,
    },
  },
  item7777: {
    index: "item7777",
    isFolder: true,
    children: ["item9999"],
    data: {
      recordIdStr: "item7777",
      itemTitle: "item7777",
      categoryId: "0",
      type: "channel",
      sequence: 0,
    },
  },
  item8888: {
    index: "item8888",
    isFolder: false,
    data: {
      recordIdStr: "item8888",
      itemTitle: "item8888",
      categoryId: "0",
      type: "channel",
      sequence: 0,
    },
  },
  item9999: {
    index: "item9999",
    isFolder: false,
    data: {
      recordIdStr: "item9999",
      itemTitle: "item9999",
      categoryId: "0",
      type: "channel",
      sequence: 0,
    },
  },
};

example 2 tree via drag and drop

 import { Tree, UncontrolledTreeEnvironment } from "react-complex-tree";
import "react-complex-tree/src/style-modern.css";
import {DataProvider, DataX} from "./store/DataProvider";
import React from "react";
import { TreeItem } from "react-complex-tree/src/types";

import log from "../../store/log";
import {itemsData} from "./data";

export const CategoryChannelTreeArrange = () => {
  return (
    <>
      <style>{`
        :root {
          --rct-color-tree-bg: #F6F8FA;
          --rct-color-tree-focus-outline: #d60303;
          --rct-color-focustree-item-selected-bg: #e2d3d3;
          --rct-color-focustree-item-focused-border: #d60303;
          --rct-color-focustree-item-draggingover-bg: #ecdede;
          --rct-color-focustree-item-draggingover-color: inherit;
          --rct-color-search-highlight-bg: #7821e2;
          --rct-color-drag-between-line-bg: #cf03d6;
          --rct-color-arrow: #b48689;
          --rct-item-height: 30px;
        }
      `}</style>
      <UncontrolledTreeEnvironment<DataX>
        canDragAndDrop={true}
        canReorderItems={true}
        canDropOnFolder={true}
        canDropOnNonFolder={true}
        canDropAt={(items, target) => {
          if (target.targetType === 'between-items') {
            log.debug("------ parent item", target.parentItem);
            let parentItemIndex = target.parentItem as string;
            let itemType = getItemType(parentItemIndex);
             return  itemType === "category";

          }
          if (target.targetType === 'item') {
            log.debug("------ parent item", target.targetItem);
            let targetItemIndex = target.targetItem as string;
            let itemType = getItemType(targetItemIndex);
            return itemType === "category";
          }
          return false;
        }
        }
        canDrag={(items) => items.map((item) => item.data.type == "channel").reduce((a, b) => a && b, true)}
        dataProvider={new DataProvider()}
        getItemTitle={(item: TreeItem<DataX>) => {
          return item.data.type + " :: " + item.data.itemTitle;
        }}
        viewState={{}}
      >
        <div
          style={{
            display: "flex",
            width: "100%",
            justifyContent: "space-between",
          }}
        >
          <div
            style={{
              width: "48%",
              backgroundColor: "#dddddd",
              color: "#000",
              borderRadius: "12px",
              padding: "16px",
              margin: "15px",
            }}
          >
            <h3> category tree arrange</h3>
            <Tree treeId="tree" rootItem="category" treeLabel="Tree" />
          </div>
          <div
            style={{
              width: "48%",
              backgroundColor: "#dfdfdf",
              color: "#000",
              borderRadius: "12px",
              padding: "16px",
              margin: "15px",
            }}
          >
            <h3> channel list </h3>
            <Tree treeId="list" rootItem="channel" treeLabel="item list" />
          </div>
        </div>
      </UncontrolledTreeEnvironment>
    </>
  );
};

// TODO: refactor this to use the data from the local cached store
function getItemType( index: string) {
  return itemsData[index].data.type;
}

react-complex-tree-demo

natetewelde commented 1 year ago

Can confirm. After trying what you said by using the StaticTreeDataProvider as reference, the event emitter is not exported.

lukasbach commented 1 year ago

The event emitter class is a very thin and simple sample implementation of any event emitter, there are plenty of libraries out there that provide something like that that you can use, or you can use your own implementation, you don't really need more than just an array or a record to get the same behaviour.

I would rather not export the existing event emitter in RCT, since it really doesn't have anything to do with the library, and would just expose innards from how RCT works. I saw similar issues with other libraries, where they exported some random internal utility for convenience, and when they internally stopped using it, they weren't able to get rid of it because what would normaly be just an internal refactor suddenly became a major breaking change since many people started using an exported symbol that didn't have anything to do with the library. I hope you understand my reasoning.

You could use mini-signals, or use a simple custom implementation like this record:

export class StaticTreeDataProvider<T = any> implements TreeDataProvider {
  private data: ExplicitDataSource;

  private handlers: Record<string, (changedItemIds: TreeItemIndex[]) => void> = {};

  private setItemName?: (item: TreeItem<T>, newName: string) => TreeItem<T>;

  constructor(
    items: Record<TreeItemIndex, TreeItem<T>>,
    setItemName?: (item: TreeItem<T>, newName: string) => TreeItem<T>
  ) {
    this.data = { items };
    this.setItemName = setItemName;
  }

  public async getTreeItem(itemId: TreeItemIndex): Promise<TreeItem> {
    return this.data.items[itemId];
  }

  public async onChangeItemChildren(itemId: TreeItemIndex, newChildren: TreeItemIndex[]): Promise<void> {
    this.data.items[itemId].children = newChildren;
    Object.values(this.handlers).forEach((handler) => handler([itemId]));
  }

  public onDidChangeTreeData(listener: (changedItemIds: TreeItemIndex[]) => void): Disposable {
    const id = (Math.random() + 1).toString(36).substring(7);
    this.handlers[id] = listener;
    return { dispose: () => delete this.handlers[id] };
  }

  public async onRenameItem(item: TreeItem<any>, name: string): Promise<void> {
    if (this.setItemName) {
      this.data.items[item.index] = this.setItemName(item, name);
    }
  }
}
tsingson commented 1 year ago

事件发射器类是任何事件发射器的一个非常精简和简单的示例实现,有很多库提供类似的东西供您使用,或者您可以使用自己的实现,您实际上只需要只是一个数组或一个记录来获得相同的行为。

我宁愿不在 RCT 中导出现有的事件发射器,因为它实际上与库没有任何关系,只会暴露 RCT 工作原理的内部结构。我在其他库中看到了类似的问题,为了方便,他们导出了一些随机的内部实用程序,当他们在内部停止使用它时,他们无法摆脱它,因为通常只是一个内部重构突然变成了一个重大问题更改,因为许多人开始使用与库没有任何关系的导出符号。我希望你明白我的推理。

您可以使用mini-signals,或使用像这样的记录的简单自定义实现:

export class StaticTreeDataProvider<T = any> implements TreeDataProvider {
  private data: ExplicitDataSource;

  private handlers: Record<string, (changedItemIds: TreeItemIndex[]) => void> = {};

  private setItemName?: (item: TreeItem<T>, newName: string) => TreeItem<T>;

  constructor(
    items: Record<TreeItemIndex, TreeItem<T>>,
    setItemName?: (item: TreeItem<T>, newName: string) => TreeItem<T>
  ) {
    this.data = { items };
    this.setItemName = setItemName;
  }

  public async getTreeItem(itemId: TreeItemIndex): Promise<TreeItem> {
    return this.data.items[itemId];
  }

  public async onChangeItemChildren(itemId: TreeItemIndex, newChildren: TreeItemIndex[]): Promise<void> {
    this.data.items[itemId].children = newChildren;
    Object.values(this.handlers).forEach((handler) => handler([itemId]));
  }

  public onDidChangeTreeData(listener: (changedItemIds: TreeItemIndex[]) => void): Disposable {
    const id = (Math.random() + 1).toString(36).substring(7);
    this.handlers[id] = listener;
    return { dispose: () => delete this.handlers[id] };
  }

  public async onRenameItem(item: TreeItem<any>, name: string): Promise<void> {
    if (this.setItemName) {
      this.data.items[item.index] = this.setItemName(item, name);
    }
  }
}

agree. thanks for reply.

lukasbach commented 7 months ago

FYI, I expanded the docs on static data providers and on custom data providers, those are now the official points in the docs that provide details on that manner, alongside a sample implementation for custom data providers.