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

configured-dnd-context

v1.0.16

Published

A context with a provider, hook, and Higher Order Component meant to make it simpler to work with dnd-kit

Downloads

32

Readme

ConfiguredDndContext

A context with a provider, hook, and Higher Order Component meant to make it simpler to work with dnd-kit for some common use cases involving multiple sortable containers and wanting to copy or move between them.

How to Use?

Check out the storybook for some quick ideas, or below for more detail.

In a project

Install Dependency

Start by installing with npm install configured-dnd-context --save or yarn add configured-dnd-context or whatever syntax your package manager uses.

The base provider

For the functionality to work, you must use the provider.

import { ConfiguredDndProvider } from 'configured-dnd-context'

...
<ConfiguredDndProvider>
      {restOfApp}
</ConfiguredDndProvider>
...

Note that this provider includes the DndContext as part of it, and can receive all of the props that it does. Additional props are as seen below

interface ConfiguredDndProviderPropTypes extends DndContextProps {
  /** Want a different dragging cursor? Do it here! */
  draggingCursor?: string
  /**
   * Do you want to maintain original ids by default?
   * When copying an element via dndCopy, this leads to the overlay showing the item going back to where it came from.
   */
  maintainOriginalIds?: boolean
  /** Want a different method for generating unique ids? */
  getUniqueId?: () => UniqueIdentifier
  /** Want to add props to the drag overlay? */
  dragOverlayProps?: DragOverlayProps
}

DragOverlayProps are the dnd-kit included ones.

Provided functions and value (Accessible by both the Hook and HOC)
type RegisterItemGroupTypeFunctionParameters = {
  /**
   * A unique id for this item group
   */
  id: UniqueIdentifier
  /**
   * Item data for this - will have ids attached and items put into item
   * Example: original = ['a', {b:1}] output = [{id: 1, item: 'a'}, {id: 2, item: {b:1}}]
   */
  items: any[]
  /**
   * itemPrefix: A prefix for the item ids
   */
  itemPrefix?: string
  /**
   * Data specific to this item group
   */
  data?: any
}
/** Use this function to register a group of items (typically used for a SortableContext group) */
registerItemGroup(params: RegisterItemGroupTypeFunctionParameters) => void

/** an item in a container or item group */
type ContainerItem = {
  /** the basic id */
  id: UniqueIdentifier
  /** the actual item information */
  item?: any
  /** was this copied from another id at some point */
  copiedFromId?: UniqueIdentifier
  /**What container was this copied to */
  copiedToContainer?: UniqueIdentifier
  /** what was this items original id */
  originalId?: UniqueIdentifier
}
/** Use this function to get a group of items by id */
type getItemGroup(id: UniqueIdentifier)=>ContainerItem[]

/** Use this function to get a unique id, defaults to uuid v4 */
type getUniqueId()=>UniqueId

/** Get the information associated with the item group - for some reason dnd-kit SortableContext doesn't already support extra data 
 * though useDroppable does */ 
type getItemGroupData(id: UniqueIdentifier)=>any

/** Get rid of an item given the id */
type removeItemOfId(id: UniqueIdentifier)=>void

/** Register an item that is independent of others, but you would still like to get the other benefits of this kit with (such as copying) */
type registerNonGroupedItem(id: UniqueIdentifier, item: any)=>void

/** Get the non-grouped item by id */
type getNonGroupedItem(id: UniqueIdentifier)=> ContainerItem

/** The currently active element, according to react-dnd
 * aka, what is being dragged - active.id is useful to compare with for styling purposes
 */
active: Active | null

/** Update any item data by id, rather than doing a full set or re-registering */
type updateItem: (id: UniqueIdentifier, item: any) => void

/** Get any item by id  */
type getItem: (id: UniqueIdentifier) => ContainerItem

/** Are you in the default provider (if so, that would mean also in the overlay) */
inDefaultProvider: boolean

/** What container id is an active currently over */
overContainerId: null | UniqueIdentifier

useConfiguredDnd Hook

This hook has access to all of the functions and properties given by the provider. Additionally, it will automatically assign a stateful id for your component. Example usage below - for complete working examples, please check out the storybook.

import { useConfiguredDnd } from 'configured-dnd-context'
...
const { id, registerItemGroup, getItemGroup } = useConfiguredDnd()

useEffect(() => {
  if (id) {
    registerItemGroup({
      id,
      items: ['A','B','C','D'],
      data
    })
  }
}, [id])

const items = getItemGroup(id)

<SortableContext
      id={id}
      items={items.map(({ id }) => id)}
      strategy={rectSortingStrategy}
    >
    {items.map(item=>{
        return <Draggable id={item.id} item={item.item} />
    })}
</SortableContext>

withConfiguredDnd HOC

This Higher Order Component has access to all of the functions and properties given by the provider. Additionally, it will automatically assign a stateful id for your component. Example usage below - for complete working examples, please check out the storybook.

import { withConfiguredDnd } from 'configured-dnd-provider'
import { useDraggable } from '@dnd-kit/core'
import { CSS } from '@dnd-kit/utilities'
...

function SomeFunctionalComponent({configuredDnd, extraData})
{
    const {id, registerNonGroupedItem, getNonGroupedItem } = configuredDnd
    const { attributes, listeners, setNodeRef, transform } = useDraggable({
        id,
        data: {
            renderOverlayItem: ()=>{
                return (<div>Sample</div>)
            }
            dndCopy: true,
            dndDisallowContainerChanging: false
        }
    })

    // registering allows it to interact in expected function
    // if allowing copying or wanting to utilize other features
    // of the framework - note that you may just want to use useSortable
    // to get the container animations done nicely
   useEffect(() => {
      registerNonGroupedItem(id, extraData)
   }, [id, extraData])

     // dnd-kit gives us the basic styles
  const style = {
    ...extraStyle,
    transform: CSS.Transform.toString(transform),
    transition,
    opacity: styleBaseAsActive ? 0.5 : 1,
    cursor: styleBaseAsActive ? 'grabbing' : 'grab'
  }

  return <div style={style}>Sample</div>
}

export default withConfiguredDnd(SomeFunctionalComponent)

Inside of the data object

In the HOC sample above, we see some extra things in the useDraggable hook data - the same work in the useSortable hook data. Below are the new items utilized.

/** If true, this should copy to containers (SortableContext) when dropping to them, not just move */
dndCopy?: boolean
/**
 * Do you want to maintain this elements original id?
 * When copying an element via dndCopy, this leads to the overlay showing the item going back to where it came from.
 */
dndMaintainOriginalId?: boolean
/** If true, this shouldn't be allowed to change what container it is in on drop*/
dndDisallowContainerChanging?: boolean
/** What to render to the overlay when this is the active item */
renderOverlayItem?: (value: typeof DndContext) => React.JSX.Element
/** Return true if this item is allowed to drop to the given container, false otherwise */
dndAllowableDropFilter?: ({containerId, containerData}: {containerId: UniqueIdentifier, containerData: any})=>boolean
/** What to do when this item moves over another - it is a hook into the DndContext onDragOver with this item
 * being the active one
 */
onDragOver?: (dragOverEvent: DragOverEvent)=>void
/** What to do when this item is dropped - it is a hook into the DndContext onDragEnd with this item 
 * being the active one */
onDragEnd?: (dragEndEvent: DragEndEvent)=>void

For the useDroppable hook data, there are two things added as well

/** What to do when this item has another move over it - it is a hook into the DndContext onDragOver with this item
 * being the over one
 */
onDragOver?: (dragOverEvent: DragOverEvent)=>void
/** What to do when this item is dropped - it is a hook into the DndContext onDragEnd with this item being the over one */
onDrop?: (dragEndEvent: DragEndEvent)=>void

withMakeDraggable HOC

This is to quickly make a basic display element draggable. It makes hooks, manages id and such for you and passes it to you, so your element just needs to grab what it receives. For more complete examples, please take a look at the stories, or the Field copied from the project at a point where it is working, and where this HOC was imported from.

type DndDraggableType = {
  /**
   * Should this be considered as a Sortable object and interact as such
   */
  sortable?: boolean
  /**
   * An id if we don't want the generated one
   */
  id?: UniqueIdentifier
  /**
   * Data associated with this item
   */
  data?: object
  /**
   * Should the cursor behave differently than grab
   */
  overCursor?: string
  /**
   * Should the cursor behave differently than grabbing
   */
  draggingCursor?: string
  /**
   * Is this an individual item outside of a normal group
   */
  nonGroupedItem?: boolean
  /**
   * Do we want any extra style while dragging?
   */
  whileDraggingExtraStyle?: object
  /**
   * Do we want a function to exist that didn't before?
   */
  dataFunction?: () => object
}

type WithMakeDraggableAttachedPropTypes = {
  dndExtras: {
    /** Extra Style To Attach to an Element */
    style?: object
    /** Is something currently being dragged over this */
    isOver?: boolean
    /**
     * Extra Data
     */
    data: any
    /** An id that should be used */
    id: UniqueIdentifier
    /**
     * Is this object in the overlay?
     */
    inOverlay?: boolean
    /** 
     * A context value - for use in rendering mainly in the items if using this in combination with the useConfiguredDndHook 
     * useConfiguredDnd({inOverlay, value})
     * */
    value: typeof ConfiguredDndContextDefaultValue
  } & (ReturnType<typeof useDraggable> | ReturnType<typeof useSortable>)
}

BaseComponent = ({dndExtras, rest} ) => {...}
const MyComponent = withMakeDraggable(BaseComponent)

<MyComponent dndDraggable={optionalInputHere} />

withMakeDroppable HOC

This is to quickly make a basic display element droppable. It makes hooks, manages id and such for you and passes it to you, so your element just needs to grab what it receives. For more complete examples, please take a look at the stories, or the Field copied from the project at a point where it is working, and where this HOC was imported from.

type DndDroppableType = {
  /**
   * A non-generated id
   */
  id?: UniqueIdentifier
  /**
   * Is this droppable disabled?
   */
  disabled?: boolean
  /**
   * What data does this droppable have to keep track of
   */
  data?: object
    /**
   * On the chance that this is not an independent drop zone,
   * but meant to act as an item group container
   */
  groupRoot?: boolean
  /**
   * If this is an item group container, allow registering item at first render
   */
  items?: any
  /**
   * Are the items already container items ({item, id})
   */
  itemsAreContainerItems?: boolean
}

type WithMakeDroppableAttachedPropTypes = {
  dndExtras: {
    /** Extra Style To Attach to an Element */
    style?: object
    /** Is something currently being dragged over this */
    isOver?: boolean
    /**
     * Extra Data
     */
    data: any
    /** An id that should be used */
    id: UniqueIdentifier
  } & ReturnType<typeof useDroppable>
}

BaseComponent = ({dndExtras, rest} ) => {...}
const MyComponent = withMakeDroppable(BaseComponent)

<MyComponent dndDroppable={optionalInputHere} />

disableSortingStrategy Convenience for variable sized items in sortable object

Used in conjunction with item data dndSwapPositionsWhileMoving: true on a SortableContext item to not use performant animations, but instead costly complete renders by moving items in the containers on the fly. See this post for more on why you might do this for multi-sized items in a the same container. Also, example in storybook.

// some sortable list
<SortableContext
        id={`${id}`}
        items={itemIds}
        strategy={disableSortingStrategy}
      >
  ...
</SortableContext>
// some sortable item used in that context
const hookData = useSortable({
  id,
  data: {
    dndSwapPositionsWhileMoving: true
  }
})

This may be good for prototyping, but before moving to production consider creating a sorting strategy.

Play with it locally before deciding to install it in a project

You may run this locally with npm install and npm run storybook to see some example functions.

const { id, ...value } = useConfiguredDnd()

History of Creation or Why Publish This?

This was pulled out of a larger project I have been working on, as I realized it might be useful for others when I had searched and saw threads such as how to drag by copying and consider adding clone from list example. After following this example and adding in more functionality that I figured I would use, I wanted to be able to make it simple for others to do the same without getting too far off of the original.

A component using this System

Below is an example component that this system is being developed around, should someone want to see a bit more on the why.

// FieldSet.tsx
import React, { useEffect } from 'react'
import { Paper, Typography, Stack } from '@mui/material'
import { useDroppable } from '@dnd-kit/core'

import { useConfiguredDnd } from 'configured-dnd-context'

import { DragEndEvent } from '@dnd-kit/core'
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'

import FieldSetProps from './FieldSet.propTypes'

import Field from './components/Field'

function FieldSet ({ fields = [], mappable, title }: FieldSetProps) {
  const {
    registerItemGroup,
    getItemGroup,
    updateItem,
    id,
    getItem,
    getUniqueId
  } = useConfiguredDnd()

  const { setNodeRef } = useDroppable({ id })

  useEffect(() => {
    if (id) {
      registerItemGroup({ id, items: fields, itemPrefix: 'field-' })
    }
  }, [id, fields])

  const items = getItemGroup(id)

  return (
    <Paper>
      <Typography variant='h6'>{title}</Typography>
      <SortableContext
        id={`${id}`}
        items={items.map(({ id }) => id)}
        strategy={rectSortingStrategy}
      >
        <Stack
          ref={setNodeRef}
          style={{ minWidth: '100px', minHeight: '100px', height: '100%' }}
        >
          {items.map(({ id: fieldId, item }) => (
            <Field
              dndDraggable={{
                sortable: true,
                id: fieldId,
                data: {
                  dndDisallowContainerChanging: true,
                  parentId: id,
                  onDrop: (onDragEnd: DragEndEvent) => {
                    if (onDragEnd.active.data?.current?.parentId === id) {
                      return
                    }

                    if (typeof item === 'string') {
                      item = {
                        name: item,
                        label: item
                      }
                    }
                    let subField = { ...getItem(onDragEnd.active.id) }
                    if (!subField) {
                      return
                    }
                    updateItem(fieldId, {
                      ...item,
                      subFields: [
                        ...(item.subFields || []),
                        { id: getUniqueId(), field: subField.item }
                      ]
                    })
                  }
                }
              }}
              key={fieldId}
              field={item}
              mappable={mappable}
            />
          ))}
        </Stack>
      </SortableContext>
    </Paper>
  )
}

export default FieldSet


// Field.tsx
import React from 'react'
import { Paper, Typography, Stack, IconButton } from '@mui/material'
import { DragIndicator, Start, Add, Delete } from '@mui/icons-material'

import FieldType from './Field.type'
import {
  useConfiguredDnd,
  withMakeDroppable,
  withMakeDraggable,
  withMakeDraggableAttachesPropTypes
} from 'configured-dnd-context'
import { DragEndEvent } from '@dnd-kit/core'

type FieldPropTypes = {
  field: FieldType
  mappable?: boolean
  subField?: boolean
  onDelete?: () => void
} & withMakeDraggableAttachesPropTypes

function Field ({
  field,
  mappable,
  subField,
  onDelete,
  dndExtras: {
    setNodeRef,
    isDragging = false,
    isOver = false,
    attributes,
    style,
    listeners,
    data,
    active,
    id,
    inOverlay
  }
}: React.PropsWithRef<FieldPropTypes>): JSX.Element {
  const { updateItem, getItem, getUniqueId, removeItemOfId } = useConfiguredDnd(
    { inOverlay }
  )

  if (typeof field === 'string') {
    field = {
      name: field,
      label: field,
      type: 'string',
      subFields: []
    }
  }

  let { item } = inOverlay ? { item: {} } : getItem(id) || {}

  let { name, label } = field
  let highlightStyle: { [key: string]: any } = { ...style }

  if (mappable && active?.data?.current?.parentId !== data.parentId) {
    if (active && !isDragging) {
      highlightStyle.backgroundColor = 'yellow'
    }
    if (isOver && !isDragging) {
      highlightStyle = {
        ...highlightStyle,
        borderColor: 'red',
        borderStyle: 'solid',
        borderWidth: '2px'
      }
    }
  }

  // if only one got passed in
  name = name || label
  label = label || name

  const finalItem: FieldType = item || field

  return (
    <Paper
      style={highlightStyle}
      ref={setNodeRef}
      {...attributes}
      {...listeners}
    >
      <Stack direction='row' padding={1}>
        {!subField && <DragIndicator />}
        {subField && <Start />}
        <Typography>{label}</Typography>
        {typeof finalItem !== 'string' &&
          finalItem.subFields &&
          finalItem.subFields.length > 0 && (
            <Stack>
              {mappable && <Add />}
              {finalItem.subFields.map((sub, index) => {
                return (
                  <DroppableField
                    key={sub.id}
                    field={sub.field}
                    subField={true}
                    mappable={mappable}
                    onDelete={() => {
                      const subItem = { ...getItem(id) }
                      const subFields = [...(subItem.item.subFields || [])]
                      const [removed] = subFields.splice(index, 1)

                      const newItem = {
                        ...(typeof sub.field === 'string' ? {} : sub.field),
                        ...subItem.item,
                        subFields
                      }

                      removeItemOfId(removed.id)
                      updateItem(id, newItem)
                    }}
                    dndDroppable={{
                      id: sub.id,
                      data: {
                        parentId: data?.parentId,
                        onDrop: (onDragEnd: DragEndEvent) => {
                          if (
                            onDragEnd.active.data?.current?.parentId ===
                            data?.parentId
                          ) {
                            return
                          }

                          let activeField = { ...getItem(onDragEnd.active.id) }
                          if (!activeField) {
                            return
                          }
                          if (typeof activeField.item === 'string') {
                            activeField.item = {
                              name: subField,
                              label: subField
                            }
                          }

                          const subItem = { ...getItem(sub.id) }
                          updateItem(sub.id, {
                            ...(typeof sub.field === 'string' ? {} : sub.field),
                            ...subItem.item,
                            subFields: [
                              ...(subItem.item.subFields || []),
                              { id: getUniqueId(), field: activeField.item }
                            ]
                          })
                        }
                      }
                    }}
                  />
                )
              })}
            </Stack>
          )}
        {mappable &&
          (typeof finalItem === 'string' ||
            !finalItem.subFields ||
            finalItem.subFields.length === 0) && (
            <>
              <Start />
              <Add />
            </>
          )}
        {subField && (
          <IconButton aria-label='delete' onClick={onDelete}>
            <Delete />
          </IconButton>
        )}
      </Stack>
    </Paper>
  )
}

const DroppableField = withMakeDroppable(Field)

const DraggableField = withMakeDraggable(Field)

export default DraggableField

A component using this System

Below is an example component that this system is being developed around, should someone want to see a bit more on the why.

// FieldSet.tsx
import React, { useEffect } from 'react'
import { Paper, Typography, Stack } from '@mui/material'
import { useDroppable } from '@dnd-kit/core'

import { useConfiguredDnd } from 'configured-dnd-context'

import { DragEndEvent } from '@dnd-kit/core'
import { SortableContext, rectSortingStrategy } from '@dnd-kit/sortable'

import FieldSetProps from './FieldSet.propTypes'

import Field from './components/Field'

function FieldSet ({ fields = [], mappable, title }: FieldSetProps) {
  const {
    registerItemGroup,
    getItemGroup,
    updateItem,
    id,
    getItem,
    getUniqueId
  } = useConfiguredDnd()

  const { setNodeRef } = useDroppable({ id })

  useEffect(() => {
    if (id) {
      registerItemGroup({ id, items: fields, itemPrefix: 'field-' })
    }
  }, [id, fields])

  const items = getItemGroup(id)

  return (
    <Paper>
      <Typography variant='h6'>{title}</Typography>
      <SortableContext
        id={`${id}`}
        items={items.map(({ id }) => id)}
        strategy={rectSortingStrategy}
      >
        <Stack
          ref={setNodeRef}
          style={{ minWidth: '100px', minHeight: '100px', height: '100%' }}
        >
          {items.map(({ id: fieldId, item }) => (
            <Field
              dndDraggable={{
                sortable: true,
                id: fieldId,
                data: {
                  dndDisallowContainerChanging: true,
                  parentId: id,
                  onDrop: (onDragEnd: DragEndEvent) => {
                    if (onDragEnd.active.data?.current?.parentId === id) {
                      return
                    }

                    if (typeof item === 'string') {
                      item = {
                        name: item,
                        label: item
                      }
                    }
                    let subField = { ...getItem(onDragEnd.active.id) }
                    if (!subField) {
                      return
                    }
                    updateItem(fieldId, {
                      ...item,
                      subFields: [
                        ...(item.subFields || []),
                        { id: getUniqueId(), field: subField.item }
                      ]
                    })
                  }
                }
              }}
              key={fieldId}
              field={item}
              mappable={mappable}
            />
          ))}
        </Stack>
      </SortableContext>
    </Paper>
  )
}

export default FieldSet


// Field.tsx
import React from 'react'
import { Paper, Typography, Stack, IconButton } from '@mui/material'
import { DragIndicator, Start, Add, Delete } from '@mui/icons-material'

import FieldType from './Field.type'
import {
  useConfiguredDnd,
  withMakeDroppable,
  withMakeDraggable,
  withMakeDraggableAttachesPropTypes
} from 'configured-dnd-context'
import { DragEndEvent } from '@dnd-kit/core'

type FieldPropTypes = {
  field: FieldType
  mappable?: boolean
  subField?: boolean
  onDelete?: () => void
} & withMakeDraggableAttachesPropTypes

function Field ({
  field,
  mappable,
  subField,
  onDelete,
  dndExtras: {
    setNodeRef,
    isDragging = false,
    isOver = false,
    attributes,
    style,
    listeners,
    data,
    active,
    id,
    inOverlay
  }
}: React.PropsWithRef<FieldPropTypes>): JSX.Element {
  const { updateItem, getItem, getUniqueId, removeItemOfId } = useConfiguredDnd(
    { inOverlay }
  )

  if (typeof field === 'string') {
    field = {
      name: field,
      label: field,
      type: 'string',
      subFields: []
    }
  }

  let { item } = inOverlay ? { item: {} } : getItem(id) || {}

  let { name, label } = field
  let highlightStyle: { [key: string]: any } = { ...style }

  if (mappable && active?.data?.current?.parentId !== data.parentId) {
    if (active && !isDragging) {
      highlightStyle.backgroundColor = 'yellow'
    }
    if (isOver && !isDragging) {
      highlightStyle = {
        ...highlightStyle,
        borderColor: 'red',
        borderStyle: 'solid',
        borderWidth: '2px'
      }
    }
  }

  // if only one got passed in
  name = name || label
  label = label || name

  const finalItem: FieldType = item || field

  return (
    <Paper
      style={highlightStyle}
      ref={setNodeRef}
      {...attributes}
      {...listeners}
    >
      <Stack direction='row' padding={1}>
        {!subField && <DragIndicator />}
        {subField && <Start />}
        <Typography>{label}</Typography>
        {typeof finalItem !== 'string' &&
          finalItem.subFields &&
          finalItem.subFields.length > 0 && (
            <Stack>
              {mappable && <Add />}
              {finalItem.subFields.map((sub, index) => {
                return (
                  <DroppableField
                    key={sub.id}
                    field={sub.field}
                    subField={true}
                    mappable={mappable}
                    onDelete={() => {
                      const subItem = { ...getItem(id) }
                      const subFields = [...(subItem.item.subFields || [])]
                      const [removed] = subFields.splice(index, 1)

                      const newItem = {
                        ...(typeof sub.field === 'string' ? {} : sub.field),
                        ...subItem.item,
                        subFields
                      }

                      removeItemOfId(removed.id)
                      updateItem(id, newItem)
                    }}
                    dndDroppable={{
                      id: sub.id,
                      data: {
                        parentId: data?.parentId,
                        onDrop: (onDragEnd: DragEndEvent) => {
                          if (
                            onDragEnd.active.data?.current?.parentId ===
                            data?.parentId
                          ) {
                            return
                          }

                          let activeField = { ...getItem(onDragEnd.active.id) }
                          if (!activeField) {
                            return
                          }
                          if (typeof activeField.item === 'string') {
                            activeField.item = {
                              name: subField,
                              label: subField
                            }
                          }

                          const subItem = { ...getItem(sub.id) }
                          updateItem(sub.id, {
                            ...(typeof sub.field === 'string' ? {} : sub.field),
                            ...subItem.item,
                            subFields: [
                              ...(subItem.item.subFields || []),
                              { id: getUniqueId(), field: activeField.item }
                            ]
                          })
                        }
                      }
                    }}
                  />
                )
              })}
            </Stack>
          )}
        {mappable &&
          (typeof finalItem === 'string' ||
            !finalItem.subFields ||
            finalItem.subFields.length === 0) && (
            <>
              <Start />
              <Add />
            </>
          )}
        {subField && (
          <IconButton aria-label='delete' onClick={onDelete}>
            <Delete />
          </IconButton>
        )}
      </Stack>
    </Paper>
  )
}

const DroppableField = withMakeDroppable(Field)

const DraggableField = withMakeDraggable(Field)

export default DraggableField

Available Scripts

  • npm run storybook - Runs just the storybook development server.
  • npm run storybook:test - Smoke test the storybook to make sure everything displays
  • npm run storybook:build - Builds the static storybook content.
  • npm run lint - Lints the project
  • npm run dev:next - Runs just the next development server.
  • npm run lint-fix - Fixing linting where it can.
  • npm run jest - Runs the jest unit tests in interactive watch mode.
  • npm run jest:ci - Runs the jest unit tests in non-interactive mode.
  • npm run playwright - Runs the playwright end to end tests against a running storybook
  • npm run playwright:ui - Runs the playwright end to end tests in UI mode.
  • npm run build - Builds an optimized version of the application for production in a .next folder.

Files and Folders

  • .github/ - Files for interactions with github
  • .storybook/ - The storybook configuration files.
  • ConfiguredDndProvider/ - A context provider for the ConfiguredDnd context
  • e2eBaseTests/ - Base tests for the e2e, as the basic stories and functionality among the components tested are the same.
  • node_modules/ - Standard node module install location.
  • playwright-report/ - Report generated by Playwright
  • storybook-static/ - The final storybook site - generated by npm run build-storybook
  • StoryComponents/ - Shared story components
  • useConfiguredDnd/ - A hook for this context
  • withConfiguredDnd/ - A Higher Order Component for this context
  • withMakeDraggable/ - A Higher Order Component for helping to make draggable components in this system
  • withMakeDroppable/ - A Higher Order Component for helping to make droppable components in this system
  • .babelrc.json - A babel configuration
  • .gitignore - Files to ignore when checking into source system.
  • .nvmrc - Specifies the node version for use with node version manager
  • ConfiguredDndContext.defaultValue.ts - default values for the ConfiguredDnd context
  • ConfiguredDndContext.js - The actual react context
  • disableSortingStrategy.ts - Used in conjunction with item data dndSwapPositionsWhileMoving: true on a SortableContext item to not use performant animations, but instead costly complete renders by moving items in the containers on the fly. See this post for more on why you might do this for multi-sized items in a the same container. Also, example in storybook.
  • index.ts - For easier import elsewhere
  • jest.config.js - The configuration file for jest.
  • jest.setup.js - Runs as part of the jest config.
  • LICENSE - The license this product is under (MIT)
  • package-lock.json - File giving full versions of things installed with npm, and their dependencies.
  • package.json - Describes project, including dependencies, scripts, ect. A good starting point when looking at any node project.
  • playwright.config.ts - PlayWright configuration for end to end tests.
  • tsconfig.json - Typescript configurations.
  • tsup.config.ts - Easy publishing of typesciprt

readme.md instead of README.md?

All of the other files (config, ect) in the modern world no longer have the convention of all caps, and it helps to keep eyes from being drawn to this file instead of other, potentially more important files in the file tree.

Why are readme.md files scattered around the repo?

For every file/folder that you create, it should be easy to have a reason behind it. This makes an easy way to document it. Additionally, you can place TODOs and easily parse it out into a readable format. Finally, it is meant to be easy navigation of documents straight in a git graphical interface (github, gitlab, ect). This allows a quick high level overview for newcomers curious about something, and also allows the component to be pulled off to its own module quickly if needed - as this one was.

But I would like it to ...

If you wish to add some functionality, please make a branch and do so. If it adds useful functionality, or is meant to speed this up and doesn't break the existing functionality, feel free to open a PR and we can discuss pulling it to the main branch. Again, the base was created for my own project and seemed like with some extra stories it could be useful to others as well.