Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support event filters in calendar component #186

Merged
merged 4 commits into from
Mar 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
119 changes: 118 additions & 1 deletion src/components/organisms/Calendar/Calendar.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ import dayGridPlugin from '@fullcalendar/daygrid';

const template = document.createElement('template');
template.innerHTML = `
<div id="calendar">
<div>
<div id="calendarFilters">
</div>
<div id="calendar">
</div>
</div>
`;

Expand Down Expand Up @@ -58,13 +62,15 @@ class Calendar extends HTMLElement {
this.calendar = new FullCalendar(calendarEl, {
plugins: [dayGridPlugin],
initialView: 'dayGridMonth',
height: 'auto',
headerToolbar: {
left: 'title',
center: '',
right: 'prev,next today',
},
eventSources: [eventArraySource],
});
this.buildEventFilters();
this.calendar.render();
}

Expand Down Expand Up @@ -120,6 +126,117 @@ class Calendar extends HTMLElement {
return this.shadowRoot.getElementById('calendar').childElementCount > 0;
}

/**
* Creates filter form elements above the calendar based on the event filters provided.
*
* @param {string} [newFiltersJSON=null] - A JSON serialized array of event filter definitions.
* If null, filters will be fetched from the 'event-filters' attribute on the
* component instead.
* @returns void
*/
buildEventFilters(newFiltersJSON = null) {
const calendarFilterElt = this.shadowRoot.getElementById('calendarFilters');
if (calendarFilterElt === null) {
return;
}

if (newFiltersJSON === null) {
newFiltersJSON = this.getAttribute('event-filters');
}

let filters = {};
try {
filters = JSON.parse(newFiltersJSON ?? '{}');
} catch (error) {
// TODO: Introduce proper error logging.
// eslint-disable-next-line no-console
console.error(`Failed to parse list of filters:\n${newFiltersJSON}`);
}
for (const filter in filters) {
this.buildEventFilter(calendarFilterElt, filters[filter]);
}
}

/**
* Creates a single filter form field set above the calendar based
* on the event filter provided.
*
* @param {HTMLElement} calendarFilterElt - An HTML element to be used
* as the container for the event filter created.
* @param {Object} filter - A single event filter object.
* @returns void
*/
buildEventFilter(calendarFilterElt, filter) {
switch (filter.type) {
case 'radio': {
const radioFiltersContainer = document.createElement('fieldset');
radioFiltersContainer.classList.add(
'd-flex',
'flex-wrap',
'm-3',
'justify-content-center',
);
const legend = document.createElement('legend');
legend.classList.add('visually-hidden');
legend.innerText = filter.legend;
radioFiltersContainer.appendChild(legend);
filter.values.forEach((value) => {
const radioButtonContainer = document.createElement('div');
radioButtonContainer.classList.add('m-2');
const radioButtonInput = document.createElement('input');
radioButtonInput.setAttribute('type', 'radio');
radioButtonInput.setAttribute('id', value);
radioButtonInput.setAttribute('name', filter.key);
radioButtonInput.setAttribute('value', value);
radioButtonInput.classList.add('btn-check');
// Bind event handler to this instance.
radioButtonInput.addEventListener(
'click',
this.filterEvents.bind(this),
);
radioButtonContainer.appendChild(radioButtonInput);
const radioButtonLabel = document.createElement('label');
radioButtonLabel.setAttribute('for', value);
radioButtonLabel.classList.add('btn', 'btn-primary');
radioButtonLabel.innerText = value;
radioButtonContainer.appendChild(radioButtonLabel);
radioFiltersContainer.appendChild(radioButtonContainer);
});
calendarFilterElt.appendChild(radioFiltersContainer);
break;
}
default: {
// TODO: Introduce proper error logging.
// eslint-disable-next-line no-console
console.warn(`Unsupported event filter type provided: ${filter.type}`);
return;
}
}
}

/**
* Handles filter element events by filter down events to the
* user-selected criteria.
*
* @param {Event} browserEvent - The browser event triggered on the filter
* form element.
*/
filterEvents(browserEvent) {
const inputKey = browserEvent.target.name;
const inputValue = browserEvent.target.value;
const currentEventsJSON = this.getAttribute('events');
let events = [];
try {
events = JSON.parse(currentEventsJSON ?? '[]');
} catch (error) {
// TODO: Introduce proper error logging.
// eslint-disable-next-line no-console
console.error(`Failed to parse list of events:\n${currentEventsJSON}`);
}
events = events.filter((calEvent) => calEvent[inputKey] === inputValue);
this.updateEventArraySource(JSON.stringify(events));
}

attributeChangedCallback(name, oldValue, newValue) {
if (name in Calendar.observedAttributeCbs) {
this.handleObservedAttribute(
Expand Down
47 changes: 46 additions & 1 deletion src/stories/calendar.stories.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,22 +9,67 @@ export default {
'A JSON array of events conforming to the FullCalenar \
event model. See: https://fullcalendar.io/docs/event-model.',
},
eventFilters: {
control: { type: 'text' },
description: `A JSON object of event filters to be applied. The filters
should take the following form:
{
"filter_name": {
type: "ui_filter_type",
legend: "A helpful legend.",
key: "event_field_key",
values: [
"event_field_key_value1",
"event_field_key_value2"
],
}
}`,
},
},
args: {
events: JSON.stringify([
{
title: 'event1',
title: 'event @ Say Detroit Play Center',
start: new Date().toISOString(),
allDay: true,
location: 'Say Detroit Play Center',
},
{
title: 'event @ Senior Facility',
start: new Date().toISOString(),
allDay: true,
location: 'Senior Facility',
},
{
title: 'event @ Detroit Housing Commission',
start: new Date().toISOString(),
allDay: true,
location: 'Detroit Housing Commission',
},
]),
eventFilters: JSON.stringify({
dei_category_filter: {
type: 'radio',
legend: 'Select a location filter:',
key: 'location',
values: [
'Say Detroit Play Center',
'Senior Facility',
'Dick & Sandy Boys and Girls Club',
'Detroit Housing Commission',
],
},
}),
},
};

// Template
const Template = (args) => {
const calendarElt = document.createElement('cod-calendar');
calendarElt.setAttribute('events', args.events);
if (args.eventFilters) {
calendarElt.setAttribute('event-filters', args.eventFilters);
}
return calendarElt;
};

Expand Down
Loading