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

# Meal Planner - Angular Timeline demo

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

- [Live demo](https://demo.mobiscroll.com/angular/timeline/meal-planner)
- [View code on GitHub](https://github.com/acidb/mobiscroll-demos-angular/tree/main/src/demos/eventcalendar/timeline/meal-planner)
- This demo in other frameworks: [JavaScript](https://demo.mobiscroll.com/javascript/timeline/meal-planner.md), [jQuery](https://demo.mobiscroll.com/jquery/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/angular/timeline/event-listing) to create a weekly meal planner. Meal types are represented as resources with a [custom template](https://demo.mobiscroll.com/angular/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 template using the [`headerTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-headerTemplate) 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/angular/eventcalendar/api#opt-theme) - option
- [`themeVariant`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-themeVariant) - option
- [`cssClass`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-cssClass) - option
- [`display`](https://mobiscroll.com/docs/angular/popup/api#opt-display) - option
- [`fullScreen`](https://mobiscroll.com/docs/angular/popup/api#opt-fullScreen) - option
- [`onClose`](https://mobiscroll.com/docs/angular/popup/api#event-onClose) - event
- [`responsive`](https://mobiscroll.com/docs/angular/popup/api#opt-responsive) - option
- [`width`](https://mobiscroll.com/docs/angular/popup/api#opt-width) - option
- [`touchUi`](https://mobiscroll.com/docs/angular/popup/api#opt-touchUi) - option
- [`view`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-view) - option
- [`dragToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToCreate) - option
- [`dragToResize`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToResize) - option
- [`dragToMove`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-dragToMove) - option
- [`clickToCreate`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-clickToCreate) - option
- [`extendDefaultEvent`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-extendDefaultEvent) - option
- [`onEventClick`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventClick) - event
- [`onEventCreated`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventCreated) - event
- [`onEventDeleted`](https://mobiscroll.com/docs/angular/eventcalendar/api#event-onEventDeleted) - event
- [`data`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-data) - option
- [`resources`](https://mobiscroll.com/docs/angular/eventcalendar/api#opt-resources) - option
- [`resourceTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-resourceTemplate) - template
- [`timelineEventContentTemplate`](https://mobiscroll.com/docs/angular/eventcalendar/api#template-timelineEventContentTemplate) - template
- [`buttons`](https://mobiscroll.com/docs/angular/popup/api#opt-buttons) - option
- [`headerTemplate`](https://mobiscroll.com/docs/angular/popup/api#template-headerTemplate) - template

## Code

### Component

```ts
import { CommonModule } from '@angular/common';
import { HttpClient } from '@angular/common/http';
import { Component, OnInit, ViewChild } from '@angular/core';
import { FormsModule } from '@angular/forms';
import {
  formatDate,
  MbscCalendarEvent,
  MbscEventcalendarOptions,
  MbscModule,
  MbscPopup,
  MbscPopupOptions,
  Notifications,
  setOptions,
} from '@mobiscroll/angular';

setOptions({
  theme: 'ios',
  themeVariant: 'light'
});

@Component({
  selector: 'app-root', 
  templateUrl: './app.component.html',
  providers: [Notifications],
  standalone: true,
  imports: [CommonModule, FormsModule, MbscModule],
})
export class AppComponent implements OnInit {
  constructor(
    private http: HttpClient,
    private notify: Notifications,
  ) {}
  @ViewChild('popup', { static: false })
  popup!: MbscPopup;
  myMeals: MbscCalendarEvent[] = [];
  name = '';
  calories = '';
  notes = '';
  type = 1;
  tempMeal: any;
  headerPrimary = '';
  headerDate = '';
  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: '🥨',
    },
  ];
  popupAddButtons = [
    'cancel',
    {
      handler: () => {
        this.saveEvent();
      },
      keyCode: 'enter',
      text: 'Add',
      cssClass: 'mbsc-popup-button-primary',
    },
  ];
  popupEditButtons = [
    'cancel',
    {
      handler: () => {
        this.saveEvent();
      },
      keyCode: 'enter',
      text: 'Save',
      cssClass: 'mbsc-popup-button-primary',
    },
  ];
  popupButtons: any = [];
  popupOptions: MbscPopupOptions = {
    display: 'bottom',
    contentPadding: false,
    fullScreen: true,
    onClose: () => {
      if (!this.isEdit) {
        // Refresh the list, if add popup was canceled, to remove the temporary event
        this.myMeals = [...this.myMeals];
      }
    },
    responsive: {
      medium: {
        display: 'center',
        width: 400,
        fullScreen: false,
        touchUi: false,
      },
    },
  };
  isEdit = false;
  calendarOptions: MbscEventcalendarOptions = {
    view: {
      timeline: {
        type: 'week',
        eventDisplay: 'fill',
      },
    },
    dragToCreate: false,
    dragToResize: false,
    dragToMove: true,
    clickToCreate: true,
    extendDefaultEvent: () => ({
      title: 'New meal',
      allDay: true,
    }),
    onEventClick: (args: any) => {
      const event = args.event;
      const resource = args.resourceObj!;
      this.isEdit = true;
      this.tempMeal = event;
      // Fill popup form with event data
      this.loadPopupForm(event);
      // Set popup options
      this.popupButtons = this.popupEditButtons;
      this.headerPrimary = resource.name;
      this.headerDate = formatDate('DDDD, DD MMMM YYYY', new Date(event.start));
      this.type = +event.resource;
      // Open the popup
      this.popup.open();
    },
    onEventCreated: (args: any) => {
      const event = args.event;
      setTimeout(() => {
        this.isEdit = false;
        this.tempMeal = event;
        // Fill popup form with event data
        this.loadPopupForm(event);
        // Set popup options
        this.popupButtons = this.popupAddButtons;
        this.headerPrimary = 'New meal';
        this.headerDate = formatDate('DDDD, DD MMMM YYYY', new Date(this.tempMeal.start));
        this.type = +event.resource;
        // Open the popup
        this.popup.open();
      });
    },
    onEventDeleted: (args) => {
      setTimeout(() => {
        this.deleteMeal(args.event);
      });
    },
  };

  ngOnInit(): void {
    this.http.jsonp<MbscCalendarEvent[]>('https://trial.mobiscroll.com/meal-planner/', 'callback').subscribe((resp) => {
      this.myMeals = resp;
    });
  }
  loadPopupForm(event: any): void {
    this.name = event.title;
    this.calories = event.calories;
    this.notes = event.notes;
  }
  saveEvent(): void {
    this.tempMeal.title = this.name;
    this.tempMeal.calories = this.calories;
    this.tempMeal.notes = this.notes;
    this.tempMeal.allDay = true;
    this.tempMeal.resource = this.type;
    if (this.isEdit) {
      // Update the event in the list
      this.myMeals = [...this.myMeals];
    } else {
      // Add the new event to the list
      this.myMeals = [...this.myMeals, this.tempMeal];
    }
    // Close the popup
    this.popup.close();
  }
  deleteMeal(event: MbscCalendarEvent): void {
    this.myMeals = this.myMeals.filter((item) => item.id !== event.id);
    this.notify.snackbar({
      button: {
        action: () => {
          this.myMeals = [...this.myMeals, event];
        },
        text: 'Undo',
      },
      message: 'Meal deleted',
    });
  }
  onDeleteClick(): void {
    this.deleteMeal(this.tempMeal);
    this.popup.close();
  }
}
```

### Template

```html
<mbsc-eventcalendar
  [data]="myMeals"
  [resources]="types"
  [options]="calendarOptions"
  [resourceTemplate]="resourceTemp"
  [timelineEventContentTemplate]="eventTemp"
  class="md-meal-planner-calendar"
>
  <ng-template #resourceTemp let-resource>
    <div class="md-meal-planner-cont">
      <div class="md-meal-planner-title" [ngStyle]="{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>
  </ng-template>
  <ng-template #eventTemp let-args>
    <div class="md-meal-planner-event">
      <div class="md-meal-planner-event-title">{{args.original.title}}</div>
      <div *ngIf="args.original.calories" class="md-meal-planner-event-desc">Calories {{args.original.calories}} kcal</div>
    </div>
  </ng-template>
</mbsc-eventcalendar>

<mbsc-popup [options]="popupOptions" [buttons]="popupButtons" [headerTemplate]="headerTemp" #popup class="md-meal-planner-popup">
  <mbsc-segmented-group [(ngModel)]="type">
    <mbsc-segmented *ngFor="let t of types" [value]="t.id">{{t.name}}</mbsc-segmented>
  </mbsc-segmented-group>
  <div class="mbsc-form-group">
    <mbsc-input label="Name" [(ngModel)]="name"></mbsc-input>
    <mbsc-input label="Calories" [(ngModel)]="calories"></mbsc-input>
    <mbsc-textarea label="Notes" [(ngModel)]="notes"></mbsc-textarea>
  </div>
  <div *ngIf="isEdit" class="mbsc-button-group">
    <mbsc-button class="mbsc-button-block" color="danger" variant="outline" (click)="onDeleteClick()">Delete meal </mbsc-button>
  </div>
</mbsc-popup>

<ng-template #headerTemp>
  <div class="md-meal-planner-header-primary">{{ headerPrimary }}</div>
  <div class="md-meal-planner-header-date">{{ headerDate }}</div>
</ng-template>
```

### 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: 0.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-angular.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 angular
   ```

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

   ```
   npm install
   ```

6. Run the project:

   ```
   ng serve
   ```

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

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

## Related demos

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