Item Templates

This sample customizes event rendering with the SchedulerDefinition itemTemplate callback. Switch between the default presentation and several templates while keeping the underlying event data unchanged.

FlexScheduler API Reference | SchedulerDefinition API Reference

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import './styles.css'; // import * as wjcCore from '@mescius/wijmo'; import * as wjcScheduler from '@mescius/wijmo.scheduler'; import { getAttendees, getDisplayDate, getMeetings, getRooms } from './data'; // const avatarUrls = { 'Maya Chen': 'resources/avatars/maya-chen.png', 'Noah Reed': 'resources/avatars/noah-reed.png', 'Priya Shah': 'resources/avatars/priya-shah.png', 'Liam Brooks': 'resources/avatars/liam-brooks.png', 'Sofia Martinez': 'resources/avatars/sofia-martinez.png', }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); function init() { let useCustomTemplate = true; const attendees = new Map(getAttendees().map((person) => [person.id, person])); const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', { itemsSource: new wjcCore.CollectionView(getMeetings()), resources: [ { fieldExpr: 'roomId', label: 'Room', dataSource: getRooms(), valueExpr: 'id', displayExpr: 'name', backgroundColorExpr: 'backgroundColor', foreColorExpr: 'foreColor', }, ], definition: createWeekDefinition((ctx, element) => renderEventItem(ctx, element, attendees)), displayDate: getDisplayDate(), toolbarViewItems: [{ name: 'Week', key: wjcScheduler.SchedulerBuiltInViewItem.Week }], }); document.querySelectorAll('[data-template]').forEach((button) => { button.addEventListener('click', () => { useCustomTemplate = button.dataset.template === 'custom'; updateItemTemplate(scheduler, useCustomTemplate ? (ctx, element) => renderEventItem(ctx, element, attendees) : null); setSelectedTemplate(useCustomTemplate); }); }); scheduler.hostElement.addEventListener('click', (event) => { const favoriteButton = event.target.closest('[data-favorite]'); if (!favoriteButton) { return; } event.preventDefault(); event.stopPropagation(); const hit = scheduler.hitTest(event); const item = hit.getHitInfo(); const meeting = item && item.item; if (meeting) { scheduler.update(meeting, { favorite: !meeting.favorite }); } }); } function updateItemTemplate(scheduler, itemTemplate) { if (scheduler.definition.itemTemplate !== itemTemplate) { scheduler.definition = Object.assign(Object.assign({}, scheduler.definition), { itemTemplate }); } } function createWeekDefinition(itemTemplate) { const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.Week); return Object.assign(Object.assign({}, definition), { itemTemplate, rows: { bindings: [ { type: wjcScheduler.SchedulerAxisBindingType.Time, unit: wjcScheduler.SchedulerTimeUnit.Hour, visible: (args) => args.hour >= 8 && args.hour < 17, }, ], } }); } function renderEventItem(ctx, element, attendees) { const item = ctx.item; const room = ctx.resources[0]; if (!item.status) { item.status = 'On track'; } if (item.progress === undefined) { item.progress = Math.floor(Math.random() * 101); } if (!item.attendeeIds) { item.attendeeIds = []; } const eventStatus = item.status; const eventProgress = item.progress; const header = createHeader(item); const meta = document.createElement('span'); const progress = document.createElement('span'); const progressValue = document.createElement('span'); const footer = document.createElement('span'); const details = document.createElement('span'); const avatars = document.createElement('span'); const status = document.createElement('span'); const progressText = document.createElement('span'); meta.className = 'event-meta'; meta.append(createMeta('bi-clock', formatTime(item)), createMeta('bi-geo-alt', room ? room.name : '')); progress.className = 'event-progress'; progressValue.style.width = `${eventProgress}%`; progress.appendChild(progressValue); footer.className = 'event-footer'; details.className = 'event-details'; status.className = `event-status status-${eventStatus.toLocaleLowerCase().replace(' ', '-')}`; status.textContent = eventStatus; progressText.className = 'event-progress-text'; progressText.textContent = `${eventProgress}%`; details.append(status, progressText); avatars.className = 'event-avatars'; item.attendeeIds.slice(0, 3).forEach((id) => { const person = attendees.get(id); const avatar = document.createElement('span'); avatar.className = 'event-avatar'; avatar.title = person.name; const image = document.createElement('img'); image.src = avatarUrls[person.name] || ''; image.alt = person.name; image.addEventListener('error', () => { avatar.textContent = person.initials; avatar.style.backgroundColor = person.color; }, { once: true }); avatar.appendChild(image); avatars.appendChild(avatar); }); footer.append(details, avatars); element.textContent = ''; element.dataset.status = eventStatus.toLocaleLowerCase().replace(' ', '-'); element.append(header, meta, progress, footer); return null; } function createHeader(item) { const header = document.createElement('span'); const icon = document.createElement('i'); const title = document.createElement('strong'); const favorite = document.createElement('button'); const type = document.createElement('span'); header.className = 'event-header'; type.className = 'event-type-icon'; icon.className = `bi ${getMeetingIcon(item.meetingType)}`; icon.setAttribute('aria-hidden', 'true'); type.title = item.meetingType; type.appendChild(icon); title.textContent = item.subject; favorite.type = 'button'; favorite.className = 'favorite-button'; favorite.dataset.favorite = ''; favorite.title = item.favorite ? 'Remove from favorites' : 'Add to favorites'; favorite.setAttribute('aria-label', favorite.title); const favoriteIcon = document.createElement('i'); favoriteIcon.className = `bi ${item.favorite ? 'bi-star-fill' : 'bi-star'}`; favoriteIcon.setAttribute('aria-hidden', 'true'); favorite.appendChild(favoriteIcon); if (item.priority === 'High') { const priority = document.createElement('i'); priority.className = 'bi bi-lightning-charge-fill event-priority'; priority.title = 'High priority'; priority.setAttribute('aria-label', 'High priority'); header.append(type, title, priority, favorite); } else { header.append(type, title, favorite); } return header; } function formatTime(item) { const options = { hour: 'numeric', minute: '2-digit' }; return `${item.start.toLocaleTimeString(undefined, options)}-${item.end.toLocaleTimeString(undefined, options)}`; } function createMeta(iconName, text) { const meta = document.createElement('span'); const icon = document.createElement('i'); icon.className = `bi ${iconName}`; icon.setAttribute('aria-hidden', 'true'); meta.append(icon, document.createTextNode(text)); return meta; } function getMeetingIcon(type) { switch (type) { case 'Design': return 'bi-bezier2'; case 'Customer': return 'bi-people'; case 'People': return 'bi-person-badge'; case 'Research': return 'bi-search'; case 'Release': return 'bi-rocket-takeoff'; default: return 'bi-calendar-event'; } } function setSelectedTemplate(useCustomTemplate) { document.querySelectorAll('[data-template]').forEach((button) => { const selected = (button.dataset.template === 'custom') === useCustomTemplate; button.setAttribute('aria-pressed', String(selected)); }); document.querySelector('#templateSummary').textContent = useCustomTemplate ? 'Status, priority, time, room, progress and participants' : 'Scheduler default item content'; }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Scheduler Item Templates</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons/font/bootstrap-icons.css" rel="stylesheet"> <!-- SystemJS --> <script src="https://cdn.jsdelivr.net/npm/systemjs@0.21.5/dist/system.src.js" integrity="sha512-skZbMyvYdNoZfLmiGn5ii6KmklM82rYX2uWctBhzaXPxJgiv4XBwJnFGr5k8s+6tE1pcR1nuTKghozJHyzMcoA==" crossorigin="anonymous"></script> <script src="systemjs.config.js"></script> <script> System.import('./src/app'); </script> </head> <body> <main class="container-fluid"> <div class="template-toolbar"> <span id="templateLabel"><i class="bi bi-layout-text-window" aria-hidden="true"></i> Event display</span> <div class="segmented-control" role="group" aria-labelledby="templateLabel"> <button type="button" data-template="default" aria-pressed="false">Default</button> <button type="button" data-template="custom" aria-pressed="true">Custom</button> </div> <span id="templateSummary" class="template-summary" aria-live="polite">Status, priority, time, room, progress and participants</span> </div> <div id="theScheduler"></div> </main> </body> </html>
function createDate(day, hour, minute = 0) { return new Date(2026, 4, day, hour, minute); } export function getDisplayDate() { return createDate(13, 9); } export function getRooms() { return [ { id: 'atlas', name: 'Atlas Room', backgroundColor: '#2563eb', foreColor: '#ffffff' }, { id: 'cedar', name: 'Cedar Room', backgroundColor: '#0f766e', foreColor: '#ffffff' }, { id: 'studio', name: 'Design Studio', backgroundColor: '#a21caf', foreColor: '#ffffff' }, { id: 'summit', name: 'Summit Room', backgroundColor: '#b45309', foreColor: '#ffffff' }, ]; } export function getAttendees() { return [ { id: 1, name: 'Maya Chen', initials: 'MC', color: '#1d4ed8' }, { id: 2, name: 'Noah Reed', initials: 'NR', color: '#047857' }, { id: 3, name: 'Priya Shah', initials: 'PS', color: '#7e22ce' }, { id: 4, name: 'Liam Brooks', initials: 'LB', color: '#b45309' }, { id: 5, name: 'Sofia Martinez', initials: 'SM', color: '#be185d' }, ]; } export function getMeetings() { return [ { id: 1, subject: 'Quarterly Planning', start: createDate(11, 9), end: createDate(11, 11), roomId: 'atlas', meetingType: 'Planning', attendeeIds: [1, 2, 3], progress: 35, favorite: true, status: 'On track', priority: 'High', }, { id: 2, subject: 'Homepage Critique', start: createDate(11, 13), end: createDate(11, 15, 30), roomId: 'studio', meetingType: 'Design', attendeeIds: [3, 5], progress: 70, favorite: false, status: 'On track', priority: 'Normal', }, { id: 3, subject: 'Customer Workshop', start: createDate(12, 9, 30), end: createDate(12, 11, 30), roomId: 'summit', meetingType: 'Customer', attendeeIds: [2, 4], progress: 50, favorite: false, status: 'At risk', priority: 'High', }, { id: 4, subject: 'Hiring Panel', start: createDate(12, 14), end: createDate(12, 15, 30), roomId: 'cedar', meetingType: 'People', attendeeIds: [1, 4, 5], progress: 80, favorite: true, status: 'On track', priority: 'Normal', }, { id: 5, subject: 'Product Sync', start: createDate(13, 9), end: createDate(13, 10, 30), roomId: 'atlas', meetingType: 'Product', attendeeIds: [1, 3], progress: 45, favorite: false, status: 'On track', priority: 'Normal', }, { id: 6, subject: 'Prototype Testing', start: createDate(13, 11), end: createDate(13, 13), roomId: 'studio', meetingType: 'Research', attendeeIds: [3, 5], progress: 60, favorite: false, status: 'Blocked', priority: 'High', }, { id: 7, subject: 'Partner Briefing', start: createDate(14, 10), end: createDate(14, 11, 30), roomId: 'summit', meetingType: 'Partner', attendeeIds: [2, 4], progress: 30, favorite: true, status: 'At risk', priority: 'High', }, { id: 8, subject: 'Engineering Retro', start: createDate(14, 14), end: createDate(14, 16), roomId: 'cedar', meetingType: 'Team', attendeeIds: [1, 2, 5], progress: 90, favorite: false, status: 'On track', priority: 'Normal', }, { id: 9, subject: 'Release Readiness', start: createDate(15, 9, 30), end: createDate(15, 11, 30), roomId: 'atlas', meetingType: 'Release', attendeeIds: [1, 2, 3], progress: 75, favorite: true, status: 'At risk', priority: 'High', }, { id: 10, subject: 'Brand Review', start: createDate(15, 13), end: createDate(15, 14, 30), roomId: 'studio', meetingType: 'Design', attendeeIds: [3, 5], progress: 55, favorite: false, status: 'On track', priority: 'Normal', }, ]; }
body { color: #1f2937; font-size: 15px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; user-select: none; margin: 0; } .container-fluid { box-sizing: border-box; margin: 0; padding: 5px; } .template-toolbar { display: flex; align-items: center; gap: 10px; margin-bottom: 6px; min-height: 46px; padding: 5px 9px; border: 1px solid #dbe2ea; background: #f8fafc; color: #334155; } #templateLabel { display: inline-flex; align-items: center; gap: 6px; font-weight: 600; } .segmented-control { display: inline-flex; overflow: hidden; border: 1px solid #64748b; border-radius: 4px; } .segmented-control button { min-width: 76px; min-height: 32px; padding: 4px 12px; border: 0; border-right: 1px solid #64748b; background: #ffffff; color: #334155; font: inherit; } .segmented-control button:last-child { border-right: 0; } .segmented-control button[aria-pressed="true"] { background: #0f6cbd; color: #ffffff; } .segmented-control button:focus-visible { outline: 2px solid #0f6cbd; outline-offset: -3px; } .template-summary { color: #526170; font-size: 13px; } .event-header, .event-footer, .event-details, .event-meta { display: flex; align-items: center; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } .event-header { gap: 5px; min-width: 0; } .event-type-icon { display: inline-grid; width: 24px; height: 24px; place-items: center; border: 1px solid rgb(255 255 255 / 40%); border-radius: 4px; background: rgb(255 255 255 / 16%); flex: 0 0 auto; } .event-priority { color: #fde68a; filter: drop-shadow(0 1px 1px rgb(15 23 42 / 30%)); } .event-header strong { overflow: hidden; min-width: 0; font-size: 13px; line-height: 1.25; text-overflow: ellipsis; } .event-header > .bi { flex: 0 0 auto; } .favorite-button { display: inline-grid; place-items: center; width: 24px; height: 24px; margin-left: auto; padding: 0; border: 0; border-radius: 3px; background: transparent; color: inherit; flex: 0 0 auto; } .favorite-button:hover { background: rgb(255 255 255 / 18%); } .favorite-button:focus-visible { outline: 2px solid currentColor; outline-offset: -2px; } .event-progress { display: block; overflow: hidden; height: 4px; margin: 4px 0; border-radius: 2px; background: rgb(255 255 255 / 26%); } .event-progress > span { display: block; height: 100%; border-radius: inherit; background: rgb(255 255 255 / 85%); } .event-footer { justify-content: space-between; gap: 5px; } .event-meta { gap: 8px; margin-top: 3px; font-size: 11px; opacity: .9; } .event-meta span { display: inline-flex; min-width: 0; align-items: center; gap: 3px; overflow: hidden; text-overflow: ellipsis; } .event-details { gap: 3px; min-width: 0; font-size: 13px; opacity: 0.92; } .event-status { padding: 1px 5px; border: 1px solid rgb(255 255 255 / 40%); border-radius: 8px; background: rgb(15 23 42 / 20%); font-size: 10px; font-weight: 700; } .status-at-risk { background: rgb(146 64 14 / 52%); } .status-blocked { background: rgb(153 27 27 / 58%); } .event-progress-text { font-size: 11px; font-weight: 700; } .event-avatars { display: flex; padding-left: 5px; flex: 0 0 auto; } .event-avatar { display: inline-grid; place-items: center; width: 23px; height: 23px; margin-left: -5px; border: 2px solid rgb(255 255 255 / 75%); border-radius: 50%; box-shadow: 0 1px 2px rgb(15 23 42 / 24%); color: #ffffff; font-size: 9px; font-weight: 700; pointer-events: none; } .event-avatar img { width: 100%; height: 100%; border-radius: 50%; object-fit: cover; } #theScheduler { height: 710px; min-height: 560px; } @media (max-width: 640px) { .template-toolbar { flex-wrap: wrap; } .template-summary { flex: 1 1 100%; } .segmented-control button { min-width: 70px; padding-right: 9px; padding-left: 9px; } #theScheduler { height: 760px; min-height: 760px; } } input, textarea, [contenteditable="true"] { user-select: text; }
(function (global) { System.config({ transpiler: 'plugin-babel', babelOptions: { es2015: true, }, meta: { '*.css': { loader: 'css' }, }, paths: { // paths serve as alias 'npm:': 'node_modules/', 'cdn:': 'https://cdn.mescius.io/demoapps/packages/wijmojs/5.20262.60-rc/', 'pcdn:': 'https://cdn.jsdelivr.net/npm/', }, // map tells the System loader where to look for things map: { '@mescius/wijmo': 'cdn:@mescius/wijmo/index.js', '@mescius/wijmo.input': 'cdn:@mescius/wijmo.input/index.js', '@mescius/wijmo.scheduler': 'cdn:@mescius/wijmo.scheduler/index.js', '@mescius/wijmo.styles': 'cdn:@mescius/wijmo.styles', '@mescius/wijmo.cultures': 'cdn:@mescius/wijmo.cultures', jszip: 'pcdn:jszip@3.8.0/dist/jszip.js', 'bootstrap.css': 'pcdn:bootstrap@5.3.8/dist/css/bootstrap.min.css', css: 'pcdn:systemjs-plugin-css@0.1.37/css.js', 'plugin-babel': 'pcdn:systemjs-plugin-babel@0.0.25/plugin-babel.js', 'systemjs-babel-build': 'pcdn:systemjs-plugin-babel@0.0.25/systemjs-babel-browser.js', }, packages: { src: { defaultExtension: 'js', }, node_modules: { defaultExtension: 'js', }, }, }); })(this);