CtrlK
BlogDocsLog inGet started
Tessl Logo

tessl/npm-react-sortable-hoc

Higher-order components to turn any list into animated, accessible and touch-friendly sortable lists

Pending
Overview
Eval results
Files

sortable-element.mddocs/

Sortable Element

Higher-order component that makes individual React components sortable within a SortableContainer. Provides simple interface for element positioning, grouping, and state management.

Capabilities

SortableElement HOC

Makes any React component sortable within a SortableContainer.

/**
 * Higher-order component that makes an element sortable within a SortableContainer
 * @param wrappedComponent - The React component to enhance with sortable functionality
 * @param config - Optional configuration object
 * @returns Enhanced React component with sortable element capabilities
 */
function SortableElement<P>(
  wrappedComponent: WrappedComponent<P>,
  config?: Config
): React.ComponentClass<P & SortableElementProps>;

interface SortableElementProps {
  /** Element's sort index within its collection (required) */
  index: number;
  /** Collection identifier for grouping elements */
  collection?: Offset;
  /** Whether the element should be sortable */
  disabled?: boolean;
}

interface Config {
  withRef: boolean;
}

Usage Examples:

import React from 'react';
import { SortableElement } from 'react-sortable-hoc';

// Basic sortable item
const SortableItem = SortableElement(({ value }) => <li>{value}</li>);

// More complex sortable component
const SortableCard = SortableElement(({ title, description, onEdit }) => (
  <div className="card">
    <h3>{title}</h3>
    <p>{description}</p>
    <button onClick={onEdit}>Edit</button>
  </div>
));

// Usage in container
const items = ['Item 1', 'Item 2', 'Item 3'];
{items.map((value, index) => (
  <SortableItem 
    key={`item-${index}`} 
    index={index} 
    value={value} 
  />
))}

Required Props

Index Property

/** Element's sortable index within its collection (required) */
index: number;

The index prop is required and determines the element's position in the sortable sequence. It must be unique within the collection.

Index Examples:

// Basic sequential indexing
{items.map((item, index) => (
  <SortableItem key={item.id} index={index} value={item.value} />
))}

// Custom indexing for sparse arrays
const sparseItems = [
  { id: 'a', value: 'First', sortOrder: 0 },
  { id: 'b', value: 'Second', sortOrder: 2 },
  { id: 'c', value: 'Third', sortOrder: 5 },
];

{sparseItems.map((item) => (
  <SortableItem 
    key={item.id} 
    index={item.sortOrder} 
    value={item.value} 
  />
))}

Optional Props

Collection Property

/** Collection identifier for grouping elements */
collection?: number | string; // default: 0

The collection prop groups elements into separate sortable areas within the same container. Useful for multi-list scenarios or categorized sorting.

Collection Examples:

// Multiple lists in same container
const todoItems = [
  { id: 1, text: 'Task 1', status: 'pending' },
  { id: 2, text: 'Task 2', status: 'completed' },
  { id: 3, text: 'Task 3', status: 'pending' },
];

{todoItems.map((item, index) => (
  <SortableTask
    key={item.id}
    index={index}
    collection={item.status}  // Group by status
    task={item}
  />
))}

// Numeric collections
{leftColumnItems.map((item, index) => (
  <SortableItem key={item.id} index={index} collection={0} value={item} />
))}
{rightColumnItems.map((item, index) => (
  <SortableItem key={item.id} index={index} collection={1} value={item} />
))}

Disabled Property

/** Whether the element should be sortable */
disabled?: boolean; // default: false

When disabled is true, the element cannot be dragged or participate in sorting operations.

Disabled Examples:

// Conditionally disable elements
{items.map((item, index) => (
  <SortableItem
    key={item.id}
    index={index}
    value={item.value}
    disabled={item.locked || item.readonly}
  />
))}

// Disable based on user permissions
{items.map((item, index) => (
  <SortableItem
    key={item.id}
    index={index}
    value={item.value}
    disabled={!userCanSort}
  />
))}

Component Integration

Passing Props Through

All props except index, collection, and disabled are passed through to the wrapped component.

const SortableProductCard = SortableElement(({ 
  product, 
  onAddToCart, 
  onViewDetails,
  className 
}) => (
  <div className={`product-card ${className}`}>
    <img src={product.image} alt={product.name} />
    <h3>{product.name}</h3>
    <p>${product.price}</p>
    <button onClick={() => onAddToCart(product)}>Add to Cart</button>
    <button onClick={() => onViewDetails(product)}>View Details</button>
  </div>
));

// Usage with additional props
<SortableProductCard
  key={product.id}
  index={index}
  product={product}
  className="featured"
  onAddToCart={handleAddToCart}
  onViewDetails={handleViewDetails}
/>

Prop Renaming Pattern

To access sortable-specific props in your component, pass them with different names:

const SortableListItem = SortableElement(({ 
  value, 
  sortIndex, // Renamed from index
  isDisabled  // Renamed from disabled
}) => (
  <li className={isDisabled ? 'disabled' : ''}>
    {value} - Position: {sortIndex}
  </li>
));

// Usage
<SortableListItem
  key={item.id}
  index={index}
  sortIndex={index}     // Pass index again with different name
  disabled={item.locked}
  isDisabled={item.locked}  // Pass disabled again with different name
  value={item.value}
/>

Event Handling

Sortable elements can contain interactive elements, but event handling requires careful consideration:

const InteractiveSortableItem = SortableElement(({ 
  item, 
  onEdit, 
  onDelete 
}) => (
  <div className="sortable-item">
    <span>{item.title}</span>
    <div className="actions">
      <button 
        onClick={(e) => {
          e.preventDefault(); // Prevent drag
          onEdit(item);
        }}
      >
        Edit
      </button>
      <button 
        onClick={(e) => {
          e.preventDefault(); // Prevent drag
          onDelete(item);
        }}
      >
        Delete
      </button>
    </div>
  </div>
));

withRef Configuration

interface Config {
  /** Enable access to wrapped component instance */
  withRef: boolean;
}

withRef Example:

const SortableItem = SortableElement(ItemComponent, { withRef: true });

// Access wrapped instance
const itemRef = useRef();
const wrappedInstance = itemRef.current?.getWrappedInstance();

<SortableItem 
  ref={itemRef} 
  index={index} 
  value={value} 
/>

Advanced Patterns

Dynamic Collections

const [items, setItems] = useState([
  { id: 1, text: 'Item 1', category: 'A' },
  { id: 2, text: 'Item 2', category: 'B' },
  { id: 3, text: 'Item 3', category: 'A' },
]);

const handleSortEnd = ({ oldIndex, newIndex, collection }) => {
  // Sort within specific collection
  const categoryItems = items.filter(item => item.category === collection);
  const otherItems = items.filter(item => item.category !== collection);
  
  const sortedCategoryItems = arrayMove(categoryItems, oldIndex, newIndex);
  setItems([...otherItems, ...sortedCategoryItems]);
};

{items.map((item, index) => (
  <SortableItem
    key={item.id}
    index={items.filter(i => i.category === item.category).indexOf(item)}
    collection={item.category}
    value={item.text}
  />
))}

Conditional Rendering

const ConditionalSortableItem = ({ item, index, showDetails }) => {
  const ItemComponent = ({ value, details }) => (
    <div className="item">
      <span>{value}</span>
      {showDetails && <div className="details">{details}</div>}
    </div>
  );

  const SortableItemComponent = SortableElement(ItemComponent);
  
  return (
    <SortableItemComponent
      index={index}
      value={item.title}
      details={item.description}
    />
  );
};

Install with Tessl CLI

npx tessl i tessl/npm-react-sortable-hoc

docs

array-utilities.md

index.md

sortable-container.md

sortable-element.md

sortable-handle.md

tile.json