---
title: "Timeline Meal Planner Example | Mobiscroll"
component: "Timeline"
framework: "JavaScript"
library_version: "6.2.0"
url: "https://demo.mobiscroll.com/timeline/meal-planner"
---

# Meal Planner - JavaScript Timeline demo

Build a weekly meal planner with the JavaScript Timeline — meal types as color-coded resource rows, custom add/edit popover for creating or updating meals.

- [Live demo](https://demo.mobiscroll.com/timeline/meal-planner)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-javascript/tree/main/src/demos/eventcalendar/timeline/meal-planner)
- This demo in other frameworks: [jQuery](https://demo.mobiscroll.com/jquery/timeline/meal-planner.md), [Angular](https://demo.mobiscroll.com/angular/timeline/meal-planner.md), [React](https://demo.mobiscroll.com/react/timeline/meal-planner.md), [Vue](https://demo.mobiscroll.com/vue/timeline/meal-planner.md)

## Description

Use the [timeline summary mode/event listing](https://demo.mobiscroll.com/timeline/event-listing) to create a weekly meal planner. Meal types are represented as resources with a [custom template](https://demo.mobiscroll.com/scheduler/custom-resource-header-template). Events are the actual meals with custom properties, like calories or notes.

The add/edit form shows up in a custom popover that opens on double click for meal creation and on click for editing.

The popover header is customized with a custom header renderer function using the [`renderHeader`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderHeader) option.

By default the second dimension of the timeline (vertical axis) is reserved for resources, however it can be configured and used as "time slots" if the times are not relevant.

## What this demo shows

- Displays a weekly timeline-based meal planner where days are shown horizontally and meal types are shown as resource rows.
- **Resources** Each resource row represents a meal type, and each meal type has its own color so related events are easy to distinguish.
- **Events** Events represent the planned food item for a specific day and meal, using the resource color across the timeline cell.
- **Editing** Clicking an existing meal opens a popup with an edit form for updating the meal name, calories, and notes.
- **Deleting** The edit popup includes a delete action that removes the selected meal immediately, then shows a snackbar confirmation with an Undo action to restore it.
- **Creating** Double-clicking an empty timeline cell opens a popup for creating a new meal for the selected day and meal row.
- **Form behavior** The create popup lets users enter the meal name, calories, and notes before saving the new event.

## Best for

- **Meal planning** Weekly meal planning workflows for restaurants, schools, kindergartens, nursing homes, and similar institutions.
- **Day-based planning** Scenarios where items need to be planned by day without showing exact start and end times.
- **Resource-based organization** Setups where color-coded meal categories or resource rows make planned items easier to scan.
- **Direct calendar editing** Applications that need users to add, edit, delete, and restore planned items directly from the calendar UI.

## API used in this demo

- [`theme`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-themeVariant) - option
- [`setOptions`](https://mobiscroll.com/docs/javascript/popup/api#method-setOptions) - method
- [`renderHeader`](https://mobiscroll.com/docs/javascript/popup/api#renderer-renderHeader) - renderer
- [`buttons`](https://mobiscroll.com/docs/javascript/popup/api#opt-buttons) - option
- [`updateEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-updateEvent) - method
- [`close`](https://mobiscroll.com/docs/javascript/popup/api#method-close) - method
- [`cssClass`](https://mobiscroll.com/docs/javascript/popup/api#opt-cssClass) - option
- [`open`](https://mobiscroll.com/docs/javascript/popup/api#method-open) - method
- [`view`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-view) - option
- [`resources`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-resources) - option
- [`dragToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-dragToMove) - option
- [`clickToCreate`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-clickToCreate) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onEventCreated`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventCreated) - event
- [`onEventClick`](https://mobiscroll.com/docs/javascript/eventcalendar/api#event-onEventClick) - event
- [`isVisible`](https://mobiscroll.com/docs/javascript/popup/api#method-isVisible) - method
- [`renderResource`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderResource) - renderer
- [`renderTimelineEventContent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#renderer-renderTimelineEventContent) - renderer
- [`setEvents`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-setEvents) - method
- [`display`](https://mobiscroll.com/docs/javascript/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/javascript/popup/api#opt-fullScreen) - option
- [`onClose`](https://mobiscroll.com/docs/javascript/popup/api#event-onClose) - event
- [`removeEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-removeEvent) - method
- [`responsive`](https://mobiscroll.com/docs/javascript/popup/api#opt-responsive) - option
- [`width`](https://mobiscroll.com/docs/javascript/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/javascript/popup/api#opt-touchUi) - option
- [`showOverlay`](https://mobiscroll.com/docs/javascript/popup/api#opt-showOverlay) - option
- [`addEvent`](https://mobiscroll.com/docs/javascript/eventcalendar/api#method-addEvent) - method

## Code

### JS

```js
mobiscroll.setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

var calendar;
var popup;
var oldMeal;
var tempMeal;
var deleteMeal;
var restoreMeal;
var formatDate = mobiscroll.formatDate;
var nameInput = document.getElementById('meal-name-input');
var caloriesInput = document.getElementById('meal-calories-input');
var notesTextarea = document.getElementById('meal-notes-textarea');
var deleteButton = document.getElementById('meal-delete');
var typeCont = document.getElementById('meal-type-segmented');
var typesSegmented;

var types = [
  {
    id: 1,
    name: 'Breakfast',
    color: '#e20f0f',
    kcal: '300 - 400 kcal',
    icon: '🍳',
  },
  {
    id: 2,
    name: 'Elevenses',
    color: '#157d13',
    kcal: '100 - 200 kcal',
    icon: '🍌',
  },
  {
    id: 3,
    name: 'Lunch',
    color: '#32a6de',
    kcal: '500 - 700 kcal',
    icon: '🍜',
  },
  {
    id: 4,
    name: 'Dinner',
    color: '#e29d1d',
    kcal: '400 - 600 kcal',
    icon: '🥙',
  },
  {
    id: 5,
    name: 'Snack',
    color: '#68169c',
    kcal: '100 - 200 kcal',
    icon: '🥨',
  },
];

function addMealPopup() {
  // Hide delete button inside add popup
  deleteButton.style.display = 'none';

  deleteMeal = true;
  restoreMeal = false;

  // Set popup header text and buttons for adding
  popup.setOptions({
    renderHeader: function () {
      return (
        '<div class="md-meal-planner-header-primary">New meal</div>' +
        '<div class="md-meal-planner-header-date">' +
        formatDate('DDDD, DD MMMM YYYY', new Date(tempMeal.start)) +
        '</div>'
      );
    },
    buttons: [
      'cancel',
      {
        text: 'Add',
        keyCode: 'enter',
        handler: function () {
          calendar.updateEvent(tempMeal);

          deleteMeal = false;
          popup.close();
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
  });

  // Fill popup with a new event data
  mobiscroll.getInst(nameInput).value = tempMeal.title;
  mobiscroll.getInst(caloriesInput).value = '';
  mobiscroll.getInst(notesTextarea).value = '';

  typesSegmented.forEach(function (elm) {
    mobiscroll.getInst(elm).checked = +elm.value == tempMeal.resource;
  });

  popup.open();
}

function editMealPopup(args) {
  var ev = args.event;
  var resource = args.resourceObj;

  // Show delete button inside edit popup
  deleteButton.style.display = 'block';

  deleteMeal = false;
  restoreMeal = true;

  // Set popup header text and buttons for editing
  popup.setOptions({
    renderHeader: function () {
      return (
        '<div class="md-meal-planner-header-primary">' +
        resource.name +
        '</div>' +
        '<div class="md-meal-planner-header-date">' +
        formatDate('DDDD, DD MMMM YYYY', new Date(tempMeal.start)) +
        '</div>'
      );
    },
    buttons: [
      'cancel',
      {
        text: 'Save',
        keyCode: 'enter',
        handler: function () {
          // Update event with the new properties on save button click
          calendar.updateEvent({
            id: ev.id,
            title: tempMeal.title,
            calories: tempMeal.calories,
            notes: tempMeal.notes,
            start: ev.start,
            end: ev.end,
            resource: tempMeal.resource,
          });

          restoreMeal = false;
          popup.close();
        },
        cssClass: 'mbsc-popup-button-primary',
      },
    ],
  });

  // Fill popup with the selected event data
  mobiscroll.getInst(nameInput).value = ev.title || '';
  mobiscroll.getInst(caloriesInput).value = ev.calories || '';
  mobiscroll.getInst(notesTextarea).value = ev.notes || '';

  typesSegmented.forEach(function (elm) {
    mobiscroll.getInst(elm).checked = +elm.value == tempMeal.resource;
  });

  popup.open();
}

calendar = mobiscroll.eventcalendar('#demo-meal-planner', {
  view: {
    timeline: {
      type: 'week',
      eventDisplay: 'fill',
      startDay: 1,
      endDay: 5,
    },
  },
  resources: types,
  dragToCreate: false,
  dragToResize: false,
  dragToMove: true,
  clickToCreate: true,
  extendDefaultEvent: function () {
    return {
      title: 'New meal',
    };
  },
  onEventCreated: function (args) {
    // Store temporary event
    tempMeal = args.event;
    setTimeout(function () {
      addMealPopup();
    }, 100);
  },
  onEventClick: function (args) {
    oldMeal = Object.assign({}, args.event);
    tempMeal = args.event;

    if (!popup.isVisible()) {
      editMealPopup(args);
    }
  },
  renderResource: function (resource) {
    return (
      '<div class="md-meal-planner-cont">' +
      '<div class="md-meal-planner-title" style="color:' +
      resource.color +
      '">' +
      '<span class="md-meal-planner-icon">' +
      resource.icon +
      '</span>' +
      resource.name +
      '</div>' +
      '<div class="md-meal-planner-kcal">' +
      resource.kcal +
      '</div>' +
      '</div>'
    );
  },
  renderTimelineEventContent: function (args) {
    var event = args.original;
    return (
      '<div class="md-meal-planner-event">' +
      '<div class="md-meal-planner-event-title">' +
      event.title +
      '</div>' +
      (event.calories ? '<div class="md-meal-planner-event-desc">Calories: ' + event.calories + ' kcal</div>' : '') +
      '</div>'
    );
  },
});

mobiscroll.getJson(
  'https://trial.mobiscroll.com/meal-planner/',
  function (events) {
    calendar.setEvents(events);
  },
  'jsonp',
);

popup = mobiscroll.popup('#meal-planner-popup', {
  display: 'bottom',
  contentPadding: false,
  fullScreen: true,
  onClose: function () {
    if (deleteMeal) {
      calendar.removeEvent(tempMeal);
    } else if (restoreMeal) {
      calendar.updateEvent(oldMeal);
    }
  },
  responsive: {
    medium: {
      display: 'center',
      width: 400,
      fullScreen: false,
      touchUi: false,
      showOverlay: false,
    },
  },
});

function appendTypes() {
  var segmented = '<div mbsc-segmented-group>';

  for (var i = 0; i < types.length; ++i) {
    var type = types[i];
    segmented +=
      '<label>' +
      type.name +
      '<input type="radio" mbsc-segmented name="meal-planner-type" value="' +
      type.id +
      '" class="meal-planner-type"></label>';
  }

  segmented += '</div>';
  typeCont.innerHTML = segmented;
  mobiscroll.enhance(typeCont);
  typesSegmented = document.querySelectorAll('input[name=meal-planner-type]');

  typesSegmented.forEach(function (elm) {
    elm.addEventListener('change', function (ev) {
      tempMeal.resource = +ev.target.value;
    });
  });
}

appendTypes();

nameInput.addEventListener('change', function (ev) {
  tempMeal.title = ev.target.value;
});

caloriesInput.addEventListener('change', function (ev) {
  tempMeal.calories = ev.target.value;
});

notesTextarea.addEventListener('change', function (ev) {
  tempMeal.notes = ev.target.value;
});

deleteButton.addEventListener('click', function () {
  // Delete current event on button click
  calendar.removeEvent(tempMeal);
  popup.close();

  // Save a local reference to the deleted event
  var deletedMeal = tempMeal;

  mobiscroll.snackbar({
    button: {
      action: function () {
        calendar.addEvent(deletedMeal);
      },
      text: 'Undo',
    },
    message: 'Event deleted',
  });
});
```

### HTML

```html
<div id="demo-meal-planner" class="md-meal-planner-calendar"></div>

<div id="meal-planner-popup" class="md-meal-planner-popup">
    <div id="meal-type-segmented" class="mbsc-form-group"></div>
    <div class="mbsc-form-group">
        <label>
            Name
            <input mbsc-input id="meal-name-input" />
        </label>
        <label>
            Calories
            <input mbsc-input id="meal-calories-input" type="number" />
        </label>
        <label>
            Notes
            <textarea mbsc-textarea id="meal-notes-textarea"></textarea>
        </label>
    </div>
    <div class="mbsc-button-group">
        <button class="mbsc-button-block" id="meal-delete" mbsc-button data-color="danger" data-variant="outline">Delete meal</button>
    </div>
</div>
```

### CSS

```css
.md-meal-planner-header-primary {
    font-size: 16px;
    line-height: 20px;
}

.md-meal-planner-header-date {
    font-size: 14px;
    font-weight: 600;
    line-height: 20px;
    opacity: .6;
}

.md-meal-planner-cont {
    position: relative;
    padding-left: 34px;
    text-align: left;
}

.md-meal-planner-title {
    font-size: 20px;
}

.md-meal-planner-kcal {
    color: #929292;
}

.md-meal-planner-icon {
    position: absolute;
    left: 0;
}

.md-meal-planner-calendar.mbsc-calendar .mbsc-schedule-event-all-day-inner {
    height: 40px;
    display: flex;
    align-items: center;
}

.md-meal-planner-calendar .md-meal-planner-event-title {
  font-size: 12px;
  line-height: 18px;
}

.md-meal-planner-calendar .md-meal-planner-event-desc {
  opacity: 0.6;
  line-height: 18px;
}

.md-meal-planner-popup .mbsc-segmented-item:first-child .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:first-child .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:first-child .mbsc-segmented-selectbox-inner {
    background: #f08786;
}

.md-meal-planner-popup .mbsc-segmented-item:nth-child(2) .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(2) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(2) .mbsc-segmented-selectbox-inner {
    background: #8abe89;
}

.md-meal-planner-popup .mbsc-segmented-item:nth-child(3) .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(3) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(3) .mbsc-segmented-selectbox-inner {
    background: #99d3ef;
}

.md-meal-planner-popup .mbsc-segmented-item:nth-child(4) .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(4) .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:nth-child(4) .mbsc-segmented-selectbox-inner {
    background: #f0ce8e;
}

.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-selected .mbsc-button-bg.mbsc-material,
.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-selected.mbsc-windows,
.md-meal-planner-popup .mbsc-segmented-item:last-child .mbsc-segmented-selectbox-inner {
    background: #b48bce;
}
```

## Implementation notes

- Use the timeline view with `type: 'week'` and `eventDisplay: 'fill'` so each meal event fills the full timeline cell for its day.
- Define resources for each meal type (Breakfast, Elevenses, Lunch, Dinner, Snack) with a `color`, a recommended `kcal` range, and an emoji `icon`. Resources serve as the vertical axis; no time grid is shown.
- Use `renderResource` (Angular: `resourceTemplate`, Vue: `#resource` slot) to display the meal type icon, name (in the resource color), and recommended calorie range in each resource row header.
- Use `renderTimelineEventContent` (Angular: `timelineEventContentTemplate`, Vue: `#timelineEventContent` slot) to render a custom event body showing the meal title and calorie count inside the timeline cell.
- Enable `clickToCreate` so a single click on an empty cell creates a new meal. Set `dragToMove: true` to allow moving meals to different days or resource rows. Set `dragToCreate: false` and `dragToResize: false`.
- Use `extendDefaultEvent` to pre-populate newly created events with a default title and `allDay: true`.
- Use `onEventCreated` to open the add popup. Read `args.event` to set the popup header date and derive the meal type from the event's `resource`.
- Use `onEventClick` to open the edit popup. Read `args.resourceObj.name` and format `args.event.start` with `formatDate` to populate the popup header.
- Use `onEventDeleted` to handle keyboard-triggered deletions by calling the same delete helper as the Delete button in the popup.
- Build the popup with the `Popup` component; default to `display="bottom"` with `fullScreen: true` on mobile, and use the `responsive` option to switch to `display: 'center'` at the medium breakpoint.
- Render a custom two-line popup header via the Popup's `renderHeader` prop (Angular: `headerTemplate` input on `<mbsc-popup>`, Vue: `#header` slot on `<MbscPopup>`) - the Popup's own renderer, distinct from the calendar's header renderer: a primary line showing the meal type name or "New meal", and a secondary line with the formatted date.
- Include a `SegmentedGroup` with one `Segmented` per meal type so users can change the category from within the popup. On change, update the in-progress event's `resource` to the selected ID.
- Add `Input` fields for name and calories and a `Textarea` for notes. Show a "Save" button in edit mode and an "Add" button in create mode, both alongside a Cancel action.
- For deletion, clicking the Delete button in the edit popup removes the event from state and shows a `Snackbar` with an Undo action that restores the meal.
- On popup close when the add form was canceled, refresh the events list to remove the temporary placeholder event that was added to the timeline.

## Run it in your project

1. If not signed up yet, or don't own a license, you can [start a free trial](https://mobiscroll.com/starttrial).

2. Clone the repository:

   ```
   git clone https://github.com/acidb/mobiscroll-demos-javascript.git
   ```

3. If not installed yet, install the Mobiscroll CLI from NPM:

   ```
   npm install -g @mobiscroll/cli
   ```

4. Config Mobiscroll - open a terminal window in the root directory and run:

   ```
   mobiscroll config javascript
   ```

5. Install dependencies - open a terminal window in the root directory and run:

   ```
   npm install
   ```

6. Run the project:

   ```
   npm run dev
   ```

On a trial, add `--trial` to the config command: `mobiscroll config javascript --trial`.

Running a demo locally requires a Mobiscroll trial or a license; setup is per framework at https://mobiscroll.com/docs/javascript/getting-started/installation

## Related demos

- Previous: [Restaurant Shift Scheduling](https://demo.mobiscroll.com/timeline/restaurant-shift-management.md) - Build a restaurant shift planning with JavaScript Timeline — organize staff by role, track occupancy per shift, and toggle shifts on or off.
- Next: [Dynamic Resource Availability](https://demo.mobiscroll.com/timeline/dynamically-color-and-invalidate.md) - Color-code resource rows and restrict drop zones by task type on a JavaScript Timeline — compatible resources turn green, incompatible ones gray during drag.
