Search code examples
reactjsoffice-ui-fabricfluent-uifluentui-reactoffice-ui-fabric-react

Rendering custom item columns with sorting


Can anyone help me resolve the typing issues below for a FluentUI details list?

I have the following code to display a list of items using Fluent UI details list. I need to add the functionality to sort items in each column in ascending order like this:

const buildColumns = (): IColumn[] => {
  const columns: IColumn[] = [];
  columns.push({
    key: "name",
    name: "Name",
    fieldName: "name",
    minWidth: 75,
    maxWidth: 75,
    isResizable: false,
    onColumnClick: _onColumnClick
  });
  columns.push({
    key: "age",
    name: "Age",
    fieldName: "age",
    minWidth: 200,
    maxWidth: 200,
    isResizable: false,
    onColumnClick: _onColumnClick
  });
  return columns;
}

function _copyAndSort<T>(items: T[], columnKey: string, isSortedDescending?: boolean): T[] {
    const key = columnKey as keyof T;
    return items.slice(0).sort((a: T, b: T) => ((isSortedDescending ? a[key] < b[key] : a[key] > b[key]) ? 1 : -1));
}

const _onColumnClick = (ev: React.MouseEvent<HTMLElement>, column: IColumn): void => {
  const [columns, setColumns] = useState<[]>();
  const [names, setNames] = useState<[]>();
  const newColumns: IColumn[] = columns.slice(); // error: columns' is possibly 'undefined'.
  const currColumn: IColumn = newColumns.filter(currCol => column.key === currCol.key)[0];
  newColumns.forEach((newCol: IColumn) => {
    if (newCol === currColumn) {
    currColumn.isSorted = true;
    currColumn.isSortedDescending = !currColumn.isSortedDescending;
    } else {
    newCol.isSorted = false;
    newCol.isSortedDescending = true;
    }
  });
  const newItems = _copyAndSort(names, currColumn.fieldName!, currColumn.isSortedDescending); // error: Argument of type '[] | undefined' is not assignable to parameter of type 'never[]'.
  Type 'undefined' is not assignable to type 'never[]'.
  setColumns(newColumns); // error: Argument of type 'never[]' is not assignable to parameter of type 'SetStateAction<[] | undefined>'.
  Type 'never[]' is not assignable to type '[]'.
    Target allows only 0 element(s) but source may have more.
  setNames(newItems); // error: Argument of type 'IColumn[]' is not assignable to parameter of type 'SetStateAction<[] | undefined>'.
  Type 'IColumn[]' is not assignable to type '[]'.
    Target allows only 0 element(s) but source may have more.
}

export const NamesListBase: React.FunctionComponent<INamesListProps> = (
  props: INamesListProps
) => {

  var columns = buildColumns();
  const names = useSelector(selectAllNames)

  return (
    <div>
          <DetailsList
            items={names}
            columns={columns}
        />
      </div>
  ); 
}

Any pointers on how to fix these typing errors in the code?


Solution

  • There's a few things that you can do to get this working. Here's a functional code sandbox to review: https://codesandbox.io/s/cocky-morning-8hqowd?file=/src/App.tsx

    In short:

    • Create state variables for the sorting field and direction
    • In your onColumnHeaderClick, update the sorting state to be whatever column was clicked. If it's the same column twice, you could reverse the direction which is a nice UX.
    • Create the list of columns and reference the state variables to decide if a certain column is currently sorted
    • Sort the array of items using the new state variables
    • Pass the sorted array into the DetailsList

    The key is that sorting is state and, your list of items and columns need to be derived from that state in order for the interaction to work.

    Hope that helps!

    export default function App() {
      const [sortKey, setSortKey] = useState<string | undefined>(undefined);
      const [isSortedDescending, setIsSortedDescending] = useState(false);
    
      const items = [
        { age: 30, name: "Bob" },
        { age: 31, name: "Alice" }
      ];
    
      const columns: IColumn[] = [
        {
          key: "name",
          fieldName: "name",
          name: "Name",
          minWidth: 100,
          isSorted: sortKey === "name",
          isSortedDescending
        },
        {
          key: "age",
          fieldName: "age",
          name: "Age",
          minWidth: 100,
          isSorted: sortKey === "age",
          isSortedDescending
        }
      ];
    
      const sortedItems = [...items].sort((a, b) => {
        if (sortKey === "age") {
          return isSortedDescending ? b.age - a.age : a.age - b.age;
        }
        if (sortKey === "name") {
          return isSortedDescending
            ? b.name.localeCompare(a.name)
            : a.name.localeCompare(b.name);
        }
        return 0;
      });
    
      function onColumnHeaderClick(
        event?: MouseEvent<HTMLElement>,
        column?: IColumn
      ) {
        if (column) {
          setIsSortedDescending(!!column.isSorted && !column.isSortedDescending);
          setSortKey(column.key);
        }
      }
    
      return (
        <DetailsList
          onColumnHeaderClick={onColumnHeaderClick}
          columns={columns}
          items={sortedItems}
        />
      );
    }