npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@itwin/presentation-hierarchies-react

v0.7.1

Published

React components based on `@itwin/presentation-hierarchies`

Downloads

524

Readme

@itwin/presentation-hierarchies-react

Copyright © Bentley Systems, Incorporated. All rights reserved. See LICENSE.md for license terms and full copyright notice.

The @itwin/presentation-hierarchies-react package provides APIs for building a headless UI for rendering tree components based on data in an iTwin.js iModel. In addition, it delivers a set of iTwinUI-based components for rendering the tree.

Headless UI

useTree

This is a React hook that creates state for a tree component.

It takes 2 required properties:

  • imodelAccess provides access to iModel's data and metadata, required to build the hierarchy. Generally, @itwin/presentation-core-interop and @itwin/presentation-shared packages are used to create this object:

    import { IModelConnection } from "@itwin/core-frontend";
    import { SchemaContext } from "@itwin/ecschema-metadata";
    import { ECSchemaRpcLocater } from "@itwin/ecschema-rpcinterface-common";
    import { createECSchemaProvider, createECSqlQueryExecutor } from "@itwin/presentation-core-interop";
    import { createLimitingECSqlQueryExecutor, createNodesQueryClauseFactory, HierarchyDefinition } from "@itwin/presentation-hierarchies";
    
    // Not really part of the package, but we need SchemaContext to create the tree state. It's
    // recommended to cache the schema context and reuse it across different application's components to
    // avoid loading and storing same schemas multiple times.
    const imodelSchemaContextsCache = new Map<string, SchemaContext>();
    
    function getIModelSchemaContext(imodel: IModelConnection) {
      let context = imodelSchemaContextsCache.get(imodel.key);
      if (!context) {
        context = new SchemaContext();
        context.addLocater(new ECSchemaRpcLocater(imodel.getRpcProps()));
        imodelSchemaContextsCache.set(imodel.key, context);
        imodel.onClose.addListener(() => imodelSchemaContextsCache.delete(imodel.key));
      }
      return context;
    }
    
    function createIModelAccess(imodel: IModelConnection) {
      const schemaProvider = createECSchemaProvider(getIModelSchemaContext(imodel));
      return {
        ...schemaProvider,
        // while caching for hierarchy inspector is not mandatory, it's recommended to use it to improve performance
        ...createCachingECClassHierarchyInspector({ schemaProvider, cacheSize: 100 }),
        // the second argument is the maximum number of rows the executor will return - this allows us to
        // avoid creating hierarchy levels of insane size (expensive to us and useless to users)
        ...createLimitingECSqlQueryExecutor(createECSqlQueryExecutor(imodel), 1000),
      };
    }
  • getHierarchyDefinition is a factory function that creates a hierarchy definition, describing the hierarchy the tree component will render. The @itwin/presentation-hierarchies package describes the concept of hierarchy definitions in more detail.

The resulting state object contains the following properties:

  • isLoading is a boolean indicating whether the root tree nodes are being loaded. Set to true on initial load and on reload (e.g. when iModel data changes).

  • rootNodes is an array of root tree nodes and is what the component should render. There are several types of nodes:

    • A PresentationHierarchyNode is the primary type of node, created based on the hierarchy definition. The isPresentationHierarchyNode type guard utility may be used to check if a node is of this type.
    • A PresentationInfoNode is a non-expandable, non-selectable informational type of node, generally created when for some reason we don't have any real nodes to show. There may be different reasons like filtered-out nodes, too large result set, a network error, etc. The type attribute of the node indicates that.
  • expandNode function to expand or collapse a node.

  • isNodeSelected and selectNodes function to inspect and change tree selection.

  • getHierarchyLevelDetails function to access details of a specific hierarchy level. The returned object provides access to:

    • hierarchy level size limit,
    • hierarchy level instance filter,
    • instance keys of the nodes in the hierarchy level.
  • reloadTree function to reload the tree, optionally keeping its state, after an iModel data change.

  • setFormatter function to set active node label formatter.

useSelectionHandler

This is a React hook that helps implement different selection modes in a tree, whose state is managed through the useTree or useUnifiedSelectionTree hooks.

It takes 3 required properties:

  • rootNodes and selectNodes are the corresponding properties from the tree state object, created using useTree or useUnifiedSelectionTree hook.

  • selectionMode is a string that defines the selection mode. It can be one of the following values:

    • none - no selection is allowed,
    • single - only one node can be selected at a time,
    • extended - multiple nodes can be selected using shift and ctrl keys,
    • multiple - multiple nodes can be selected without using shift or ctrl keys.

The returned object contains 2 functions, that should be called by the node renderer: onNodeClick and onNodeKeyDown.

Our tree renderer implementation calls this hook and passes the callbacks to the node renderer, so there's no need to use it unless implementing a custom tree renderer.

useUnifiedSelectionTree & UnifiedSelectionProvider

The package delivers a variation of useTree, that automatically hooks tree selection into the Unified Selection system. It takes the same properties as useTree, plus a couple of additional ones:

  • imodelKey is a string that uniquely identifies the iModel the tree is associated with. It's used to distinguish selection changes between different iModels. Generally, the value is obtained using IModelConnection.key getter.

  • sourceName is a string that distinguishes selection changes being made by different components. It's used to avoid conflicts between different components that use the same iModel and the same selection storage. The value should be unique for each component.

The returned result is identical to the one from useTree.

The hook also relies on unified selection storage being provided to it through a React context. For that, the package delivers the UnifiedSelectionProvider component, that should wrap the tree component, using the useUnifiedSelectionTree hook:

import { TreeRenderer, UnifiedSelectionProvider, useUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";
import { createStorage } from "@itwin/unified-selection";
import { useEffect, useState } from "react";

// Not part of the package - this should be created once and reused across different components of the application.
const selectionStorage = createStorage();

/** Component providing the selection storage and access to iModel. Usually this is done in a top-level component. */
function MyTreeComponent({ imodel }: { imodel: IModelConnection }) {
  const [imodelAccess, setIModelAccess] = useState<IModelAccess>();
  useEffect(() => {
    setIModelAccess(createIModelAccess(imodel));
  }, [imodel]);

  if (!imodelAccess) {
    return null;
  }

  return (
    <UnifiedSelectionProvider storage={selectionStorage}>
      <MyTreeComponentInternal imodelKey={imodel.key} imodelAccess={imodelAccess} />
    </UnifiedSelectionProvider>
  );
}

After providing the unified selection storage through the context, the useUnifiedSelectionTree hook can be used inside the component as follows:

function MyTreeComponentInternal({ imodelAccess, imodelKey }: { imodelAccess: IModelAccess; imodelKey: string }) {
  const { rootNodes, ...state } = useUnifiedSelectionTree({
    // the source name is used to distinguish selection changes being made by different components
    sourceName: "MyTreeComponent",
    // the iModel key is required for unified selection system to distinguish selection changes between different iModels
    imodelKey,
    // iModel access is used to build the hierarchy
    imodelAccess,
    // the hierarchy definition describes the hierarchy using ECSQL queries
    getHierarchyDefinition,
  });
  if (!rootNodes || !rootNodes.length) {
    return "No data to display";
  }
  return <TreeRenderer {...state} rootNodes={rootNodes} />;
}

iTwinUI components

While the package provides a headless UI, it also delivers a set of iTwinUI-based components for rendering the tree, which should cover majority of use cases. Consumers using the below components are required to provide a compatible @itwin/itwinui-react package, which is an optional peer dependency to this package.

TreeRenderer

The component is based on iTwinUI Tree component and uses our TreeNodeRenderer to render the nodes. In addition, it makes use of the useSelectionHandler hook to add selection modes' support.

The iTwinUI Tree component requires a getNode function that maps nodes to NodeData<TNode> objects. Our TreeRenderer uses createRenderedTreeNodeData function for this purpose, and it's available for consumers as well, in case a custom iTwinUI Tree component implementation is being written.

TreeNodeRenderer

The component is based on TreeNode component from iTwinUI library and supports the following features:

  • Rendering informational type of nodes (e.g. "No filter matches", "Too many nodes in a hierarchy level", etc.).
  • Reporting click and key down events for use with useSelectionHandler hook.
  • Icons, selection, expand / collapse.
  • Action buttons to clear / set hierarchy level instance filter.

Full example

import { IModelConnection } from "@itwin/core-frontend";
import { SchemaContext } from "@itwin/ecschema-metadata";
import { ECSchemaRpcLocater } from "@itwin/ecschema-rpcinterface-common";
import { createECSchemaProvider, createECSqlQueryExecutor } from "@itwin/presentation-core-interop";
import { createLimitingECSqlQueryExecutor, createNodesQueryClauseFactory, HierarchyDefinition } from "@itwin/presentation-hierarchies";

import { TreeRenderer, UnifiedSelectionProvider, useUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";
import { createStorage } from "@itwin/unified-selection";
import { useEffect, useState } from "react";

import { createBisInstanceLabelSelectClauseFactory, createCachingECClassHierarchyInspector } from "@itwin/presentation-shared";

// Not really part of the package, but we need SchemaContext to create the tree state. It's
// recommended to cache the schema context and reuse it across different application's components to
// avoid loading and storing same schemas multiple times.
const imodelSchemaContextsCache = new Map<string, SchemaContext>();

function getIModelSchemaContext(imodel: IModelConnection) {
  let context = imodelSchemaContextsCache.get(imodel.key);
  if (!context) {
    context = new SchemaContext();
    context.addLocater(new ECSchemaRpcLocater(imodel.getRpcProps()));
    imodelSchemaContextsCache.set(imodel.key, context);
    imodel.onClose.addListener(() => imodelSchemaContextsCache.delete(imodel.key));
  }
  return context;
}

function createIModelAccess(imodel: IModelConnection) {
  const schemaProvider = createECSchemaProvider(getIModelSchemaContext(imodel));
  return {
    ...schemaProvider,
    // while caching for hierarchy inspector is not mandatory, it's recommended to use it to improve performance
    ...createCachingECClassHierarchyInspector({ schemaProvider, cacheSize: 100 }),
    // the second argument is the maximum number of rows the executor will return - this allows us to
    // avoid creating hierarchy levels of insane size (expensive to us and useless to users)
    ...createLimitingECSqlQueryExecutor(createECSqlQueryExecutor(imodel), 1000),
  };
}

// Not part of the package - this should be created once and reused across different components of the application.
const selectionStorage = createStorage();

/** Component providing the selection storage and access to iModel. Usually this is done in a top-level component. */
function MyTreeComponent({ imodel }: { imodel: IModelConnection }) {
  const [imodelAccess, setIModelAccess] = useState<IModelAccess>();
  useEffect(() => {
    setIModelAccess(createIModelAccess(imodel));
  }, [imodel]);

  if (!imodelAccess) {
    return null;
  }

  return (
    <UnifiedSelectionProvider storage={selectionStorage}>
      <MyTreeComponentInternal imodelKey={imodel.key} imodelAccess={imodelAccess} />
    </UnifiedSelectionProvider>
  );
}

type IModelAccess = Parameters<typeof useUnifiedSelectionTree>[0]["imodelAccess"];

// The hierarchy definition describes the hierarchy using ECSQL queries; here it just returns all `BisCore.PhysicalModel` instances
function getHierarchyDefinition({ imodelAccess }: { imodelAccess: IModelAccess }): HierarchyDefinition {
  // Create a factory for building nodes SELECT query clauses in a format understood by the provider
  const nodesQueryFactory = createNodesQueryClauseFactory({ imodelAccess });
  // Create a factory for building labels SELECT query clauses according to BIS conventions
  const labelsQueryFactory = createBisInstanceLabelSelectClauseFactory({ classHierarchyInspector: imodelAccess });
  return {
    defineHierarchyLevel: async () => [
      {
        fullClassName: "BisCore.PhysicalModel",
        query: {
          ecsql: `
            SELECT
              ${await nodesQueryFactory.createSelectClause({
                ecClassId: { selector: "this.ECClassId" },
                ecInstanceId: { selector: "this.ECInstanceId" },
                nodeLabel: {
                  selector: await labelsQueryFactory.createSelectClause({ classAlias: "this", className: "BisCore.PhysicalModel" }),
                },
                hasChildren: false,
              })}
            FROM BisCore.PhysicalModel this
          `,
        },
      },
    ],
  };
}

/** Internal component that creates and renders tree state. */
function MyTreeComponentInternal({ imodelAccess, imodelKey }: { imodelAccess: IModelAccess; imodelKey: string }) {
  const { rootNodes, setFormatter, isLoading, ...state } = useUnifiedSelectionTree({
    // the source name is used to distinguish selection changes being made by different components
    sourceName: "MyTreeComponent",
    // the iModel key is required for unified selection system to distinguish selection changes between different iModels
    imodelKey,
    // iModel access is used to build the hierarchy
    imodelAccess,
    // supply the hierarchy definition
    getHierarchyDefinition,
  });
  if (!rootNodes) {
    return "Loading...";
  }
  return <TreeRenderer {...state} rootNodes={rootNodes} />;
}

Localization

Localization can be enabled for TreeRenderer component and useTree and useUnifiedSelectionTree hooks by providing an object with localized strings that will be used instead of the default English ones.

Example:

import { useUnifiedSelectionTree } from "@itwin/presentation-hierarchies-react";

type IModelAccess = Parameters<typeof useUnifiedSelectionTree>[0]["imodelAccess"];

const localizedStrings = {
  // strings for the `useUnifiedSelectionTree` hook
  unspecified: "Unspecified",
  other: "Other",

  // strings for `TreeRenderer` and `TreeNodeRenderer`
  loading: "Loading...",
  filterHierarchyLevel: "Apply hierarchy filter",
  clearHierarchyLevelFilter: "Clear active filter",
  noFilteredChildren: "No child nodes match current filter",
  resultLimitExceeded: "There are more items than allowed limit of {{limit}}.",
  resultLimitExceededWithFiltering: "Please provide <link>additional filtering</link> - there are more items than allowed limit of {{limit}}.",
  increaseHierarchyLimit: "<link>Increase the hierarchy level size limit to {{limit}}.</link>",
  increaseHierarchyLimitWithFiltering: "Or, <link>increase the hierarchy level size limit to {{limit}}.</link>",
};

function MyTreeComponent({ imodelAccess, imodelKey }: { imodelAccess: IModelAccess; imodelKey: string }) {
  const { rootNodes, ...state } = useUnifiedSelectionTree({
    sourceName: "MyTreeComponent",
    imodelKey,
    imodelAccess,
    localizedStrings,
    getHierarchyDefinition,
  });
  if (!rootNodes) {
    return localizedStrings.loading;
  }
  return <TreeRenderer {...state} rootNodes={rootNodes} localizedStrings={localizedStrings} onFilterClick={() => {}} />;
}

In case the TreeNodeRenderer component is used within a custom tree renderer, the tree component should supply localized strings through LocalizationContextProvider:

import {
  createRenderedTreeNodeData,
  LocalizationContextProvider,
  RenderedTreeNode,
  TreeNodeRenderer,
  TreeRenderer,
} from "@itwin/presentation-hierarchies-react";

type TreeProps = ComponentPropsWithoutRef<typeof Tree<RenderedTreeNode>>;
type TreeRendererProps = Parameters<typeof TreeRenderer>[0];

function MyTreeRenderer(props: TreeRendererProps) {
  const nodeRenderer = useCallback<TreeProps["nodeRenderer"]>((nodeProps) => {
    return <TreeNodeRenderer {...nodeProps} onFilterClick={() => {}} expandNode={() => {}} />;
  }, []);

  const getNode = useCallback<TreeProps["getNode"]>((node) => createRenderedTreeNodeData(node, () => false), []);

  return (
    <LocalizationContextProvider localizedStrings={localizedStrings}>
      <Tree<RenderedTreeNode> {...props} data={props.rootNodes} nodeRenderer={nodeRenderer} getNode={getNode} />
    </LocalizationContextProvider>
  );
}