Open goetzrobin opened 10 months ago
I want to kickstart the discussion around the implementation of the calendar component.
As I was in dire need of a date-picker for a project of mine I already took the liberty to simply copy Angular Material's datepicker and simply apply Tailwind classes to it instead of it's provided SCSS styles (PR linked above). I personally think the API is perfect (as with all Material components, their API and usage is chefs-kiss) and since I haven't used any other calendar/date-picker implementations I'm curious if others have any good examples for other good implementations.
Unfortunately from a code perspective the Material calendar is quite inflexible to be customized and very opinionated in how it is built (lots of SCSS which is quite difficult to translate one-by-one to Tailwind classes) and also uses a lot of template function calls (which are not signal-based), so in that regard I think there is a lot of room for improvement to make it really "ours", but that also means quite a lot of work. So I don't necessarily think simply copying and adjusting their implementation is the way to go.
Curious to hear what others think about that.
The Material API is really good and I think the usage fits well with the rest of the spartan components, I've used the ng-zorro-antd calendar/date-picker but it has the same customisation issues, quite hard to override it and it's most of the time a bare nz-date-picker component, ex:
<nz-date-picker [(ngModel)]="date" (ngModelChange)="onChange($event)"></nz-date-picker>
I think for the component to provide a big amount of value, it should have a similar API as the one Material has and the implementation should be done from scratch in order to make it as customisable as the rest of the Spartan lib, this will allow to have the best of both worlds. Certainly this would be a huge amount of work, but I think it'd be really worth it, specially for a long term perspective.
I found the date picker to be an extremely important component, and something I've always wished to have is datepicker (specially a mobile view as the one material has) while being able to make it my own easily, would be a deal breaker for me and def a lot of people I know in the field.
<hlm-calendar />
Will probably surface same inputs available from brnCalendar in hlmCalendar as well
Surface same ones as available to brnCalendar
I think for helm standpoint we can have one component to encompass the entire calendar, since underlying helm is available in end user codebase I was thinking that we could probably leverage the attribute hlm directives more on the brn component pattern.
<brn-x hlm></brn-x>
I think this would work best since there will only be one primary hlm component for calendar but user still has all the flexibility to re-arrange the available brn components in hlm-calendar template. This may result is some added logic/code in hlm however my hope is it will be very simple input pass throughs to brn calendar and maybe some very straight forward conditional rendering.
Not listed but we would still have a hlm directive for each of the brn components below
API (Proposed)
<brn-calendar>
<brn-calendar-header>
<!-- Probably pick one of these Calendar Header Displays or we canjust have a switch case or something -->
<!-- 1. Similar to material will be a button to switch between views-->
<brn-calendar-view-switcher>
<!-- This will render the actual month year or year range content depending on view-->
<brn-calendar-month-year/>
</brn-calendar-view-switcher>
<!-- 2. This will render the actual month year or year range content used standalone-->
<brn-calendar-month-year/>
<!-- 3. Will be similar to select dropdowns in react calendar for month and year-->
<brn-calendar-month-dropdown/>
<brn-calendar-year-dropdown>
<brn-calendar-prev-month-button>
<hlm-icon icon>
</brn-calendar-prev-month-button>
<brn-calendar-next-month-button>
<hlm-icon icon>
</brn-calendar-next-month-button>
</brn-calendar-header>
<!-- row display the days of the week-->
<brn-calendar-weekday/>
<brn-calendar-month-display/>
<!-- Months of the Year -->
<brn-calendar-months-display/>
<!-- Years view display -->
<brn-calendar-year-display/>
<!-- Slot of custom messages or buttons like 'today button' or whatever user wants -->
<brn-calendar-footer></brn-calendar-footer>
</brn-calendar>
Will be the primary parent component and will also be the point of contact for interfacing with the the underlying brn components
For inputs/outputs - from the discussion so far there is a desire for a similar inputs to material so we can definitely use some of the same ones but I was looking to borrow a few from the react-day-picker as well. Especially in regards to some of their localization filters which gives users some fine grain control over localization of specific content on the calendar
Inputs (not all of the inputs but a good base number of inputs we need)
mode = input<'single' | 'multiple' | 'range'>('single');
selectedDate = input<Date | null>(null);
minDate = input<Date | null>(null);
maxDate = input<Date | null>(null);
startAt = input<Date | null>(null);
startView = input<'month' | 'year'>('month');
dateFilter = input<(d: Date) => boolean>();
locale = input<string>(this._brnCalendarService.getLocale());
weekStartsOn = input<1 | 2 | 3 | 4 | 5 | 6 | 7>();
dir = input<'ltr' | 'rtl'>();
Probably just a directive mainly just for placement
Will be a button for actually doign the action of switchiing the calendar views similar to material datepicker
Actually responsible for showing current month year or year range depending on view. Can be used independently if you just want to display that content but not allow actuall switching of views similar to shadcn default calendar
I was thinking of having these button components be independent so user can play around with their placement
can also pass them content although it would primarily be the icon you would want if want a custom icon
Will be a component solely responsible for rendering the days of the week customizations like week start and localization options can be passed directly to this component styling also
7. brn-calendar-display (Component) or brn-calendar-month-display and brn-calendar-year-display (Component)
Responsible for rendering actual calendar debating this one still if we should have 1 component or independent components for each view. I know mat has a component per view so maybe this would help separate all the different styling/locale and other customizations we might allow between the 2 views.
Component to display the singular month (days of the month) and allow day selection
Component to display all the months of the year and allow a specific month selection.
Component to display all the months of the year and allow year selection.
One thing for sure I think we will have a "day" template input with context that a user can use to pass to brn-calendar-display if they want to add some additional css class or special elements/icons depending on if day is active or not, or whatever the use case for customizing the individual days. The best first hand example that i've used is this https://ng-bootstrap.github.io/#/components/datepicker/examples#customday
Probably just a directive mainly just for placement of some additional content
I think I would like to continue a similar pattern to what we used in select. I for the most part liked it although I think some minor improvements from some of my learnings from developing the select component with this pattern.
1. I think its better for top level parent to set the initial state and we do a one time bulk update of the state with all the signal inputs. I saw in select component we ran into a few race conditions and I think this might help mitigate that a little bit especially if we have some sort of initialized flag (still debating the flag and if its even needed here maybe it was just the nature of the select component) as well.
2. Another improvement I think I would rather have some updater functions in the service instead of doing the "this.service.state.update((state) => {...state})" everywhere. I think just having this abstracted in service to make it look like "this.service.updateLocale(locale);" would look much nicer. Very similar to NGRX Component store or SignalState
Sample of what the calendar state might look like:
public readonly state = signal<{
id: string;
mode: 'single' | 'multiple' | 'range';
selectedDate: Date | null;
minDate: Date | null;
maxDate: Date | null;
startAt: Date | null;
startView: 'month' | 'year';
dateFilter?: (d: Date) => boolean | null;
daysOfTheWeek: string[];
currentMonthYear: Date | null;
currentMonthYearDays: Array<Array<Date | null>> | null;
locale: string | null;
}>({
id: '',
mode: 'single',
selectedDate: null,
minDate: null,
maxDate: null,
startAt: null,
startView: 'month',
daysOfTheWeek: [],
currentMonthYear: null,
currentMonthYearDays: null,
locale: null,
});
Similar to select service we will have the computed values to easily access independent pieces of the state
public readonly id = computed(() => this.state().id);
public readonly mode = computed(() => this.state().mode);
public readonly selectedDate = computed(() => this.state().selectedDate);
public readonly minDate = computed(() => this.state().minDate);
public readonly maxDate = computed(() => this.state().maxDate);
public readonly startAt = computed(() => this.state().startAt);
public readonly startView = computed(() => this.state().startView);
public readonly daysOfTheWeek = computed(() => this.state().daysOfTheWeek);
public readonly currentMonthYear = computed(() => this.state().currentMonthYear);
public readonly currentMonthYearDays = computed(() => this.state().currentMonthYearDays);
public readonly locale = computed(() => this.state().locale);
I definitely want to keep it date library agnostic.
I initially thought we would need a date library of some sort but I was checking the mat-datepicker and seems they are able to get away with not having one
https://github.com/angular/components/blob/main/src/material/core/datetime/native-date-adapter.ts
It seems this is all they use but I could be wrong. I did see they do have adapters which could be something we could do also to support some of the date libraries users have in their projects natively. Seeing this file also makes me further believe we can probably get away with just using the Intl API for localization and maybe not need something like the adobe internationalization library
https://www.w3.org/WAI/ARIA/apg/patterns/dialog-modal/examples/datepicker-dialog/
To form-control or not form control As far as outputting the values outside of calendar. I've seen, especially in (non-angular) many will simply emit the selected values and take in selectedDate or selectedDates as an input and not necessarily have a formcontrol attachable to them because usually i've seen most angular implementations dont really a standalone calendar component its usually bundled with the datepicker input so usually that has the formcontrol. But definitely open to whether we should have calendar be able to take in a formcontrol as well even in standalone state. In my experience I have had some scenarios where a calendar was wanted just inline not necessarily with a visible
Definitely want some input on my question regarding brn-calendar-display (7) should we have independent components for the month/year view or just have one over-arching component
With centralizing the setup of the calendar component, setting up various visually different calendars leveraging the one hlm component maybe a bit hard. (Would probably involve some kind of style object input and then map those each of the underlying brn components. Probably be easier for developer to create a separate "hlm" calendar component essentially if it's truly visually very different)
Implementing a form control directly on calendar may prove to be a bit difficult from library implementation standpoint. We might need to traverse multiple levels of components ultimately with datepicker component if we decide to support form control directly on calendar
It's great that you're taking a look at this one! 💯
Is there an specific reason to use a component declaration for the different views (day, week, month, year), instead of just something such as a "variant" or "mode"? In this case, I don't think it makes much sense, most of the time what you want to customize is the cell itself, so instead of having a dayTemplate, it fits more something like cellTemplate, and it would work for all the views while still giving the possibility to dynamically update it along with the mode or variant. What I would suggest in this point is that the template is the whole container, without putting any parent div with styles that would require an extra effort to remove it for custom solutions.
Regarding the FormControl, I find it to be a better solution, taking into account we have it in the Angular context we work on, I think we should take advantage of it, there will be plenty of times where the calendar is going to be used as a bare inline calendar as a filter where an ngModel offers not only the value binding but also the ngModelChange that can be used for other more specific needs (In my experience mostly formatting in these cases)
Also have to say that, on the other hand: would we have two form-controls when using the datepicker? (Datepicker FormControl + Child calendar FormControl) I ain't sure if this is actually something good to do (A solution comes to mind that involves a third component that decouples it but maybe I'm just thinking too much).
Edit:
I think especially if we keep year view and month view separate we can definitely just have each take in a cell template and you can customize it. I assume the templates probably wouldn't be that different.
In general for the explicit component declaration, I think one thing I was hoping to be able to reduce was a lot of calendars you usually have pass some complex object of styles to pass down classes to the internal components. I think being able to have access to, I'll say most items directly is a big plus.
One assumption I think I'm making is I think most people would probably configure their calendar's once per app and maybe have some minor differences/adjustments throughout their app and they can apply those through the hlm component at the specific instance. Although if you need to pass a lot of different classes in a specific instance then we probably can't avoid the class object unless we also allow you pass the specific components optionally as content through the hlmCalendar, which may be possible as well.
For form control that's where I'm also kind of stuck on and cant seem to decide. I think you are definitely right about the advantages of of form control direct on calendar especially in scenarios where it's used inline but I worry, once we put this together with the datepicker input there will be a lot of layered ControlValueAccessors at that point (datepicker --> hlmCalendar --> brnCalendar). I dont think it bad but doesn't seem great so I'm not sure
Yes my understanding is minDate and maxDate determines in general the range of valid dates you wish to allow to be selectable. Then on top of that I've seen calendar components provide a "filter" input so you can further disable additional dates within that range.
I'm thinking Intl api might be enough as well and besides no-dep calendar would be super nice!
First of all nice work of evaluating the different Datepickers.
For point 7, would go with 2 separate Components for years and month view. I guess for me this would be easer to read. How have you planned the year, month and day select in this scenario? When I look at the material Datepicker, it has actually 3 modes.
I especially like the additional functionality for year select, as on shadcn, selecting my birthday is quite a job.
Q: What does the dateFilter do?
For formcontrol my first feeling points to "no formControl", because I also thought that in a form, I would rather use datePicker. I already did not like the FormControl passing from Hlm to Brn, this would then be 3 steps. But I also would support making it a formControl as it provides some benefits.
Nice job putting this all together!
You know your right I missed the month view. I'll add that in there.
DateFilter input allows you to provide a custom filter function to exclude certain dates. Min and maxDate cover a range of valid dates but sometimes you want to simply exclude specific dates so you can provide that.
Maybe we can allow people to provide either an array of dates or pass a function but then we might run into weird date format mismatch or something especially if you have some date library in use which could be resolved with adapters but maybe to keep it simple we can just have user provide a function for filtering out specific dates. I need to double check how react calendar is doing it exactly and I can add an example of it here of the function type
@goetzrobin interested to hear your thoughts on design. Was thinking I can start on the rough implementation and can figure out out some tasks for anyone who wants to help with parts of it so we can hopefully get it done quicker
@goetzrobin @elite-benni - curious what you guys think of this. For the actual component, that renders the table for calendar month days for example. Which of the below ways would you rather that display be set up in the top level hlm component.
Anyone else feel free to chime in, I've been trying to add some basic stylings now just to get a feel for how the customization would work and feel like I needed to decided basically how much do I want to break these components down. AFAIK option 2 is definitely feasible and there shouldn't be an issues.
On a separate note, just want to give a sneak preview got basic navigation of the months and almost got the material like variation calendar navigation working as well. Still a long ways to go but it's a start
https://github.com/user-attachments/assets/af3f7ac3-17e9-4852-834b-5f675a94c197
Nice progress! As long as it is hidden behind a component for the enduser I would vote for Option 2.
Just wanted to share a quick update for anyone following this issue but im hoping to have a draft PR up in next week or so just cleaning up the implementation a bit. By no means complete but I think the basic structure and functionality will be there and I want to get some eyes on it also. I'm really excited though I think this might be the most customizable calendar that will be available without being too complicated I hope
Which scope/s are relevant/related to the feature request?
calendar
Information
TBD
Describe any alternatives/workarounds you're currently using
No response
I would be willing to submit a PR to fix this issue