Want to learn how to make your own selectable calendar component you can style and modify for your SAP Build Apps application? Look no further! In this blog I will go over the basic principles of creating a calendar component for your SAP Build Apps application.
Screen recording of the calendar functionality
Please note that this blog assumes the reader to have familiarity with SAP Build Apps and how to work with variables, logic, and at least some formulas. I won't explain every detail of creating this calendar, but rather the most crucial and complex parts. Please don't hesitate to ask if you can't figure some other part out!
To get started quickly, you can copy the component to paste onto your view canvas from below. You still need to do step 1 to set up the variables for the calendar to work. Reading the other steps will deepen your understanding on why and how the calendar works, and how to do any adjustments or modifications you might want to make.
Find the component you can copy paste on the UI canvas from
here.
---
Step 1: Setting up variables
This calendar requires only two page variables:
firstDayOfTheMonth and
selectedDates.
They are exactly what it says on the tin – in one you store the datetime of the first day of the month currently visible in the calendar, and the other stores the information of the currently selected dates.
Screenshot of variables in the app
On the page's main logic canvas, set these variables.
firstDayOfMonth will be set to default to the first day of the current month. Whenever
firstDayOfMonth variable changes, such as after page mounted, but also as user switches between months,
selectedDates is changed to default to all dates being selected. You can modify the logic you want for this, if you want to exclude some days for example or not have any pre-selected dates at all.
Screenshot from the logic canvas
The logic for this initial setting of the
firstDayOfMonth is simple enough. However, a known issue here is that if the user is from a timezone far away from UTC and it is the first or last day of the month, they may get the wrong month as the default month that opens. I have not figured out an easy way to fix this as of yet, and don't think it is as serious as the user can easily change months, but if you come up with a solution, please comment below.
SET_DATETIME_COMPONENT(NOW(), 1, "date")
For the
selectedDates, there is a little more complex calculating. The most reliable way I've found to determine how many days a month has is to add one month to the first day of the month and then subtract one day. This way you get the date of the last day of the month, and I use that in doing the selection.
MAP(GENERATE_RANGE(1, GET_DATETIME_COMPONENT(SUBTRACT_DURATION(ADD_DURATION(pageVars.firstDayOfMonth, 1, "months"), 1, "days"), "date")), {date: ADD_DURATION(pageVars.firstDayOfMonth, item-1, "days")})
---
Step 2: Generating the calendar
Next we get into the most important and complex part: generating the calendar itself.
Using the
firstDayOfMonth primed in the previous part, my implementation creates the following structure:
[
{
weekNumber: (Number),
days: [
{
date: (Datetime)
}
]
},
...
]
If you need to store additional information to a day, you can add more properties to the Object – in my implementation of a time logging app I had the information of whether or not a day was a day off or not stored here as well for ease of use in determining styling and functionality.
This list of weeks with a nested list of days is generated via formula in the
Repeat with property of a Container I have made to hold the data of one week. Here is what the formula does, in rough reading order:
- Generate a range from 0 to 5 to loop with my trusty MAP formula. This generates six weeks for a month, as it is the maximum amount of weeks a month can have (the extra week(s) are hidden using the Visibility property)
- Set the week number after modifying the firstDayOfMonth by adding 0-5 weeks to it
- For the days in the week, start another MAP formula, for which I generate a range from 1 to 7 this time (I use 1-7 instead of 0-6 since in the formulas where weekday is determined, 1 is Monday and 7 is Sunday)
- Set the date of the day by adding the 0-5 weeks to the firstDayOfMonth and then modifying that date to be the one for the correct weekday
Note here that the formula editor will not recognize "weekday" as a valid component for the
SET_DATETIME_COMPONENT formula, but it works as expected.
MAP<week>(GENERATE_RANGE(0,5), { weekNumber: NUMBER(FORMAT_DATETIME_WITH_TIMEZONE(ADD_DURATION(pageVars.firstDayOfMonth, week, "weeks"), "W")), days: MAP<day>(GENERATE_RANGE(1, 7), { date: SET_DATETIME_COMPONENT(ADD_DURATION(pageVars.firstDayOfMonth, week, "weeks"), day, "weekday") }) })
After you have set the repeat formula, since you will be doing a nested repeat by repeating the days horizontally, give it a name in the
Repeat as property, or you will run into trouble later on.
Screenshot from the component properties
For hiding the extra week(s) of the month, I use a simple formula to determine whether that week has any days that are in the month to be displayed.
SOME(repeated.week.days, NUMBER(FORMAT_DATETIME_WITH_TIMEZONE(item.date, "M")) == NUMBER(FORMAT_DATETIME_WITH_TIMEZONE(pageVars.firstDayOfMonth, "M")))
A much similar formula is used for determining how the days should be styled (days of the previous or next month are styled differently for better UX).
---
Step 3: Selecting and deselecting days per week
The last crucial bit of this calendar is the ability to select or deselect a whole week's worth of dates at once. This is done in the app by clicking the week number section of a specific week. The functionality triggered sets the
selectedDates page variable with the following formula that does the following:
- Check if every day of the week is selected or not
- If yes, remove the days of the week from the selectedDates (by selecting only dates that are not in this week's dates, could also be done by checking week number)
- If no, add to selectedDates the dates of this week that were not already there
The reason this is this complicated is that date types are not easily compared for uniqueness, and as such I have to use
DATETIME_IS_SAME at every step to check whether two dates are the same on the level of accuracy I need to use.
IF(EVERY<workingDay>(repeated.week.days, SOME<selectedDay>(pageVars.selectedDates, DATETIME_IS_SAME(selectedDay.date, workingDay.date, "day"))), SELECT<date>(pageVars.selectedDates, !SOME(repeated.week.days, DATETIME_IS_SAME(item.date, date.date, "day"))), UNION(pageVars.selectedDates, SELECT<day>(repeated.week.days, !SOME<selectedDate>(pageVars.selectedDates, DATETIME_IS_SAME(day.date, selectedDate.date, "day")))))
---
Done!
...At least, kind of. Since this does not cover every step, I recommend using the copypasteable component I included before Step 1, unless you want to do some intense reverse engineering
🙂
This, however, concludes my brief explanation into the most crucial and complex parts of the logic to generate a calendar component. If you have any specific questions, feel free to ask in the comments below. For more blogs on the topic, check out the
SAP Builders group.
You can modify the provided component to suit your needs and use it in any app you like, but if you do and the app goes live, I'd love to have you comment below with a screenshot or information on how you utilized the calendar!