Date picker with blackout dates
This App Extension opens a popup calendar that prevents users from selecting blacked-out date ranges. Blackout dates are loaded dynamically from a separate Fulcrum app using LOADRECORDS, so administrators can manage blocked dates without touching the data event code.
Date picker with blackout dates
This example uses an App Extension to display a Flatpickr-powered calendar popup that restricts users from selecting certain dates. Blackout date ranges are stored in a separate Fulcrum app and loaded at runtime via LOADRECORDS, so administrators can add or remove blocked dates without updating the Data Event.
When the user clicks a button on the form, OPENEXTENSION launches the calendar. The selected date is returned to the form and written to a date field.
How it works
- A separate blackout dates app stores date range records (start date / end date).
- On
load-record, the Data Event fetches those ranges viaLOADRECORDSand stores them in a variable. - When the user clicks the calendar button,
OPENEXTENSIONopenscalendar_picker.html— a self-contained HTML page that uses Flatpickr to render the calendar. - The blackout ranges are passed to the HTML page via the
dataoption. Flatpickr disables those date ranges in the calendar. - When the user picks a date, the HTML page sends it back via
Fulcrum.finish(), and the Data Event writes it to an appointment date field.
Setup
- Create a blackout dates app with two Date fields:
start— the first day of the blocked rangeend— the last day of the blocked range (inclusive)
Note the app's Form ID and the field keys forstartandend.
- Upload
calendar_picker.html(below) as a Reference File in your Fulcrum org, or attach it directly to your app. - In your data collection app, add:
- A Date field for the appointment result (e.g. data name:
appointment_date) - A Button field to trigger the calendar (e.g. data name:
open_calendar)
- A Date field for the appointment result (e.g. data name:
- Add the Data Event code below to the app and update the configuration constants.
Note: The Flatpickr calendar requires an internet connection when using the CDN version below. For fully offline use, replace the CDN links with a locally hosted or embedded copy of Flatpickr.
Data Event Code
// ─── Configuration ───────────────────────────────────────────────────────────
// Form ID of the blackout dates app
const BLACKOUT_FORM_ID = 'YOUR-BLACKOUT-DATES-FORM-ID-HERE';
// Field key of the start date field in the blackout app
const START_DATE_KEY = 'YOUR-START-DATE-FIELD-KEY';
// Field key of the end date field in the blackout app
const END_DATE_KEY = 'YOUR-END-DATE-FIELD-KEY';
// Data name of the date field to write the selected appointment date to
const APPOINTMENT_FIELD = 'appointment_date';
// Data name of the button field that opens the calendar
const CALENDAR_BUTTON = 'open_calendar';
// ─── Load blackout ranges on record open ─────────────────────────────────────
let blackoutRanges = [];
ON('load-record', () => {
LOADRECORDS({ form_id: BLACKOUT_FORM_ID }, (err, result) => {
if (err) {
console.log('Error loading blackout dates:', INSPECT(err));
return;
}
// Build an array of { start, end } objects from the loaded records
blackoutRanges = (result.records || []).map(rec => ({
start: rec.form_values[START_DATE_KEY] || '',
end: rec.form_values[END_DATE_KEY] || ''
})).filter(range => range.start && range.end);
});
});
// ─── Open the calendar extension ─────────────────────────────────────────────
ON('click', CALENDAR_BUTTON, () => {
if (!blackoutRanges.length) {
// Allow the calendar to open even if no blackout dates loaded yet
console.log('No blackout ranges loaded; opening calendar without restrictions.');
}
OPENEXTENSION({
url: 'attachment://calendar_picker.html',
title: 'Select Appointment Date',
width: 400,
height: 500,
data: {
blackoutRanges: blackoutRanges,
today: (function (d) {
const year = d.getFullYear();
const month = String(d.getMonth() + 1).padStart(2, '0');
const day = String(d.getDate()).padStart(2, '0');
return year + '-' + month + '-' + day;
})(new Date())
},
onMessage: ({ data }) => {
if (data.selectedDate) {
SETVALUE(APPOINTMENT_FIELD, data.selectedDate);
}
}
});
});HTML Extension File (calendar_picker.html)
calendar_picker.html)Save the content below as calendar_picker.html and attach it to your Fulcrum app as a Reference File. This file is loaded inside the OPENEXTENSION popup.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Appointment Calendar</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Arial, sans-serif;
text-align: center;
padding: 20px;
background-color: #f9f9f9;
}
h2 { margin-bottom: 10px; }
#calendar { margin: 0 auto; max-width: 340px; }
.flatpickr-calendar { margin: 0 auto; font-size: 16px !important; }
button {
display: inline-block;
padding: 10px 18px;
margin-top: 20px;
font-size: 16px;
border-radius: 6px;
border: none;
background: #007aff;
color: white;
cursor: pointer;
}
button:hover { background: #005fcc; }
</style>
<!-- Flatpickr date picker library (requires internet connection) -->
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/flatpickr/dist/flatpickr.min.css">
<script src="https://cdn.jsdelivr.net/npm/flatpickr"></script>
<!-- Fulcrum App Extension communication bridge.
This is the standard bridge snippet from the App Extensions introduction
(https://docs.fulcrumapp.com/docs/app-extensions-introduction). It provides
Fulcrum.load() and Fulcrum.finish(). Copy the current version from that page. -->
<script>
(()=>{var s=(e,i)=>()=>(i||e((i={exports:{}}).exports,i),i.exports);
var o=s((a,r)=>{var l=new URLSearchParams(location.search);
function c(e){try{return JSON.parse(e)}catch(i){return null}}
r.exports=window.Fulcrum={isExtension:l.get("extension")==="1",
initialize:()=>{var i;let{params:e}=Fulcrum;Fulcrum.id=e?.id,Fulcrum.url=e?.url,Fulcrum.data=e?.data,Fulcrum.origin=e?.origin,(i=Fulcrum.onLoadOnce)?.call(Fulcrum)},
load:e=>{Fulcrum.onLoadOnce=()=>{Fulcrum.params&&!Fulcrum.isLoaded&&(Fulcrum.isLoaded=!0,e({data:Fulcrum.data}))},Fulcrum.onLoadOnce()},
send:(e,{close:i=!1}={})=>{var u;e=e||{};let n={id:Fulcrum.id,url:Fulcrum.url,data:e,close:i};
(u=window.webkit)?.messageHandlers?window.webkit.messageHandlers.extensionListener.postMessage(JSON.stringify(n)):
window.parent&&window.parent.postMessage({extensionMessage:n},Fulcrum.origin)},
receive:e=>{let i=c(e.data);i&&i.command==="initialize"&&!Fulcrum.params&&(Fulcrum.params=i.params,Fulcrum.initialize())},
finish:e=>{Fulcrum.send(e,{close:!0})}};Fulcrum.isExtension?
window.addEventListener("message",Fulcrum.receive,!1):window.addEventListener("DOMContentLoaded",Fulcrum.initialize)});o();})();
</script>
</head>
<body>
<h2>Select Appointment Date</h2>
<div id="calendar"></div>
<button id="cancel">Cancel</button>
<script>
Fulcrum.load(({ data }) => {
const blackoutRanges = data.blackoutRanges || [];
// Convert blackout ranges to the format Flatpickr expects for disabled dates
// Each range disables all dates from start through end (inclusive)
// Date strings are parsed as local dates (not UTC) so ranges line up with
// the calendar days the user sees. Flatpickr treats `to` as inclusive.
const parseLocalDate = (str) => {
const [y, m, d] = String(str).slice(0, 10).split('-').map(Number);
return new Date(y, m - 1, d);
};
const disabledDates = blackoutRanges.map(r => ({
from: parseLocalDate(r.start),
to: parseLocalDate(r.end)
}));
flatpickr('#calendar', {
inline: true,
minDate: data.today, // Prevent selecting dates in the past
disable: disabledDates, // Grey out the blackout date ranges
dateFormat: 'Y-m-d', // ISO format compatible with Fulcrum date fields
onChange: (selectedDates, dateStr) => {
if (selectedDates[0]) {
// dateStr is already formatted as YYYY-MM-DD in local time
// (avoid toISOString(), which converts to UTC and can shift the day)
Fulcrum.finish({ selectedDate: dateStr });
}
}
});
// Cancel button closes the extension without setting a date
document.getElementById('cancel').addEventListener('click', () => {
Fulcrum.finish({});
});
});
</script>
</body>
</html>Notes
- Dates are handled as local calendar days (
YYYY-MM-DD) end to end, so ranges and the selected date are not shifted by time zone. - The bridge
<script>in the HTML is the standard snippet from the App Extensions introduction. Copy the current version from there if it changes. - The blackout date ranges are fetched each time the record is opened. If the blackout app is updated while a user has the form open, they will need to close and reopen the record to get the latest dates.
- Flatpickr's
disableoption accepts an array of{ from, to }objects. Dates within those ranges are greyed out and unselectable in the calendar UI. - The
minDate: data.todaysetting prevents users from selecting any date in the past. - The
Fulcrum.finish({ selectedDate })call sends the result back to the Data Event'sonMessagehandler and closes the extension popup.
Updated about 12 hours ago