Live Updates

This sample updates a CollectionView while it is bound to FlexScheduler. Add, update, and remove events to see the scheduler refresh from live data changes.

FlexScheduler API Reference | SchedulerDefinition API Reference

import 'bootstrap.css'; import '@mescius/wijmo.styles/wijmo.css'; import './styles.css'; // import { CollectionView } from '@mescius/wijmo'; import * as wjcScheduler from '@mescius/wijmo.scheduler'; import { createDispatchEvent, getDisplayDate, getEvents, getTeams } 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', 'Ethan Cole': 'resources/avatars/ethan-cole.png', }; const statusLabels = { scheduled: 'Scheduled', enRoute: 'En route', onSite: 'On site', complete: 'Complete', }; const statusProgress = { scheduled: 10, enRoute: 35, onSite: 70, complete: 100, }; document.readyState === 'loading' ? document.addEventListener('DOMContentLoaded', init) : init(); function init() { const eventView = new CollectionView(getEvents()); const toggleButton = document.querySelector('#toggleLiveButton'); const nextButton = document.querySelector('#nextUpdateButton'); const indicator = document.querySelector('#liveIndicator'); const status = document.querySelector('#liveStatus'); const history = document.querySelector('#updateHistory'); const historyCount = document.querySelector('#historyCount'); let updateIndex = 0; let appliedUpdates = 0; let isLive = true; const scheduler = new wjcScheduler.FlexScheduler('#theScheduler', { itemsSource: eventView, resources: [ { fieldExpr: 'teamId', label: 'Team', dataSource: getTeams(), valueExpr: 'id', displayExpr: 'name', backgroundColorExpr: 'color', foreColorExpr: 'textColor', }, ], definition: getDayDefinition(renderEvent), displayDate: getDisplayDate(), workHours: [{ start: '08:00', end: '18:00' }], toolbarViewItems: [{ name: 'Day', key: wjcScheduler.SchedulerBuiltInViewItem.Day }], }); scheduler.itemChanging.addHandler((_sender, args) => initializeEventPriority(args)); const applyNextUpdate = () => { const items = eventView.sourceCollection; const item = items[updateIndex % items.length]; Object.assign(item, createDispatchEvent(item)); const statuses = ['scheduled', 'enRoute', 'onSite', 'complete']; const nextStatus = statuses[(statuses.indexOf(item.status) + 1) % statuses.length]; const previousStatus = item.status; eventView.editItem(item); item.status = nextStatus; item.progress = statusProgress[nextStatus]; eventView.commitEdit(); updateIndex++; appliedUpdates++; status.value = `${appliedUpdates} applied | ${item.subject}: ${statusLabels[nextStatus]}`; prependHistory(history, historyCount, item, previousStatus, nextStatus, appliedUpdates); }; const timer = window.setInterval(() => { if (isLive) { applyNextUpdate(); } }, 4500); toggleButton.addEventListener('click', () => { isLive = !isLive; indicator.classList.toggle('is-live', isLive); updateToggleButton(toggleButton, isLive); status.value = `${appliedUpdates} applied | Feed ${isLive ? 'running' : 'paused'}`; }); nextButton.addEventListener('click', applyNextUpdate); window.addEventListener('unload', () => window.clearInterval(timer), { once: true }); } function updateToggleButton(button, isLive) { const icon = document.createElement('i'); const label = document.createElement('span'); icon.className = `bi ${isLive ? 'bi-pause-fill' : 'bi-play-fill'}`; icon.setAttribute('aria-hidden', 'true'); label.textContent = isLive ? 'Pause' : 'Resume'; button.replaceChildren(icon, label); } function getDayDefinition(itemTemplate) { const definition = wjcScheduler.createSchedulerDefinitionPreset(wjcScheduler.SchedulerBuiltInViewItem.Day); return Object.assign(Object.assign({}, definition), { itemTemplate, rows: { bindings: [ { type: wjcScheduler.SchedulerAxisBindingType.Time, unit: wjcScheduler.SchedulerTimeUnit.Hour, visible: (args) => args.hour >= 8 && args.hour < 18, }, ], } }); } function renderEvent(ctx, element) { const item = ctx.item; Object.assign(item, createDispatchEvent(item)); const header = document.createElement('span'); const title = document.createElement('strong'); const ticket = document.createElement('span'); const state = document.createElement('span'); const details = document.createElement('span'); const technician = document.createElement('span'); const technicianImage = document.createElement('img'); const progress = document.createElement('span'); const progressValue = document.createElement('span'); header.className = 'dispatch-header'; title.textContent = item.subject; ticket.className = 'dispatch-ticket'; ticket.textContent = item.ticket; header.append(title, ticket); state.className = `dispatch-state state-${item.status}`; state.textContent = statusLabels[item.status]; details.className = 'dispatch-details'; technician.className = 'dispatch-technician'; technicianImage.src = avatarUrls[item.technician] || ''; technicianImage.alt = item.technician; technicianImage.addEventListener('error', () => technicianImage.remove(), { once: true }); technician.append(technicianImage, document.createTextNode(item.technician)); details.append(technician, document.createTextNode(` | ${item.location}`)); progress.className = 'dispatch-progress'; progressValue.style.width = `${item.progress}%`; progress.appendChild(progressValue); element.textContent = ''; element.dataset.priority = item.priority.toLocaleLowerCase(); element.append(header, state, details, progress); return null; } function initializeEventPriority(args) { if (args.request.action !== wjcScheduler.SchedulerItemChangeAction.Create || !args.request.values) return; Object.assign(args.request.values, createDispatchEvent(args.request.values)); } function prependHistory(history, count, item, previousStatus, nextStatus, updateNumber) { var _a, _b; const entry = document.createElement('li'); const marker = document.createElement('span'); const content = document.createElement('div'); const title = document.createElement('strong'); const transition = document.createElement('span'); const meta = document.createElement('small'); entry.className = `history-entry history-${nextStatus}`; marker.className = 'history-marker'; title.textContent = item.subject; transition.textContent = `${statusLabels[previousStatus]} -> ${statusLabels[nextStatus]}`; meta.textContent = `Update ${updateNumber} | ${item.ticket} | ${item.technician}`; content.append(title, transition, meta); entry.append(marker, content); (_a = history.querySelector('.empty-history')) === null || _a === void 0 ? void 0 : _a.remove(); history.prepend(entry); while (history.children.length > 10) (_b = history.lastElementChild) === null || _b === void 0 ? void 0 : _b.remove(); count.textContent = `${updateNumber} ${updateNumber === 1 ? 'change' : 'changes'}`; }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <title>MESCIUS Wijmo Scheduler Live Updates</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"> <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="live-toolbar"> <div class="live-heading"> <span id="liveIndicator" class="live-indicator is-live" aria-hidden="true"></span> <div> <strong>Operations feed</strong> <span>Simulated external changes are committed through CollectionView</span> </div> </div> <div class="live-actions" role="group" aria-label="Live update controls"> <button id="toggleLiveButton" class="btn btn-sm btn-outline-primary" type="button"> <i class="bi bi-pause-fill" aria-hidden="true"></i><span>Pause</span> </button> <button id="nextUpdateButton" class="btn btn-sm btn-primary" type="button"> <i class="bi bi-arrow-repeat" aria-hidden="true"></i><span>Apply next update</span> </button> </div> <output id="liveStatus" aria-live="polite">0 updates applied</output> </div> <div class="live-workspace"> <div id="theScheduler"></div> <aside class="update-panel" aria-label="Update history"> <div class="update-panel-header"> <div><i class="bi bi-activity" aria-hidden="true"></i><strong>Update history</strong></div> <span id="historyCount">0 changes</span> </div> <ol id="updateHistory" class="update-history" aria-live="polite"> <li class="empty-history">Updates will appear here as the external feed changes events.</li> </ol> </aside> </div> </main> </body> </html>
export const DefaultDispatchEvent = { status: 'scheduled', ticket: 'WO-1851', technician: 'Priya Shah', location: 'Harbor Office', progress: 0, priority: 'Standard', }; export function createDispatchEvent(values) { return Object.assign(Object.assign({}, values), { status: values.status || DefaultDispatchEvent.status, ticket: values.ticket || DefaultDispatchEvent.ticket, technician: values.technician || DefaultDispatchEvent.technician, location: values.location || DefaultDispatchEvent.location, progress: typeof values.progress === 'number' ? values.progress : DefaultDispatchEvent.progress, priority: values.priority || DefaultDispatchEvent.priority }); } function date(day, hour, minute = 0) { return new Date(2026, 4, day, hour, minute); } export function getDisplayDate() { return date(13, 9); } export function getTeams() { return [ { id: 'north', name: 'North team', color: '#2563eb', textColor: '#ffffff' }, { id: 'central', name: 'Central team', color: '#0f766e', textColor: '#ffffff' }, { id: 'south', name: 'South team', color: '#a3460a', textColor: '#ffffff' }, ]; } export function getEvents() { const events = [ { id: 1, subject: 'Router Installation', start: date(13, 8, 30), end: date(13, 10), teamId: 'north', status: 'enRoute', ticket: 'WO-1842', technician: 'Maya Chen', location: 'Riverside Branch', progress: 30, priority: 'Priority', }, { id: 2, subject: 'Fiber Inspection', start: date(13, 10), end: date(13, 11, 30), teamId: 'central', status: 'onSite', ticket: 'WO-1847', technician: 'Noah Reed', location: 'Market Street Hub', progress: 65, priority: 'Standard', }, { id: 3, subject: 'Branch Site Survey', start: date(13, 12), end: date(13, 13, 30), teamId: 'south', status: 'scheduled', ticket: 'WO-1851', technician: 'Priya Shah', location: 'Harbor Office', progress: 10, priority: 'Standard', }, { id: 4, subject: 'Access Point Upgrade', start: date(13, 14), end: date(13, 15, 30), teamId: 'north', status: 'scheduled', ticket: 'WO-1854', technician: 'Liam Brooks', location: 'North Campus', progress: 10, priority: 'Priority', }, { id: 5, subject: 'Customer Handoff', start: date(13, 15), end: date(13, 16, 30), teamId: 'central', status: 'scheduled', ticket: 'WO-1858', technician: 'Sofia Martinez', location: 'Central Plaza', progress: 10, priority: 'Standard', }, { id: 6, subject: 'Emergency Repair', start: date(13, 16), end: date(13, 18), teamId: 'south', status: 'scheduled', ticket: 'INC-932', technician: 'Ethan Cole', location: 'Airport Terminal', progress: 10, priority: 'Critical', }, ]; return events.map((event) => createDispatchEvent(event)); }
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; } .live-toolbar { display: flex; align-items: center; gap: 10px 18px; min-height: 56px; margin-bottom: 6px; padding: 6px 10px; border: 1px solid #dbe2ea; background: #f8fafc; } .live-heading { display: flex; align-items: center; gap: 9px; } .live-heading > div { display: flex; flex-direction: column; } .live-heading span:not(.live-indicator) { color: #64748b; font-size: 12px; } .live-indicator { width: 9px; height: 9px; border: 2px solid #94a3b8; border-radius: 50%; background: #ffffff; } .live-indicator.is-live { border-color: #16825d; background: #24a778; box-shadow: 0 0 0 3px rgb(36 167 120 / 14%); } .live-actions { display: flex; gap: 6px; margin-left: auto; } .live-actions .btn { display: inline-flex; align-items: center; gap: 6px; } #liveStatus { min-width: 230px; color: #475569; font-size: 13px; } .btn:focus-visible { border-color: #0f6cbd; box-shadow: 0 0 0 2px rgb(15 108 189 / 22%); } .dispatch-header, .dispatch-details { display: flex; overflow: hidden; align-items: center; gap: 5px; text-overflow: ellipsis; white-space: nowrap; } .dispatch-header strong { overflow: hidden; text-overflow: ellipsis; } .dispatch-ticket { margin-left: auto; font-size: 10px; opacity: .82; } .dispatch-state { display: inline-block; margin-top: 2px; padding: 1px 5px; border: 1px solid rgb(255 255 255 / 42%); border-radius: 8px; font-size: 11px; font-weight: 600; } .dispatch-details { margin-top: 2px; font-size: 10px; opacity: .9; } .dispatch-technician { display: inline-flex; align-items: center; gap: 4px; } .dispatch-technician img { width: 18px; height: 18px; border: 1px solid rgb(255 255 255 / 75%); border-radius: 50%; object-fit: cover; flex: 0 0 auto; } .dispatch-progress { display: block; overflow: hidden; height: 4px; margin-top: 4px; border-radius: 2px; background: rgb(255 255 255 / 28%); } .dispatch-progress > span { display: block; height: 100%; background: #ffffff; transition: width .25s ease; } .live-workspace { display: grid; grid-template-columns: minmax(0, 1fr) 300px; border: 1px solid #cbd5e1; } .update-panel { min-width: 0; max-height: calc(100vh - 64px); overflow: auto; border-left: 1px solid #cbd5e1; background: #f8fafc; } .update-panel-header { position: sticky; top: 0; z-index: 1; display: flex; min-height: 48px; align-items: center; justify-content: space-between; padding: 0 12px; border-bottom: 1px solid #dbe2ea; background: #eef3f8; } .update-panel-header > div { display: flex; align-items: center; gap: 7px; } .update-panel-header span { color: #64748b; font-size: 12px; } .update-history { margin: 0; padding: 0; list-style: none; } .empty-history { padding: 18px 12px; color: #64748b; font-size: 13px; line-height: 1.45; } .history-entry { display: grid; grid-template-columns: 10px minmax(0, 1fr); gap: 8px; padding: 11px 12px; border-bottom: 1px solid #e2e8f0; background: #ffffff; } .history-marker { width: 8px; height: 8px; margin-top: 5px; border-radius: 50%; background: #64748b; } .history-entry > div { display: flex; min-width: 0; flex-direction: column; gap: 2px; } .history-entry strong { overflow: hidden; color: #0f172a; font-size: 13px; text-overflow: ellipsis; white-space: nowrap; } .history-entry span:not(.history-marker) { color: #334155; font-size: 12px; font-weight: 600; } .history-entry small { color: #64748b; font-size: 11px; } .history-enRoute .history-marker { background: #2563eb; } .history-onSite .history-marker { background: #b45309; } .history-complete .history-marker { background: #16825d; } .history-scheduled .history-marker { background: #64748b; } #theScheduler { min-width: 0; height: calc(100vh - 66px); min-height: 600px; } @media (max-width: 760px) { .live-toolbar { align-items: flex-start; flex-wrap: wrap; } .live-heading { flex: 1 1 100%; } .live-actions { margin-left: 0; } #liveStatus { flex: 1 1 100%; min-width: 0; } #theScheduler { height: 680px; min-height: 680px; } .live-workspace { grid-template-columns: 1fr; } .update-panel { max-height: 340px; border-top: 1px solid #cbd5e1; border-left: 0; } } 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);