From d654e5e1b59d6051fdbb4ac318320274ffd2465f Mon Sep 17 00:00:00 2001 From: Max Morgan Date: Mon, 18 Mar 2024 14:45:20 -0400 Subject: [PATCH 1/4] Add support for radio filter --- src/components/organisms/Calendar/Calendar.js | 81 ++++++++++++++++++- src/stories/calendar.stories.js | 32 ++++++++ 2 files changed, 112 insertions(+), 1 deletion(-) diff --git a/src/components/organisms/Calendar/Calendar.js b/src/components/organisms/Calendar/Calendar.js index c4c58c5f..b6c7dc8e 100644 --- a/src/components/organisms/Calendar/Calendar.js +++ b/src/components/organisms/Calendar/Calendar.js @@ -9,7 +9,11 @@ import dayGridPlugin from '@fullcalendar/daygrid'; const template = document.createElement('template'); template.innerHTML = ` -
+
+
+
+
+
`; @@ -65,6 +69,7 @@ class Calendar extends HTMLElement { }, eventSources: [eventArraySource], }); + this.buildEventFilters(); this.calendar.render(); } @@ -120,6 +125,80 @@ 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'); + 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'); + const radioButtonInput = document.createElement('input'); + radioButtonInput.setAttribute('type', 'radio'); + radioButtonInput.setAttribute('id', value); + radioButtonInput.setAttribute('name', filter.key); + radioButtonInput.setAttribute('value', value); + radioButtonContainer.appendChild(radioButtonInput); + const radioButtonLabel = document.createElement('label'); + radioButtonLabel.setAttribute('for', value); + 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; + } + } + } + attributeChangedCallback(name, oldValue, newValue) { if (name in Calendar.observedAttributeCbs) { this.handleObservedAttribute( diff --git a/src/stories/calendar.stories.js b/src/stories/calendar.stories.js index 55c1b369..56677d4d 100644 --- a/src/stories/calendar.stories.js +++ b/src/stories/calendar.stories.js @@ -9,6 +9,22 @@ 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([ @@ -18,6 +34,19 @@ export default { allDay: true, }, ]), + eventFilters: JSON.stringify({ + dei_category_filter: { + type: 'radio', + legend: 'Select a location filter:', + key: 'dei_location', + values: [ + 'Say Detroit Play Center', + 'Senior Facility', + 'Dick & Sandy Boys and Girls Club', + 'Detroit Housing Commission', + ], + }, + }), }, }; @@ -25,6 +54,9 @@ export default { 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; }; From aa8479e66ec0b1c75d2c3c189b55acb426e6f1fe Mon Sep 17 00:00:00 2001 From: Max Morgan Date: Mon, 18 Mar 2024 15:27:58 -0400 Subject: [PATCH 2/4] Respond to user input --- src/components/organisms/Calendar/Calendar.js | 28 +++++++++++++++++++ src/stories/calendar.stories.js | 17 +++++++++-- 2 files changed, 43 insertions(+), 2 deletions(-) diff --git a/src/components/organisms/Calendar/Calendar.js b/src/components/organisms/Calendar/Calendar.js index b6c7dc8e..51153963 100644 --- a/src/components/organisms/Calendar/Calendar.js +++ b/src/components/organisms/Calendar/Calendar.js @@ -180,6 +180,11 @@ class Calendar extends HTMLElement { radioButtonInput.setAttribute('id', value); radioButtonInput.setAttribute('name', filter.key); radioButtonInput.setAttribute('value', value); + // 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); @@ -199,6 +204,29 @@ class Calendar extends HTMLElement { } } + /** + * 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( diff --git a/src/stories/calendar.stories.js b/src/stories/calendar.stories.js index 56677d4d..6d03b2c8 100644 --- a/src/stories/calendar.stories.js +++ b/src/stories/calendar.stories.js @@ -29,16 +29,29 @@ export default { 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: 'dei_location', + key: 'location', values: [ 'Say Detroit Play Center', 'Senior Facility', From 15df6b1f3465eb816b1ac0f51385cb489c8dc76c Mon Sep 17 00:00:00 2001 From: Max Morgan Date: Mon, 18 Mar 2024 15:42:33 -0400 Subject: [PATCH 3/4] Add styles --- src/components/organisms/Calendar/Calendar.js | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/components/organisms/Calendar/Calendar.js b/src/components/organisms/Calendar/Calendar.js index 51153963..ea4965bc 100644 --- a/src/components/organisms/Calendar/Calendar.js +++ b/src/components/organisms/Calendar/Calendar.js @@ -169,17 +169,25 @@ class Calendar extends HTMLElement { 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', @@ -188,6 +196,7 @@ class Calendar extends HTMLElement { 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); From 1808f45a4754dc9fdcd03de0395c351597a31358 Mon Sep 17 00:00:00 2001 From: Max Morgan Date: Mon, 18 Mar 2024 15:49:41 -0400 Subject: [PATCH 4/4] Height auto for responsive sizing --- src/components/organisms/Calendar/Calendar.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/organisms/Calendar/Calendar.js b/src/components/organisms/Calendar/Calendar.js index ea4965bc..2fee08cb 100644 --- a/src/components/organisms/Calendar/Calendar.js +++ b/src/components/organisms/Calendar/Calendar.js @@ -62,6 +62,7 @@ class Calendar extends HTMLElement { this.calendar = new FullCalendar(calendarEl, { plugins: [dayGridPlugin], initialView: 'dayGridMonth', + height: 'auto', headerToolbar: { left: 'title', center: '',