Vue Timeline Meal Planner
Get started with this exampleUsage
theme: 'ios' You can turn auto theming on and let it decide based on the platform.
Usage
themeVariant: 'light' You can turn auto dark mode on based on system settings.
Usage
clickToCreate: true,
// click to create is on dragToCreate: true,
// drag to create is on dragToMove: true,
// reordering is on dragToResize: true,
// resizing is on eventDelete: true
// deleting is on Fine-tune drag & drop with these options, which are false by default.
Usage
locale: localeEn Selecting the locale loads the localized settings and the translated UI text.
Meal Planner
Use the timeline summary mode/event listing to create a weekly meal planner. Meal types are represented as resources with a custom 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 template using the header template slot.
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.
Demo

Code
View code on GitHub
<script setup>
import {
formatDate,
getJson,
MbscButton,
MbscEventcalendar,
MbscInput,
MbscPopup,
MbscSegmented,
MbscSegmentedGroup,
MbscSnackbar,
MbscTextarea,
setOptions
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const 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: '🥨'
}
]
const myView = {
timeline: {
type: 'week',
eventDisplay: 'fill'
}
}
const responsivePopup = {
medium: {
display: 'center',
width: 400,
fullScreen: false,
touchUi: false,
showOverlay: false
}
}
const popupAddButtons = [
'cancel',
{
handler: () => {
saveEvent()
},
keyCode: 'enter',
text: 'Add',
cssClass: 'mbsc-popup-button-primary'
}
]
const popupEditButtons = [
'cancel',
{
handler: () => {
saveEvent()
},
keyCode: 'enter',
text: 'Save',
cssClass: 'mbsc-popup-button-primary'
}
]
const myMeals = ref([])
const tempMeal = ref(null)
const isPopupOpen = ref(false)
const isEdit = ref(false)
const name = ref('')
const calories = ref('')
const notes = ref('')
const headerPrimary = ref('')
const headerDate = ref('')
const type = ref(1)
const popupButtons = ref([])
const isSnackbarOpen = ref(false)
const snackbarButton = ref({})
function myDefaultEvent() {
return {
title: 'New meal',
allDay: true
}
}
function saveEvent() {
tempMeal.value.title = name.value
tempMeal.value.calories = calories.value
tempMeal.value.notes = notes.value
tempMeal.value.allDay = true
tempMeal.value.resource = type.value
if (isEdit.value) {
// Update the event in the list
myMeals.value = [...myMeals.value]
} else {
// Add the new event to the list
myMeals.value = [...myMeals.value, tempMeal.value]
}
// Close the popup
isPopupOpen.value = false
}
function loadPopupForm(event) {
name.value = event.title
calories.value = event.calories
notes.value = event.notes
}
function deleteEvent(event) {
myMeals.value = myMeals.value.filter((item) => item.id !== event.id)
snackbarButton.value = {
action: () => {
myMeals.value = [...myMeals.value, event]
},
text: 'Undo'
}
isSnackbarOpen.value = true
}
function handleEventClick(args) {
const event = args.event
const resource = args.resourceObj
isEdit.value = true
tempMeal.value = event
// Fill popup form with event data
loadPopupForm(event)
// Set popup options
popupButtons.value = popupEditButtons
headerPrimary.value = resource.name
headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
type.value = event.resource
// Open the popup
isPopupOpen.value = true
}
function handleEventCreated(args) {
const event = args.event
isEdit.value = false
tempMeal.value = event
// Fill popup form with event data
loadPopupForm(event)
// Set popup options
popupButtons.value = popupAddButtons
headerPrimary.value = 'New meal'
headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
type.value = event.resource
// Open the popup
isPopupOpen.value = true
}
function handleEventDeleted(args) {
deleteEvent(args.event)
}
function handleDelete() {
deleteEvent(tempMeal)
isPopupOpen.value = false
}
function handlePopupClose() {
isPopupOpen.value = false
}
function handleSnackbarClose() {
isSnackbarOpen.value = false
}
onMounted(() => {
getJson(
'https://trial.mobiscroll.com/meal-planner/',
(events) => {
myMeals.value = events
},
'jsonp'
)
})
</script>
<template>
<MbscEventcalendar
cssClass="md-meal-planner-calendar"
:view="myView"
:data="myMeals"
:resources="types"
:dragToCreate="false"
:dragToResize="false"
:dragToMove="true"
:clickToCreate="true"
:extendDefaultEvent="myDefaultEvent"
@event-click="handleEventClick"
@event-created="handleEventCreated"
@event-deleted="handleEventDeleted"
>
<template #resource="resource">
<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>
</template>
<template #timelineEventContent="args">
<div class="md-meal-planner-event">
<div class="md-meal-planner-event-title">{{ args.original.title }}</div>
<div v-if="args.original.calories" class="md-meal-planner-event-desc">
Calories {{ args.original.calories }} kcal
</div>
</div>
</template>
</MbscEventcalendar>
<MbscPopup
cssClass="md-meal-planner-popup"
display="bottom"
:fullScreen="true"
:contentPadding="false"
:buttons="popupButtons"
:isOpen="isPopupOpen"
:responsive="responsivePopup"
@close="handlePopupClose"
>
<template #header>
<div class="md-meal-planner-header-primary">{{ headerPrimary }}</div>
<div class="md-meal-planner-header-date">{{ headerDate }}</div>
</template>
<MbscSegmentedGroup v-model="type">
<template v-for="t in types" :key="t.id">
<MbscSegmented :value="t.id">{{ t.name }}</MbscSegmented>
</template>
</MbscSegmentedGroup>
<div class="mbsc-form-group">
<MbscInput label="Name" v-model="name" />
<MbscInput label="Calories" v-model="calories" />
<MbscTextarea label="Notes" v-model="notes" />
</div>
<div v-if="isEdit" class="mbsc-button-group">
<MbscButton
className="mbsc-button-block"
color="danger"
variant="outline"
@click="handleDelete"
>
Delete meal
</MbscButton>
</div>
</MbscPopup>
<MbscSnackbar
message="Event deleted"
:button="snackbarButton"
:isOpen="isSnackbarOpen"
@close="handleSnackbarClose"
/>
</template>
<style>
.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;
}
</style> <script setup lang="ts">
import {
formatDate,
getJson,
MbscButton,
MbscEventcalendar,
MbscInput,
MbscPopup,
MbscSegmented,
MbscSegmentedGroup,
MbscSnackbar,
MbscTextarea,
setOptions
} from '@mobiscroll/vue'
import type {
MbscCalendarEvent,
MbscEventcalendarView,
MbscPopupOptions,
MbscResource,
MbscResponsiveOptions
} from '@mobiscroll/vue'
import { onMounted, ref } from 'vue'
setOptions({
locale: ,
theme: ' ',
themeVariant: ' '
})
const types: MbscResource[] = [
{
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: '🥨'
}
]
const myView: MbscEventcalendarView = {
timeline: {
type: 'week',
eventDisplay: 'fill'
}
}
const responsivePopup: MbscResponsiveOptions<MbscPopupOptions> = {
medium: {
display: 'center',
width: 400,
fullScreen: false,
touchUi: false,
showOverlay: false
}
}
const popupAddButtons = [
'cancel',
{
handler: () => {
saveEvent()
},
keyCode: 'enter',
text: 'Add',
cssClass: 'mbsc-popup-button-primary'
}
]
const popupEditButtons = [
'cancel',
{
handler: () => {
saveEvent()
},
keyCode: 'enter',
text: 'Save',
cssClass: 'mbsc-popup-button-primary'
}
]
const myMeals = ref<MbscCalendarEvent[]>([])
const tempMeal = ref<MbscCalendarEvent | null>(null)
const isPopupOpen = ref<boolean>(false)
const isEdit = ref<boolean>(false)
const name = ref<string>('')
const calories = ref<string>('')
const notes = ref<string>('')
const headerPrimary = ref<string>('')
const headerDate = ref<string>('')
const type = ref<number>(1)
const popupButtons = ref<any>([])
const isSnackbarOpen = ref<boolean>(false)
const snackbarButton = ref<any>({})
function myDefaultEvent() {
return {
title: 'New meal',
allDay: true
}
}
function saveEvent() {
tempMeal.value!.title = name.value
tempMeal.value!.calories = calories.value
tempMeal.value!.notes = notes.value
tempMeal.value!.allDay = true
tempMeal.value!.resource = type.value
if (isEdit.value) {
// Update the event in the list
myMeals.value = [...myMeals.value]
} else {
// Add the new event to the list
myMeals.value = [...myMeals.value, tempMeal.value!]
}
// Close the popup
isPopupOpen.value = false
}
function loadPopupForm(event: MbscCalendarEvent) {
name.value = event.title || ''
calories.value = event.calories
notes.value = event.notes
}
function deleteEvent(event: any) {
myMeals.value = myMeals.value.filter((item) => item.id !== event.id)
snackbarButton.value = {
action: () => {
myMeals.value = [...myMeals.value, event]
},
text: 'Undo'
}
isSnackbarOpen.value = true
}
function handleEventClick(args: any) {
const event = args.event
const resource = args.resourceObj
isEdit.value = true
tempMeal.value = event
// Fill popup form with event data
loadPopupForm(event)
// Set popup options
popupButtons.value = popupEditButtons
headerPrimary.value = resource.name
headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
type.value = event.resource
// Open the popup
isPopupOpen.value = true
}
function handleEventCreated(args: any) {
const event = args.event
isEdit.value = false
tempMeal.value = event
// Fill popup form with event data
loadPopupForm(event)
// Set popup options
popupButtons.value = popupAddButtons
headerPrimary.value = 'New meal'
headerDate.value = formatDate('DDDD, DD MMMM YYYY', new Date(event.start))
type.value = event.resource
// Open the popup
isPopupOpen.value = true
}
function handleEventDeleted(args: any) {
deleteEvent(args.event)
}
function handleDelete() {
deleteEvent(tempMeal)
isPopupOpen.value = false
}
function handlePopupClose() {
isPopupOpen.value = false
}
function handleSnackbarClose() {
isSnackbarOpen.value = false
}
onMounted(() => {
getJson(
'https://trial.mobiscroll.com/meal-planner/',
(events: MbscCalendarEvent[]) => {
myMeals.value = events
},
'jsonp'
)
})
</script>
<template>
<MbscEventcalendar
cssClass="md-meal-planner-calendar"
:view="myView"
:data="myMeals"
:resources="types"
:dragToCreate="false"
:dragToResize="false"
:dragToMove="true"
:clickToCreate="true"
:extendDefaultEvent="myDefaultEvent"
@event-click="handleEventClick"
@event-created="handleEventCreated"
@event-deleted="handleEventDeleted"
>
<template #resource="resource">
<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>
</template>
<template #timelineEventContent="args">
<div class="md-meal-planner-event">
<div class="md-meal-planner-event-title">{{ args.original.title }}</div>
<div v-if="args.original.calories" class="md-meal-planner-event-desc">
Calories {{ args.original.calories }} kcal
</div>
</div>
</template>
</MbscEventcalendar>
<MbscPopup
cssClass="md-meal-planner-popup"
display="bottom"
:fullScreen="true"
:contentPadding="false"
:buttons="popupButtons"
:isOpen="isPopupOpen"
:responsive="responsivePopup"
@close="handlePopupClose"
>
<template #header>
<div class="md-meal-planner-header-primary">{{ headerPrimary }}</div>
<div class="md-meal-planner-header-date">{{ headerDate }}</div>
</template>
<MbscSegmentedGroup v-model="type">
<template v-for="t in types" :key="t.id">
<MbscSegmented :value="t.id">{{ t.name }}</MbscSegmented>
</template>
</MbscSegmentedGroup>
<div class="mbsc-form-group">
<MbscInput label="Name" v-model="name" />
<MbscInput label="Calories" v-model="calories" />
<MbscTextarea label="Notes" v-model="notes" />
</div>
<div v-if="isEdit" class="mbsc-button-group">
<MbscButton
className="mbsc-button-block"
color="danger"
variant="outline"
@click="handleDelete"
>
Delete meal
</MbscButton>
</div>
</MbscPopup>
<MbscSnackbar
message="Event deleted"
:button="snackbarButton"
:isOpen="isSnackbarOpen"
@close="handleSnackbarClose"
/>
</template>
<style>
.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;
}
</style>