External Resource Drag & Drop

By combining the resourceReorder feature with external resource drag-and-drop, you can easily organize, group, and adjust resources by dragging them into or out of the timeline view.

Dragging resources onto the timeline

Resources can be created by dragging and dropping an external element onto the timeline. In order for that to work you will need to have two things set up:

  • Enable the timeline to receive external resources by setting externalResourceDrop to true.
  • Initialize the external resources (containers) as draggable components.

Use the Draggable component to specify a skeleton event through its dragData option and reference the draggable container in the element option. The dragData accepts a full event definition that will be added to the event calendar on drop. If omitted, a default event will be created.

Use the onResourceCreate for triggering a custom logic on drop like showing a toast.

Dragging resources off the timeline)

Additionally resource can be removed by dragging them out from the timeline and dropping them onto an external drop container. To activate this, you will need to:

  • Enable the timeline to allow dragging events out by setting externalResourceDrag to true.
  • Initialize the

dropcontainer.

Resources can also be dropped on another timeline with externalResourceDrop enabled.

When a resource is dropped into an external drop container or another timeline, it will be deleted from the original calendar and the onResourceDelete will be fired.

You can also use the onResourceDragLeave and onResourceDragEnter, and the drop container's onItemDragEnter and onItemDragLeave events to provide visual feedback or running custom logic during drag.

Looking for external event drag & drop?  Check out this example →
Get started with this example

Demo

Loading the live demo

Preview in full screen

Drag external resources onto a Timeline to add team members, or drag them off to remove them — build crew scheduling and team management.

Code

import { Button, Draggable, Dropcontainer, Eventcalendar, Page, setOptions, Toast  } from '@mobiscroll/react';
import PropTypes from 'prop-types';
import { useCallback, useEffect, useMemo, useRef, useState } from 'react';

setOptions({
  locale:  ,
  theme: ' ',
  themeVariant: ' '
});

function Installer(props) {
  const [draggable, setDraggable] = useState();

  const setDragElm = useCallback((elm) => {
    setDraggable(elm);
  }, []);

  const resource = props.data;

  return (
    <div className="mds-ext-res-item mbsc-font" ref={setDragElm}>
      <div className="mbsc-flex">
        <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
          {resource.name[0]}
        </div>
        <div className="mds-ext-res-cont">
          <div className="mds-ext-res-name">{resource.name}</div>
          <div className="mds-ext-res-title">{resource.title}</div>
        </div>
        <Draggable dragData={resource} element={draggable} type="resource" />
      </div>
    </div>
  );
}

Installer.propTypes = {
  data: PropTypes.object.isRequired,
};

function App() {
  const [dropCont, setDropCont] = useState();
  const [shouldScroll, setShouldScroll] = useState(0);
  const [toastMessage, setToastMessage] = useState('');
  const [isToastOpen, setToastOpen] = useState(false);

  const timelineRef = useRef();
  const installerRef = useRef();

  const [installers, setInstallers] = useState([
    {
      id: 'it-1',
      name: 'Installer team 1',
      eventCreation: false,
      reorder: false,
      children: [
        {
          id: 1,
          name: 'Emily Carter',
          color: '#007acc',
          title: 'Electrician',
        },
        {
          id: 2,
          name: 'Michael Lawson',
          color: '#008000',
          title: 'Plumber',
        },
      ],
    },
    // Showing partial data. Download full source.
  ]);

  const [availableInstallers, setAvailableInstallers] = useState([
    {
      id: 9,
      name: 'Adam Miller',
      color: '#C70039',
      title: 'Mason',
    },
    {
      id: 10,
      name: 'Isabella Martinez',
      color: '#2ECC71',
      title: 'Surveyor',
    },
    {
      id: 11,
      name: 'Mark White',
      color: '#34495E',
      title: 'Glazier',
    },
    // Showing partial data. Download full source.
  ]);

  const tasks = useMemo(
    () => [
      {
        id: 1,
        start: '2026-10-27T07:00',
        end: '2026-10-27T13:00',
        title: 'Electrical wiring installation',
        resource: 1,
      },
      {
        id: 2,
        start: '2026-10-27T14:00',
        end: '2026-10-27T18:00',
        title: 'Power panel connection',
        resource: 1,
      },
      {
        id: 3,
        start: '2026-10-27T08:00',
        end: '2026-10-27T12:00',
        title: 'Water pipe fitting',
        resource: 2,
      },
      // Showing partial data. Download full source.
    ],
    [],
  );

  const myView = useMemo(
    () => ({
      timeline: { type: 'day', resourceReorder: true, startTime: '07:00', endTime: '18:00' },
    }),
    [],
  );

  const setDropElm = useCallback((elm) => {
    setDropCont(elm);
  }, []);

  const handleResourceCreate = useCallback(
    (args) => {
      setAvailableInstallers(availableInstallers.filter((installer) => installer.id !== args.resource.id));
      setToastMessage(args.resource.name + ' added to ' + args.parent.name);
      setToastOpen(true);
    },
    [availableInstallers],
  );

  const handleResourceDelete = useCallback((args) => {
    setToastMessage(args.resource.name + ' removed from ' + args.parent.name);
    setToastOpen(true);
  }, []);

  const handleResourceOrderUpdate = useCallback((args) => {
    const parent = args.parent;
    const oldParent = args.oldParent;

    if (parent && oldParent) {
      setToastMessage(args.resource.name + ' moved to ' + args.parent.name);
      setToastOpen(true);
    }

    if (parent && parent.children) {
      // Remove placeholder resource
      parent.children = parent.children.filter((child) => !child.placeholder);
    }

    if (oldParent && !oldParent.children.length) {
      // Add placeholder resource
      oldParent.children.push({
        id: 'ph-' + oldParent.id,
        name: 'Drag Technicians here',
        placeholder: true,
        reorder: false,
      });
    }
  }, []);

  const handleItemDrop = useCallback(
    (args) => {
      if (args.data && args.dataType === 'resource') {
        setAvailableInstallers([...availableInstallers, args.data]);
        setShouldScroll((prevScroll) => prevScroll + 1);
      }
    },
    [availableInstallers],
  );

  const handleToastClose = useCallback(() => {
    setToastOpen(false);
  }, []);

  const addNewTeam = useCallback(() => {
    const teamLength = installers.length + 1;
    const resourceId = 'it-' + teamLength;
    setInstallers([
      ...installers,
      {
        id: resourceId,
        eventCreation: false,
        reorder: false,
        name: 'Installer team ' + teamLength,
        children: [
          {
            id: 'ph-' + resourceId,
            name: 'Drag Technicians here',
            placeholder: true,
            reorder: false,
          },
        ],
      },
    ]);

    if (timelineRef && timelineRef.current) {
      timelineRef.current.navigateToEvent({
        start: new Date(),
        resource: resourceId,
      });
    }
  }, [installers]);

  const customResourceHeader = useCallback(
    () => (
      <div className="mbsc-flex mbsc-align-items-center">
        <div className="mds-ext-res-header mbsc-flex-1-1">Set up teams</div>
        <Button variant="outline" className="mds-ext-res-add" onClick={addNewTeam}>
          Add team
        </Button>
      </div>
    ),
    [addNewTeam],
  );

  const customResource = useCallback(
    (resource) =>
      resource.isParent || resource.placeholder ? (
        <div className={'mds-ext-res-name' + (resource.placeholder ? ' mds-ext-res-name-ph' : '')}>{resource.name}</div>
      ) : (
        <div className="mbsc-flex">
          <div className="mds-ext-res-avatar" style={{ background: resource.color }}>
            {resource.name[0]}
          </div>
          <div className="mds-ext-res-cont">
            <div className="mds-ext-res-name">{resource.name}</div>
            <div className="mds-ext-res-title">{resource.title}</div>
          </div>
        </div>
      ),
    [],
  );

  useEffect(() => {
    if (shouldScroll && installerRef.current) {
      installerRef.current.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
    }
  }, [installerRef, shouldScroll]);

  return (
    <Page className="mds-ext-res-drop">
      <div className="mbsc-grid mbsc-no-padding">
        <div className="mbsc-row">
          <div className="mbsc-col-sm-3 mbsc-flex-col mds-ext-res-drop-cont" ref={setDropElm}>
            <Dropcontainer onItemDrop={handleItemDrop} element={dropCont}>
              <div className="mds-ext-res-header">Available technicians</div>
              <div className="mds-ext-res-list">
                {availableInstallers.map((installer) => (
                  <div key={installer.id} ref={installerRef}>
                    <Installer data={installer} />
                  </div>
                ))}
              </div>
            </Dropcontainer>
          </div>
          <div className="mbsc-col-sm-9 mds-ext-res-drop-calendar">
            <Eventcalendar
              ref={timelineRef}
              data={tasks}
              dragBetweenResources={false}
              externalResourceDrop={true}
              externalResourceDrag={true}
              resources={installers}
              view={myView}
              renderResourceHeader={customResourceHeader}
              renderResource={customResource}
              onResourceCreate={handleResourceCreate}
              onResourceDelete={handleResourceDelete}
              onResourceOrderUpdate={handleResourceOrderUpdate}
            />
            <Toast message={toastMessage} isOpen={isToastOpen} onClose={handleToastClose} />
          </div>
        </div>
      </div>
    </Page>
  );
}

export default App;

Duplicate Session

You have signed in on a different machine. To keep everything in sync, you have been signed out of this session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.

Duplicate Session

We noticed you are logged in on a different machine and have closed your old session.

If you need to invite team members, manage seats and accounts you can do it from the licenses page.