ember-appointment-slotS-pickerS provides four different pickerS components to select one or several slotS, plus a suite of optional composable components to customize the experience.
We transferred as is the components from a British Gas private addon, some of them consuming old javascript libraries that would helpfully be rewritten using ember-animation.
You can choose to only pick up the components and dependencies you need from the suite using tree-shaking.
- Ember.js v3.24 or above
- Ember CLI v3.24 or above
- Node.js v12 or above
At the time of writing, you can use any of the following children calendar components:
Device: | Slots selection: | Composable with: | Use when slots availability is: | ||||||
---|---|---|---|---|---|---|---|---|---|
slots-picker calendar | Mobile | Desktop | Single | Multi | slots-filter | clock-reloader | Low | Medium | High |
easy-slot-picker | ✔ | ✔ | ✔ | ✔ | ✔ | ||||
slots-picker/mobile | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ||
slots-picker/desktop | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | |
slots-picker/pickadate | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | |
slots-picker/cards | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ | ✔ |
We tried to have all of the slots-picker/XX
components use the same API (still needs to be improved), starting from a common definition of the appointmentSlots
array / promiseArray sent as attribute to the parent slots-picker
container component:
One appointmentSlot
can be an ember-data model or a standard Ember object and can contain the following properties:
const appointmentSlot = EmberObject.extend({
slotPickerRowId: '20181220_AllDay', // currently mandatory, one new row (time of the appointment) will be defined for each new slotPickerRowId
slotPickerRowLabel: 'AllDay', // currently mandatory, the label to be applied to each row (typically slotPickerTime)
slotPickerDay: '20181220', /*(or moment object)*/, // mandatory, the day of the slot, must be a moment-compatible day string, used to form columns,
slotPickerDayLabel: 'Sun 24th Nov 2019', // currently mandatory, shown as label for the columns of the calendars and when selecting the slots,
slotPickerTime: '8am - 5pm', // mandatory (or override ), the time of the slot, used when displaying the particular slot
slotPickerStartTimeLabel: '8am,' // mandatory (or override slots-picker/selection-single)
slotPickerEndTimeLabel: '10am', // mandatory (or override slots-picker/selection-single)
slotPickerRowLabelClassName: 'bold', // optional, the class name to be applied to the label of each row
slotPickerGroup: 0,//optional, group rows by, for example, slots with time ranges, and slots with variant labels,
slotPickerLongDayLabel: 'Thursday 28th November' , // optional (defaults to slotPickerDayLabel) (used in slots-picker/selection-single)
slotPickerNotAvailable: false, // optional (default false), to filter out the slots passed to the set of all slots in the components
slotPickerNotDisplayable: false // optional (default false), to filter out the slots you want to show, while still including them in the global (to show empty days, for example),
slotPickerHasTag: false // optional (default false), will display an `fa-tag` icon on slots buttons when true
});
Additional inputs that can be passed into the slots-picker
component:
noSlotLabel: 'Not Available' //optional, defaults to 'Fully Booked'
You will find in the dummy app lots of examples on how to use these components, some of them are also summarized below:
For those who are on a rush, we created an easy to use but non-customizable and non-extendable default slot picker, that you can just drop in your app like so:
Under the hood, easy-slot-picker
is using a particular combination of slots-picker/desktop
, slots-picker/mobile
and slots-picker/slots-loader
which can be replicated and extended in the steps below:
The basic use case is to use one of the slots-picker/xx
individual calendars (mobile
, desktop
, pickadate
or cards
) inside the slot-picker
container component:
baseProps
is a hash of properties that children components need to be able to do their work. All children components extend the slot-picker/base
class converting each of those properties into an alias on the child component: days: readOnly('baseProps.days'),
One of those baseProps
properties is the slotsAreLoading
property, set to true
when appointmentSlots
is a isPending
promiseArray, or an array with null
length. You can customize the loading template for the slot-pickers in your app (a default one is provided). We created a slots-picker/slots-loader
default loading template that you can customize if you have nothing better:
One of the things you will have to maintain if you move away from easy-slot-picker
is to choose in your app which calendars to display for the different screen sizes. Some calendars are only available on mobile, some others only on desktop, some on both.
The easy-slot-picker
code is doing just that, by changing the child component name depending on the viewport size:
//appointment-slot-picker/component.js
viewport: service(),
slotPickerComponentName: computed('viewport.isMobile', function () {
const showMobile = this.get('viewport.isMobile');
return showMobile ? 'slots-picker/mobile' : 'slots-picker/desktop';
}),
To show an overlay on the calendars once some time has expired, and ask customers to click to refresh their slots, you can use our clock-reloader
and clock-reloader/overlay
components:
Some components (slots-picker/pickadate
, slots-picker/mobile
) show the dates before the times, so in that case it can be very useful to add a capability for customers to filter the available dates by time-range. This is what our slots-filter
and slots-filter/ui
components do:
Due to composition, you can modularize your slot-picker as you want. If you use all the components together, you could get something looking like this (real world use case):
By contextual components we mean using things like:
where the children asp.XX
components would be yielded by the parent slot-picker
component. This apparently leads to cleaner syntax than passing the baseProps
properties down to the children, but the problem with this pattern is that:
- There is one big massive component containing all the others, so it prevents us from splitting them in different addons / tree-shake.
- Using clearly separate components allows the addon consumers to extend only one of them (parent or one of the children) in one app (or updating the corresponding addon), without bothering about the others.
- Previously, when adding a functionality to a calendar, we had to pass it as an attribute to the parent container, which would then itself transfer it to the child component, it didn't really make sense. Now you can just add / override children attributes in your app as you wish, without touching the container component
- This allows to use as and when needed optional components like
clock-reloader
,slots-picker/slots-loader
,slots-filter
.
This component's primary use is to transform the array of appointmentSlots
provided to it into a format more consumable by the children components, yielding downstream for example the rows (one for each time range), columns (one for each date) and cells of the calendars:
This base component just aliases the baseProps.xx
objects given by the parent slots-picker
container to similarly names properties in the children calendar components:
days: computed('baseProps.days', function () {
return this.get('baseProps.days');
}).readOnly(),
It also contains patterns common to all calendars, for example the action called when changing a date.
Each child slots-picker/xx
calendar component is then built by extending this base class.
You may have a better design in mind, or want to do things better than us and use ember-animation to build new calendars. In that case, all you have to do is create a new slots-picker/better-calendar
component extending slots-picker/base
and add it to the suite!
Any better idea of how to do things? Create an issue or even better create a PR!
Based on broccoli-funnel, and extended with bundles
that you can include or exclude. The available keys for bundles correspond to the different calendars (easy
, mobile
, desktop
, cards
, pickadate
) and also a bg
bundle that you have to exclude if you work for British Gas (otherwise just ignore this bundle).
Some examples:
The below will only load the easy-slot-picker
component and associated files, including stylesheets:
options['ember-appointment-slots-pickers'] = {
bundles: {
include: ['easy']
}
};
This will exclude the files and libraries needed for the mobile
and cards
calendars, and also the clock-reloader
component suite, and keep everything else:
//ember-cli-build.js
options['ember-appointment-slots-pickers'] = {
bundles: {
exclude: ['mobile', 'cards']
},
exclude: [
/clock-reloader/
]
},
This will only load the pickadate-input
component and stylesheets, and mandatory services / helpers:
//ember-cli-build.js
options['ember-appointment-slots-pickers'] = {
include: [
/components\/pickadate-input/,
'**/ember-appointment-slots-pickers.css'
],
bundles: {
exclude: [
'mobile'
]
}
};
git clone <repository-url>
this repositorycd ember-appointment-slots-pickers
npm install
bower install
ember serve
- Visit your app at http://localhost:4200.
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
ember build