From fde91e090de6a792e43a5001b568873db868d231 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 13 Jan 2025 13:27:02 -0800 Subject: [PATCH 01/16] extracts l10n imports from task. both files are 4k large uncompressed combined, gzip compressed it's a little more than 1k. extracting these imports removes async/await altogether from the task that sets up the picker. that's a worthwhile tradeoff imo. --- packages/ilios-common/addon/components/date-picker.js | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index cc63ff7f76..29a52c9cf4 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -3,6 +3,8 @@ import { service } from '@ember/service'; import { dropTask, restartableTask, waitForProperty } from 'ember-concurrency'; import { tracked } from '@glimmer/tracking'; import flatpickr from 'flatpickr'; +import { French } from 'flatpickr/dist/l10n/fr.js'; +import { Spanish } from 'flatpickr/dist/l10n/es.js'; import { later, next } from '@ember/runloop'; import { isTesting } from '@embroider/macros'; @@ -19,18 +21,14 @@ export default class DatePickerComponent extends Component { } }); - setupPicker = dropTask(async (element) => { + setupPicker = dropTask((element) => { const currentLocale = this.intl.primaryLocale; let locale; switch (currentLocale) { case 'fr': - // eslint-disable-next-line no-case-declarations - const { French } = await import('flatpickr/dist/l10n/fr.js'); locale = French; break; case 'es': - // eslint-disable-next-line no-case-declarations - const { Spanish } = await import('flatpickr/dist/l10n/es.js'); locale = Spanish; break; default: From 1261a663a4c5c097624ade527944bb9ce01a65b2 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 13 Jan 2025 14:24:57 -0800 Subject: [PATCH 02/16] replaces ember-render-modifier with custom modifier. --- .../addon/components/date-picker.hbs | 5 ++- .../addon/components/date-picker.js | 31 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index 7230d46242..eafa1222dd 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,7 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{did-insert (perform this.setupPicker)}} - {{did-update (perform this.updatePicker) @value @maxDate @minDate}} + {{this.picker @value @minDate @maxDate}} ...attributes -/> +/> \ No newline at end of file diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index 29a52c9cf4..b10235f055 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -1,7 +1,7 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import { dropTask, restartableTask, waitForProperty } from 'ember-concurrency'; import { tracked } from '@glimmer/tracking'; +import { modifier } from 'ember-modifier'; import flatpickr from 'flatpickr'; import { French } from 'flatpickr/dist/l10n/fr.js'; import { Spanish } from 'flatpickr/dist/l10n/es.js'; @@ -10,18 +10,25 @@ import { isTesting } from '@embroider/macros'; export default class DatePickerComponent extends Component { @service intl; - - @tracked _flatPickerInstance; @tracked isOpen = false; + _flatPickerInstance; - updatePicker = restartableTask(async (element, [value]) => { - await waitForProperty(this, '_flatPickerInstance'); - if (this._flatPickerInstance.selectedDates[0] != value) { + picker = modifier((element, [value, minDate, maxDate]) => { + if (!this._flatPickerInstance) { + this._flatPickerInstance = this.initPicker(element, value, minDate, maxDate); + } + if (this._flatPickerInstance.selectedDates[0] !== value) { this._flatPickerInstance.setDate(value); } + if (this._flatPickerInstance.minDate !== minDate) { + this._flatPickerInstance.set('minDate', minDate); + } + if (this._flatPickerInstance.maxDate !== maxDate) { + this._flatPickerInstance.set('maxDate', maxDate); + } }); - setupPicker = dropTask((element) => { + initPicker(element, value, minDate, maxDate) { const currentLocale = this.intl.primaryLocale; let locale; switch (currentLocale) { @@ -34,9 +41,9 @@ export default class DatePickerComponent extends Component { default: locale = 'en'; } - this._flatPickerInstance = flatpickr(element, { + return flatpickr(element, { locale, - defaultDate: this.args.value, + defaultDate: value, formatDate: (dateObj) => this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), onChange: (selectedDates) => this.onChange(selectedDates[0]), @@ -49,11 +56,11 @@ export default class DatePickerComponent extends Component { onClose: () => { this.isOpen = false; }, - maxDate: this.args.maxDate ?? null, - minDate: this.args.minDate ?? null, + maxDate: maxDate ?? null, + minDate: minDate ?? null, disableMobile: isTesting(), }); - }); + } willDestroy() { super.willDestroy(...arguments); From 650b45a5c4f81863545327359ac3389268e3a957 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 13 Jan 2025 14:47:21 -0800 Subject: [PATCH 03/16] corrects localization updates. in the current version, changes to the locale are not registered by the picker - we'll remain in English. declaring the locale as input allows us to be responsive here. --- .../addon/components/date-picker.hbs | 2 +- .../addon/components/date-picker.js | 25 +++++++++++-------- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index eafa1222dd..ee38343bd0 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,6 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{this.picker @value @minDate @maxDate}} + {{this.picker @value @minDate @maxDate @locale}} ...attributes /> \ No newline at end of file diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index b10235f055..efdabb7aea 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -13,7 +13,7 @@ export default class DatePickerComponent extends Component { @tracked isOpen = false; _flatPickerInstance; - picker = modifier((element, [value, minDate, maxDate]) => { + picker = modifier((element, [value, minDate, maxDate, localeIdentifier]) => { if (!this._flatPickerInstance) { this._flatPickerInstance = this.initPicker(element, value, minDate, maxDate); } @@ -26,21 +26,26 @@ export default class DatePickerComponent extends Component { if (this._flatPickerInstance.maxDate !== maxDate) { this._flatPickerInstance.set('maxDate', maxDate); } + const locale = this.getLocale(localeIdentifier); + if (this._flatPickerInstance.l10n !== locale) { + this._flatPickerInstance.set('locale', locale); + } }); - initPicker(element, value, minDate, maxDate) { - const currentLocale = this.intl.primaryLocale; - let locale; - switch (currentLocale) { + getLocale(localeIdentifier) { + //console.log(French); + switch (localeIdentifier) { case 'fr': - locale = French; - break; + return French; case 'es': - locale = Spanish; - break; + return Spanish; default: - locale = 'en'; + return 'en'; } + } + + initPicker(element, value, minDate, maxDate, localeIdentifier) { + const locale = this.getLocale(localeIdentifier); return flatpickr(element, { locale, defaultDate: value, From 6604fa5f58b4dc909cf95272fb461489b3fc1eea Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 13 Jan 2025 14:50:06 -0800 Subject: [PATCH 04/16] updates lint todo file. --- packages/ilios-common/.lint-todo | 2 -- 1 file changed, 2 deletions(-) diff --git a/packages/ilios-common/.lint-todo b/packages/ilios-common/.lint-todo index 7c494831a2..2aceee0c5d 100644 --- a/packages/ilios-common/.lint-todo +++ b/packages/ilios-common/.lint-todo @@ -1,5 +1,3 @@ -add|ember-template-lint|no-at-ember-render-modifiers|5|2|5|2|fb8a149d14413d4dfc84ffd31349ef3f2ac6d17b|1731542400000|1762646400000|1793750400000|addon/components/date-picker.hbs -add|ember-template-lint|no-at-ember-render-modifiers|6|2|6|2|993f1e23f796f19a221eae6e24872755e0436cb4|1731542400000|1762646400000|1793750400000|addon/components/date-picker.hbs add|ember-template-lint|no-at-ember-render-modifiers|6|8|6|8|c628ee621a6e921e369bf6bcb158a5ef932e6741|1731542400000|1762646400000|1793750400000|addon/components/editable-field.hbs add|ember-template-lint|no-at-ember-render-modifiers|2|2|2|2|ad17d66e0fe1720bc8ddedc12dff3a105709765c|1731542400000|1762646400000|1793750400000|addon/components/html-editor.hbs add|ember-template-lint|no-at-ember-render-modifiers|3|2|3|2|d39abab22a3e75d93f69335da422e7ef73b36283|1731542400000|1762646400000|1793750400000|addon/components/html-editor.hbs From 3b28965da32d0799d8098154ce08624879aae9ae Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Mon, 13 Jan 2025 15:26:38 -0800 Subject: [PATCH 05/16] ensure that the current locale is passed to the modifier for reactivity. --- packages/ilios-common/addon/components/date-picker.hbs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index ee38343bd0..78702e5db4 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,6 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{this.picker @value @minDate @maxDate @locale}} + {{this.picker @value @minDate @maxDate this.intl.primaryLocale}} ...attributes /> \ No newline at end of file From ec9ab8a6229167b608e075079f8ab1fad0ff22b9 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 10:50:28 -0800 Subject: [PATCH 06/16] better track given locale and locale-changes. the flatpickr locale handling is inconsistent - if you give it a locale identifier, like we do by default with `en`, and then try to get that back from the picker instance, you'll get an object. handling it this way is much cleaner, at the expense of adding another property. it also gets us around having to compare object to object for non-english locales. --- .../addon/components/date-picker.js | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index efdabb7aea..c14ff8be9f 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -12,10 +12,12 @@ export default class DatePickerComponent extends Component { @service intl; @tracked isOpen = false; _flatPickerInstance; + _locale; - picker = modifier((element, [value, minDate, maxDate, localeIdentifier]) => { + picker = modifier((element, [value, minDate, maxDate, locale]) => { if (!this._flatPickerInstance) { - this._flatPickerInstance = this.initPicker(element, value, minDate, maxDate); + this._flatPickerInstance = this.initPicker(element, value, minDate, maxDate, locale); + this._locale = locale; } if (this._flatPickerInstance.selectedDates[0] !== value) { this._flatPickerInstance.setDate(value); @@ -26,14 +28,13 @@ export default class DatePickerComponent extends Component { if (this._flatPickerInstance.maxDate !== maxDate) { this._flatPickerInstance.set('maxDate', maxDate); } - const locale = this.getLocale(localeIdentifier); - if (this._flatPickerInstance.l10n !== locale) { - this._flatPickerInstance.set('locale', locale); + if (this._locale !== locale) { + this._locale = locale; + this._flatPickerInstance.set('locale', this.getFlatpickrLocale(locale)); } }); - getLocale(localeIdentifier) { - //console.log(French); + getFlatpickrLocale(localeIdentifier) { switch (localeIdentifier) { case 'fr': return French; @@ -44,10 +45,9 @@ export default class DatePickerComponent extends Component { } } - initPicker(element, value, minDate, maxDate, localeIdentifier) { - const locale = this.getLocale(localeIdentifier); + initPicker(element, value, minDate, maxDate, locale) { return flatpickr(element, { - locale, + locale: this.getFlatpickrLocale(locale), defaultDate: value, formatDate: (dateObj) => this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), @@ -69,6 +69,7 @@ export default class DatePickerComponent extends Component { willDestroy() { super.willDestroy(...arguments); + this._locale = null; if (this._flatPickerInstance) { this._flatPickerInstance.destroy(); } From 5b76bf35bdb998ec1be749407795c8e8ee869085 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 10:57:32 -0800 Subject: [PATCH 07/16] rm kruft. --- .../ilios-common/addon/components/date-picker.js | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index c14ff8be9f..3a2e141afa 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -5,12 +5,11 @@ import { modifier } from 'ember-modifier'; import flatpickr from 'flatpickr'; import { French } from 'flatpickr/dist/l10n/fr.js'; import { Spanish } from 'flatpickr/dist/l10n/es.js'; -import { later, next } from '@ember/runloop'; +import { next } from '@ember/runloop'; import { isTesting } from '@embroider/macros'; export default class DatePickerComponent extends Component { @service intl; - @tracked isOpen = false; _flatPickerInstance; _locale; @@ -52,15 +51,6 @@ export default class DatePickerComponent extends Component { formatDate: (dateObj) => this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), onChange: (selectedDates) => this.onChange(selectedDates[0]), - onOpen: () => { - // eslint-disable-next-line ember/no-runloop - later(() => { - this.isOpen = true; - }, 250); - }, - onClose: () => { - this.isOpen = false; - }, maxDate: maxDate ?? null, minDate: minDate ?? null, disableMobile: isTesting(), From 0d5c557ddbd60da1b3aaf74f787584f1e1b5f139 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 10:58:02 -0800 Subject: [PATCH 08/16] rm kruft. the isOpen tracked prop wasn't doing anything. it's wired up and toggled, but its state never gets checked. BALETED. --- packages/ilios-common/addon/components/date-picker.js | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index 3a2e141afa..bbd0ef2fc0 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -1,6 +1,5 @@ import Component from '@glimmer/component'; import { service } from '@ember/service'; -import { tracked } from '@glimmer/tracking'; import { modifier } from 'ember-modifier'; import flatpickr from 'flatpickr'; import { French } from 'flatpickr/dist/l10n/fr.js'; From b94f69d815674e15037b554580c2acbbaab4ee48 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 11:00:04 -0800 Subject: [PATCH 09/16] declare private props with the correct prefix. it's pound, not underscore. see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Classes/Private_properties --- .../addon/components/date-picker.js | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js index bbd0ef2fc0..7fec4ffad2 100644 --- a/packages/ilios-common/addon/components/date-picker.js +++ b/packages/ilios-common/addon/components/date-picker.js @@ -9,26 +9,26 @@ import { isTesting } from '@embroider/macros'; export default class DatePickerComponent extends Component { @service intl; - _flatPickerInstance; - _locale; + #flatPickerInstance; + #locale; picker = modifier((element, [value, minDate, maxDate, locale]) => { - if (!this._flatPickerInstance) { - this._flatPickerInstance = this.initPicker(element, value, minDate, maxDate, locale); - this._locale = locale; + if (!this.#flatPickerInstance) { + this.#flatPickerInstance = this.initPicker(element, value, minDate, maxDate, locale); + this.#locale = locale; } - if (this._flatPickerInstance.selectedDates[0] !== value) { - this._flatPickerInstance.setDate(value); + if (this.#flatPickerInstance.selectedDates[0] !== value) { + this.#flatPickerInstance.setDate(value); } - if (this._flatPickerInstance.minDate !== minDate) { - this._flatPickerInstance.set('minDate', minDate); + if (this.#flatPickerInstance.minDate !== minDate) { + this.#flatPickerInstance.set('minDate', minDate); } - if (this._flatPickerInstance.maxDate !== maxDate) { - this._flatPickerInstance.set('maxDate', maxDate); + if (this.#flatPickerInstance.maxDate !== maxDate) { + this.#flatPickerInstance.set('maxDate', maxDate); } - if (this._locale !== locale) { - this._locale = locale; - this._flatPickerInstance.set('locale', this.getFlatpickrLocale(locale)); + if (this.#locale !== locale) { + this.#locale = locale; + this.#flatPickerInstance.set('locale', this.getFlatpickrLocale(locale)); } }); @@ -58,9 +58,9 @@ export default class DatePickerComponent extends Component { willDestroy() { super.willDestroy(...arguments); - this._locale = null; - if (this._flatPickerInstance) { - this._flatPickerInstance.destroy(); + this.#locale = null; + if (this.#flatPickerInstance) { + this.#flatPickerInstance.destroy(); } } From d510c7b6d1611d4b69d8b1a4187c305d3b35144f Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 12:03:38 -0800 Subject: [PATCH 10/16] refactor in-component modifier function out into its own Modifier Class. --- .../addon/components/date-picker.hbs | 2 +- .../addon/components/date-picker.js | 72 --------------- .../addon/modifiers/date-picker.js | 87 +++++++++++++++++++ .../ilios-common/app/modifiers/date-picker.js | 1 + .../integration/modifiers/date-picker-test.js | 21 +++++ 5 files changed, 110 insertions(+), 73 deletions(-) delete mode 100644 packages/ilios-common/addon/components/date-picker.js create mode 100644 packages/ilios-common/addon/modifiers/date-picker.js create mode 100644 packages/ilios-common/app/modifiers/date-picker.js create mode 100644 packages/test-app/tests/integration/modifiers/date-picker-test.js diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index 78702e5db4..b06c257c83 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,6 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{this.picker @value @minDate @maxDate this.intl.primaryLocale}} + {{date-picker @value @minDate @maxDate this.intl.primaryLocale @onChange}} ...attributes /> \ No newline at end of file diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js deleted file mode 100644 index 7fec4ffad2..0000000000 --- a/packages/ilios-common/addon/components/date-picker.js +++ /dev/null @@ -1,72 +0,0 @@ -import Component from '@glimmer/component'; -import { service } from '@ember/service'; -import { modifier } from 'ember-modifier'; -import flatpickr from 'flatpickr'; -import { French } from 'flatpickr/dist/l10n/fr.js'; -import { Spanish } from 'flatpickr/dist/l10n/es.js'; -import { next } from '@ember/runloop'; -import { isTesting } from '@embroider/macros'; - -export default class DatePickerComponent extends Component { - @service intl; - #flatPickerInstance; - #locale; - - picker = modifier((element, [value, minDate, maxDate, locale]) => { - if (!this.#flatPickerInstance) { - this.#flatPickerInstance = this.initPicker(element, value, minDate, maxDate, locale); - this.#locale = locale; - } - if (this.#flatPickerInstance.selectedDates[0] !== value) { - this.#flatPickerInstance.setDate(value); - } - if (this.#flatPickerInstance.minDate !== minDate) { - this.#flatPickerInstance.set('minDate', minDate); - } - if (this.#flatPickerInstance.maxDate !== maxDate) { - this.#flatPickerInstance.set('maxDate', maxDate); - } - if (this.#locale !== locale) { - this.#locale = locale; - this.#flatPickerInstance.set('locale', this.getFlatpickrLocale(locale)); - } - }); - - getFlatpickrLocale(localeIdentifier) { - switch (localeIdentifier) { - case 'fr': - return French; - case 'es': - return Spanish; - default: - return 'en'; - } - } - - initPicker(element, value, minDate, maxDate, locale) { - return flatpickr(element, { - locale: this.getFlatpickrLocale(locale), - defaultDate: value, - formatDate: (dateObj) => - this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), - onChange: (selectedDates) => this.onChange(selectedDates[0]), - maxDate: maxDate ?? null, - minDate: minDate ?? null, - disableMobile: isTesting(), - }); - } - - willDestroy() { - super.willDestroy(...arguments); - this.#locale = null; - if (this.#flatPickerInstance) { - this.#flatPickerInstance.destroy(); - } - } - - async onChange(date) { - await this.args.onChange(date); - // eslint-disable-next-line ember/no-runloop - await next(() => {}); - } -} diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js new file mode 100644 index 0000000000..6666bd6332 --- /dev/null +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -0,0 +1,87 @@ +import Modifier from 'ember-modifier'; +import { registerDestructor } from '@ember/destroyable'; +import flatpickr from 'flatpickr'; +import { French } from 'flatpickr/dist/l10n/fr.js'; +import { Spanish } from 'flatpickr/dist/l10n/es.js'; +import { next } from '@ember/runloop'; +import { isTesting } from '@embroider/macros'; +import { service } from '@ember/service'; + +function cleanup(instance) { + instance.locale = null; + instance.onChangeHandler = null; + if (instance.flatpickr) { + instance.flatpickr.destroy(); + instance.flatpickr = null; + } +} + +export default class DatePickerModifier extends Modifier { + @service intl; + flatpickr = null; + locale = null; + onChangeHandler = null; + + constructor(owner, args) { + super(owner, args); + registerDestructor(this, cleanup); + } + + modify(element, [value, minDate, maxDate, locale, onChangeHandler]) { + // We only need to set this once. + if (!this.onChangeHandler) { + this.onChangeHandler = onChangeHandler; + } + if (!this.flatpickr) { + this.flatpickr = this.initPicker(element, value, minDate, maxDate, locale); + this.locale = locale; + } + + if (this.flatpickr.selectedDates[0] !== value) { + this.flatpickr.setDate(value); + } + if (this.flatpickr.minDate !== minDate) { + this.flatpickr.set('minDate', minDate); + } + if (this.flatpickr.maxDate !== maxDate) { + this.flatpickr.set('maxDate', maxDate); + } + + if (this.locale !== locale) { + this.locale = locale; + this.flatpickr.set('locale', this.getFlatpickrLocale(locale)); + } + } + + // @see https://flatpickr.js.org/localization/ + getFlatpickrLocale(localeIdentifier) { + switch (localeIdentifier) { + case 'fr': + return French; + case 'es': + return Spanish; + default: + return 'en'; + } + } + + initPicker(element, value, minDate, maxDate, locale) { + return flatpickr(element, { + locale: this.getFlatpickrLocale(locale), + defaultDate: value, + formatDate: (dateObj) => + this.intl.formatDate(dateObj, { day: '2-digit', month: '2-digit', year: 'numeric' }), + onChange: (selectedDates) => this.onChange(selectedDates[0]), + maxDate: maxDate ?? null, + minDate: minDate ?? null, + disableMobile: isTesting(), + }); + } + async onChange(date) { + if (this.onChangeHandler) { + await this.onChangeHandler(date); + } + // eslint-disable-next-line ember/no-runloop + await next(() => {}); + } +} diff --git a/packages/ilios-common/app/modifiers/date-picker.js b/packages/ilios-common/app/modifiers/date-picker.js new file mode 100644 index 0000000000..663824728e --- /dev/null +++ b/packages/ilios-common/app/modifiers/date-picker.js @@ -0,0 +1 @@ +export { default } from 'ilios-common/modifiers/date-picker'; diff --git a/packages/test-app/tests/integration/modifiers/date-picker-test.js b/packages/test-app/tests/integration/modifiers/date-picker-test.js new file mode 100644 index 0000000000..7100a0b16c --- /dev/null +++ b/packages/test-app/tests/integration/modifiers/date-picker-test.js @@ -0,0 +1,21 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'test-app/tests/helpers'; +import { render } from '@ember/test-helpers'; +import { hbs } from 'ember-cli-htmlbars'; + +// todo: flesh this out. [ST 2025/01/19] +module('Integration | Modifier | date-picker', function (hooks) { + setupRenderingTest(hooks); + + test('it works with minimal input', async function (assert) { + this.set('value', Date.now()); + this.set('minDate', null); + this.set('maxDate', null); + this.set('locale', 'en'); + await render( + hbs`
`, + ); + // todo: add more test assertions here. + assert.ok(true); + }); +}); From 216b819fc370f96f1cf50c727ab0b41e48b546f1 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 15 Jan 2025 14:48:04 -0800 Subject: [PATCH 11/16] rm runloop interferences. it's been a while since this was kludged in. let's see if we can consistently roll high without it now. --- packages/ilios-common/addon/modifiers/date-picker.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js index 6666bd6332..bd282f8eae 100644 --- a/packages/ilios-common/addon/modifiers/date-picker.js +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -3,7 +3,6 @@ import { registerDestructor } from '@ember/destroyable'; import flatpickr from 'flatpickr'; import { French } from 'flatpickr/dist/l10n/fr.js'; import { Spanish } from 'flatpickr/dist/l10n/es.js'; -import { next } from '@ember/runloop'; import { isTesting } from '@embroider/macros'; import { service } from '@ember/service'; @@ -81,7 +80,5 @@ export default class DatePickerModifier extends Modifier { if (this.onChangeHandler) { await this.onChangeHandler(date); } - // eslint-disable-next-line ember/no-runloop - await next(() => {}); } } From b6f7280f81efcd4f963ae82ad7a2b33aff3f843f Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Thu, 16 Jan 2025 11:11:18 -0800 Subject: [PATCH 12/16] bringing the component class back - we need it so we can provide the current locale as input to the modifier. --- packages/ilios-common/addon/components/date-picker.js | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 packages/ilios-common/addon/components/date-picker.js diff --git a/packages/ilios-common/addon/components/date-picker.js b/packages/ilios-common/addon/components/date-picker.js new file mode 100644 index 0000000000..780d997abf --- /dev/null +++ b/packages/ilios-common/addon/components/date-picker.js @@ -0,0 +1,6 @@ +import Component from '@glimmer/component'; +import { service } from '@ember/service'; + +export default class DatePickerComponent extends Component { + @service intl; +} From 0dc2a407151d343ae02909b7c2d9bcf01ba4f98a Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 22 Jan 2025 15:16:21 -0800 Subject: [PATCH 13/16] use positional arguments for all optional input. --- packages/ilios-common/addon/components/date-picker.hbs | 2 +- packages/ilios-common/addon/modifiers/date-picker.js | 7 ++++--- .../tests/integration/modifiers/date-picker-test.js | 10 +++++++++- 3 files changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/ilios-common/addon/components/date-picker.hbs b/packages/ilios-common/addon/components/date-picker.hbs index b06c257c83..d88b4dbe4f 100644 --- a/packages/ilios-common/addon/components/date-picker.hbs +++ b/packages/ilios-common/addon/components/date-picker.hbs @@ -2,6 +2,6 @@ aria-label={{t "general.pickADate"}} class="date-picker" data-test-date-picker - {{date-picker @value @minDate @maxDate this.intl.primaryLocale @onChange}} + {{date-picker @value minDate=@minDate maxDate=@maxDate locale=this.intl.primaryLocale onChangeHandler=@onChange}} ...attributes /> \ No newline at end of file diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js index bd282f8eae..79157c5f3b 100644 --- a/packages/ilios-common/addon/modifiers/date-picker.js +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -8,7 +8,7 @@ import { service } from '@ember/service'; function cleanup(instance) { instance.locale = null; - instance.onChangeHandler = null; + instance.onChange = null; if (instance.flatpickr) { instance.flatpickr.destroy(); instance.flatpickr = null; @@ -26,13 +26,13 @@ export default class DatePickerModifier extends Modifier { registerDestructor(this, cleanup); } - modify(element, [value, minDate, maxDate, locale, onChangeHandler]) { + modify(element, [value], { minDate, maxDate, locale, onChangeHandler }) { // We only need to set this once. if (!this.onChangeHandler) { this.onChangeHandler = onChangeHandler; } if (!this.flatpickr) { - this.flatpickr = this.initPicker(element, value, minDate, maxDate, locale); + this.flatpickr = this.initPicker(element, value, minDate, maxDate, this.locale); this.locale = locale; } @@ -76,6 +76,7 @@ export default class DatePickerModifier extends Modifier { disableMobile: isTesting(), }); } + async onChange(date) { if (this.onChangeHandler) { await this.onChangeHandler(date); diff --git a/packages/test-app/tests/integration/modifiers/date-picker-test.js b/packages/test-app/tests/integration/modifiers/date-picker-test.js index 7100a0b16c..add61f9c5a 100644 --- a/packages/test-app/tests/integration/modifiers/date-picker-test.js +++ b/packages/test-app/tests/integration/modifiers/date-picker-test.js @@ -13,7 +13,15 @@ module('Integration | Modifier | date-picker', function (hooks) { this.set('maxDate', null); this.set('locale', 'en'); await render( - hbs`
`, + hbs`
`, ); // todo: add more test assertions here. assert.ok(true); From 3d0b6bad8ae34f241591998819226e3f77008600 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 22 Jan 2025 15:24:59 -0800 Subject: [PATCH 14/16] only change locale if one is provided. on picker init, use the default locale if none was given. --- packages/ilios-common/addon/modifiers/date-picker.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js index 79157c5f3b..2614313871 100644 --- a/packages/ilios-common/addon/modifiers/date-picker.js +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -32,8 +32,8 @@ export default class DatePickerModifier extends Modifier { this.onChangeHandler = onChangeHandler; } if (!this.flatpickr) { + this.locale = locale ?? this.intl.primaryLocale; this.flatpickr = this.initPicker(element, value, minDate, maxDate, this.locale); - this.locale = locale; } if (this.flatpickr.selectedDates[0] !== value) { @@ -46,7 +46,7 @@ export default class DatePickerModifier extends Modifier { this.flatpickr.set('maxDate', maxDate); } - if (this.locale !== locale) { + if (locale && this.locale !== locale) { this.locale = locale; this.flatpickr.set('locale', this.getFlatpickrLocale(locale)); } From 70cb334637c64e1c3f5416024cc1104b98f6a8ca Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Wed, 22 Jan 2025 16:30:15 -0800 Subject: [PATCH 15/16] flesh out test coverage for modifier. --- .../integration/modifiers/date-picker-test.js | 112 +++++++++++++++--- 1 file changed, 97 insertions(+), 15 deletions(-) diff --git a/packages/test-app/tests/integration/modifiers/date-picker-test.js b/packages/test-app/tests/integration/modifiers/date-picker-test.js index add61f9c5a..cdf50f01fe 100644 --- a/packages/test-app/tests/integration/modifiers/date-picker-test.js +++ b/packages/test-app/tests/integration/modifiers/date-picker-test.js @@ -1,29 +1,111 @@ import { module, test } from 'qunit'; import { setupRenderingTest } from 'test-app/tests/helpers'; -import { render } from '@ember/test-helpers'; +import { render, find } from '@ember/test-helpers'; import { hbs } from 'ember-cli-htmlbars'; -// todo: flesh this out. [ST 2025/01/19] module('Integration | Modifier | date-picker', function (hooks) { setupRenderingTest(hooks); test('it works with minimal input', async function (assert) { - this.set('value', Date.now()); - this.set('minDate', null); - this.set('maxDate', null); - this.set('locale', 'en'); + const value = new Date('2014-03-02'); + this.set('value', value); + await render(hbs`
`); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + assert.notOk(flatpickr.config.minDate); + assert.notOk(flatpickr.config.maxDate); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('January'); + }); + + test('it works with minDate, maxDate, and locale as input', async function (assert) { + const value = new Date('2014-03-02'); + const minDate = new Date('2014-01-12'); + const maxDate = new Date('2014-05-11'); + const locale = 'es'; + this.set('value', value); + this.set('minDate', minDate); + this.set('maxDate', maxDate); + this.set('locale', locale); await render( hbs`
`, ); - // todo: add more test assertions here. - assert.ok(true); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + assert.strictEqual(minDate.getTime(), flatpickr.config.minDate.getTime()); + assert.strictEqual(maxDate.getTime(), flatpickr.config.maxDate.getTime()); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('Enero'); + }); + + test('changing value is responsive', async function (assert) { + const value = new Date('2014-03-02'); + this.set('value', value); + await render(hbs`
`); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(value.getTime(), flatpickr.selectedDates[0].getTime()); + const newValue = new Date('2024-03-03'); + this.set('value', newValue); + assert.strictEqual(newValue.getTime(), flatpickr.selectedDates[0].getTime()); + }); + + test('changing minDate is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const minDate = new Date('2014-01-12'); + this.set('value', value); + this.set('minDate', minDate); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(minDate.getTime(), flatpickr.config.minDate.getTime()); + const newMinDate = new Date('2024-03-03'); + this.set('minDate', newMinDate); + assert.strictEqual(newMinDate.getTime(), flatpickr.config.minDate.getTime()); + }); + + test('changing maxDate is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const maxDate = new Date('2014-01-12'); + this.set('value', value); + this.set('maxDate', maxDate); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.strictEqual(maxDate.getTime(), flatpickr.config.maxDate.getTime()); + const newMaxDate = new Date('2024-03-03'); + this.set('maxDate', newMaxDate); + assert.strictEqual(newMaxDate.getTime(), flatpickr.config.maxDate.getTime()); + }); + + test('changing locale is responsive', async function (assert) { + const value = new Date('2014-03-02'); + const locale = 'es'; + this.set('value', value); + this.set('locale', locale); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('Enero'); + this.set('locale', 'fr'); + assert.dom('option:nth-of-type(1)', flatpickr.monthElements[0]).hasText('janvier'); + }); + + test('onChange callback fires', async function (assert) { + assert.expect(1); + const value = new Date('2014-03-02'); + const newValue = new Date('2022-06-23'); + this.set('value', value); + this.set('onChange', (date) => { + assert.strictEqual(newValue.getTime(), date.getTime()); + }); + await render( + hbs`
`, + ); + const flatpickr = find('[data-test-picker-element]')._flatpickr; + flatpickr.setDate(newValue, true); }); }); From e2116c1b19cba25111bd1f48426245171782c001 Mon Sep 17 00:00:00 2001 From: Stefan Topfstedt Date: Thu, 23 Jan 2025 14:05:28 -0800 Subject: [PATCH 16/16] inlines destructor callback. --- .../addon/modifiers/date-picker.js | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/packages/ilios-common/addon/modifiers/date-picker.js b/packages/ilios-common/addon/modifiers/date-picker.js index 2614313871..13d9345fae 100644 --- a/packages/ilios-common/addon/modifiers/date-picker.js +++ b/packages/ilios-common/addon/modifiers/date-picker.js @@ -6,15 +6,6 @@ import { Spanish } from 'flatpickr/dist/l10n/es.js'; import { isTesting } from '@embroider/macros'; import { service } from '@ember/service'; -function cleanup(instance) { - instance.locale = null; - instance.onChange = null; - if (instance.flatpickr) { - instance.flatpickr.destroy(); - instance.flatpickr = null; - } -} - export default class DatePickerModifier extends Modifier { @service intl; flatpickr = null; @@ -23,7 +14,14 @@ export default class DatePickerModifier extends Modifier { constructor(owner, args) { super(owner, args); - registerDestructor(this, cleanup); + registerDestructor(this, () => { + this.locale = null; + this.onChangeHandler = null; + if (this.flatpickr) { + this.flatpickr.destroy(); + this.flatpickr = null; + } + }); } modify(element, [value], { minDate, maxDate, locale, onChangeHandler }) {