ember-appointment-slots-pickers
v0.1.1
Published
Appointment slot pickers component suite written in Ember.
Downloads
6
Readme
ember-appointment-slots-pickers
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.
Choose your own calendventure
At the time of writing, you can use any of the following children calendar components:
Many calendars, one API
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'
Leveraging composition to give power to the developers
You will find in the dummy app lots of examples on how to use these components, some of them are also summarized below:
Easy slot picker
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:
{{easy-slot-picker
appointmentSlots=model.appointmentSlots
selected=model.selected
onSelect=(action (mut model.selected))
}}
Under the hood, easy-slot-picker
is using a particular combination of slots-picker/desktop
, slots-picker/mobile
and slots-picker/loader
which can be replicated and extended in the steps below:
Basic setup
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:
{{#slot-picker
appointmentSlots=model.appointmentSlots
selected=model.selected
noSlotLabel='Not available'
select=(action (mut model.selected))
as |baseProps onSelectSlot|
}}
{{component 'slot-picker/desktop'
baseProps=baseProps
onSelectSlot=onSelectSlot
}}
{{/slot-picker}}
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'),
Add a loading template
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/loader
default loading template that you can customize if you have nothing better:
{{#slots-picker
appointmentSlots=model.appointmentSlots
selected=model.selected
noSlotLabel='Not available'
select=(action (mut model.selected))
as |baseProps onSelectSlot|
}}
{{#if baseProps.slotsAreLoading}}
{{slots-picker/loader title="Finding the next available appointments in your area.."}}
{{else}}
{{component 'slots-picker/mobile'
baseProps=baseProps
onSelectSlot=onSelectSlot
}}
{{/if}}
{{/slots-picker}}
Different calendars for different screen sizes
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';
}),
{{#slots-picker
appointmentSlots=appointment-slots
selected=selected
noSlotLabel=no-slot-label
select=(action 'select')
as |baseProps onSelectSlot onSelectDate|
}}
{{#if baseProps.slotsAreLoading}}
{{slots-picker/loader title=loaderSentence}}
{{else}}
{{component slotPickerComponentName
baseProps=baseProps
onSelectSlot=onSelectSlot
onSelectDate=onSelectDate
}}
{{/if}}
{{/slots-picker}}
Refresh the slots after some time
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:
{{#clock-reloader
delay=delay
onrefresh=(route-action "resetAsyncSlots" delay)
as |isExpired refresh|
}}
{{#slots-picker
appointmentSlots=model.showableSlots
selected=model.selected
noSlotLabel='Not available'
select=(action (mut model.selected))
as |baseProps onSelectSlot|
}}
{{component model.slotPickerName
baseProps=baseProps
onSelectSlot=onSelectSlot
}}
{{#if isExpired}}
{{clock-reloader/overlay title="<h3>Hello World</h3>" refresh=(action refresh)}}
{{/if}}
{{/slots-picker}}
{{/clock-reloader}}
Filter the slots by time-range
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:
{{#slots-filter
appointmentSlots=appointment.availableSlots
as |filteredAppointmentSlots changeFilter selectedFilter|
}}
{{#slots-picker
appointmentSlots=filteredAppointmentSlots
selected=appointment.appointmentSlot
noSlotLabel='Not available'
select=(route-action selectSlot)
as |baseProps onSelectSlot onSelectDate|
}}
{{slots-filter/ui
timeSlots=baseProps.rows
changeFilter=changeFilter
selectedFilter=selectedFilter
}}
{{component model.slotPickerName
baseProps=baseProps
onSelectSlot=onSelectSlot
}}
{{/slots-picker}}
{{/slots-filter}}
Combining everything together
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):
{{#clock-reloader
delay=appointment.timerRefresherContainer
onrefresh=(route-action "refresh")
class="mb6" as |isExpired refresh|
}}
{{#slots-filter
appointmentSlots=appointment.availableSlots
select=(route-action 'selectSlot')
filterOption=selectedTimeSlot
as |filteredAppointmentSlots filter|
}}
<div class="{{if appointmentSlotNotSelectedError 'p1 border border-red'}} mb6">
{{#services-account/services-sector-holding/job-type/appointment/select-date/component-select-date/tab-view-low-availability
appointment=appointment
filteredAppointmentSlots=filteredAppointmentSlots
as |slotPickerComponentNameFromTabView availableOrSuggestedSlots|
}}
{{#slots-picker
appointment-slots=availableOrSuggestedSlots
selected=appointment.appointmentSlot.content
no-slot-label="Fully booked"
on-select=(route-action "select")
as |baseProps onSelectSlot onSelectDate|
}}
{{#if baseProps.slotsAreLoading}}
{{slots-picker/loader title="Finding the next available appointments in your area.."}}
{{else}}
{{slots-filter/ui changeFilterTimeSlot=(action filter)}}
{{#if isExpired}}
{{clock-reloader/overlay title=overlayTitle refresh=(action refresh)}}
{{/if}}
{{component (or slotPickerComponentNameFromTabView slotPickerComponentName)
baseProps=baseProps
onSelectSlot=onSelectSlot
onSelectDate=onSelectDate
}}
{{/if}}
{{/slots-picker}}
{{/services-account/services-sector-holding/job-type/appointment/select-date/component-select-date/tab-view-low-availability}}
{{/slots-filter}}
{{/clock-reloader}}
Extend the base classes to create your own calendar
Why not using contextual components?
By contextual components we mean using things like:
{{#slots-picker
appointmentSlots=model.appointmentSlots
selected=model.selected
noSlotLabel='Not available'
select=(action (mut model.selected))
as |asp|
}}
{{asp.desktop}}
{{/slots-picker}}
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/loader
,slots-filter
.
Core classes
slots-picker container component
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:
{{yield
(hash
days=days
rows=rows
cols=cols
cellsPerCol=cellsPerCol
multiSelected=multiSelected
cellsForColOfSelectedDays=cellsForColOfSelectedDays
noSlotLabel=noSlotLabel
slotsAreLoading=slotsAreLoading
selectedFilter=selectedFilter
canSelectMultipleSlots=canSelectMultipleSlots
)
(action "onSelectSlot")
(action "onDeselectSlot")
}}
slots-picker/base component
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.
Add your own calendar
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!
Tree-shaking
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/
]
};
Installation
git clone <repository-url>
this repositorycd ember-appointment-slots-pickers
npm install
bower install
Running
ember serve
- Visit your app at http://localhost:4200.
Running Tests
npm test
(Runsember try:each
to test your addon against multiple Ember versions)ember test
ember test --server
Building
ember build