Skip to main content

leptos_use/
use_calendar.rs

1use crate::core::MaybeRwSignal;
2use chrono::*;
3use default_struct_builder::DefaultBuilder;
4use leptos::prelude::*;
5use std::ops::Deref;
6
7/// Create bare-bone calendar data to use in your component.
8/// See [`UseCalendarOptions`] for options and [`UseCalendarReturn`] for return values.
9///
10/// ## Demo
11///
12/// [Link to Demo](https://github.com/Synphonyte/leptos-use/tree/main/examples/use_calendar)
13///
14/// ## Usage
15///
16/// ```
17/// # use leptos::prelude::*;
18/// # use leptos_use::{use_calendar, UseCalendarReturn};
19/// #
20/// # #[component]
21/// # fn Demo() -> impl IntoView {
22/// let UseCalendarReturn {
23///     dates,
24///     weekdays,
25///     previous_month,
26///     today,
27///     month_by_date,
28///     next_month
29/// } = use_calendar();
30/// #
31/// # view! {
32/// # }
33/// # }
34/// ```
35///
36/// Use [`use_calendar_with_options`] to change the initial date and first day of the week.
37///
38/// ```
39/// # use leptos::prelude::*;
40/// # use chrono::NaiveDate;
41/// # use leptos_use::{use_calendar_with_options, UseCalendarReturn, UseCalendarOptions};
42/// #
43/// # #[component]
44/// # fn Demo() -> impl IntoView {
45/// let initial_date = RwSignal::new(
46///     Some(NaiveDate::from_ymd_opt(2022, 1, 1).unwrap())
47/// );
48///
49/// let options = UseCalendarOptions::default()
50///     .first_day_of_the_week(6)
51///     .initial_date(initial_date);
52///
53/// let UseCalendarReturn {
54///     dates,
55///     weekdays,
56///     previous_month,
57///     today,
58///     month_by_date,
59///     next_month
60/// } = use_calendar_with_options(options);
61/// #
62/// # view! {
63/// # }
64/// # }
65/// ```
66///
67///
68/// ## Server-Side Rendering
69///
70/// > Make sure you follow the [instructions in Server-Side Rendering](https://leptos-use.rs/server_side_rendering.html).
71///
72/// Not tested yet.
73// #[doc(cfg(feature = "use_calendar"))]
74pub fn use_calendar() -> UseCalendarReturn<
75    impl Fn() + Clone + Send + Sync,
76    impl Fn() + Clone + Send + Sync,
77    impl Fn(&NaiveDate) + Clone + Send + Sync,
78    impl Fn() + Clone + Send + Sync,
79> {
80    use_calendar_with_options(UseCalendarOptions::default())
81}
82
83/// Version of [`use_calendar`] that takes a [`UseCalendarOptions`]. See [`use_calendar`] for how to use.
84// #[doc(cfg(feature = "use_calendar"))]
85pub fn use_calendar_with_options(
86    options: UseCalendarOptions,
87) -> UseCalendarReturn<
88    impl Fn() + Clone + Send + Sync,
89    impl Fn() + Clone + Send + Sync,
90    impl Fn(&NaiveDate) + Clone + Send + Sync,
91    impl Fn() + Clone + Send + Sync,
92> {
93    let UseCalendarOptions {
94        initial_date: date,
95        first_day_of_the_week,
96    } = options;
97    let (date, _set_date) = date.into_signal();
98
99    let show_date = RwSignal::new(date.get_untracked().unwrap_or(Local::now().date_naive()));
100    Effect::new(move |_| {
101        if let Some(selected_date) = date.get() {
102            let show_date_data = show_date.get_untracked();
103            if selected_date.year() != show_date_data.year()
104                || selected_date.month() != show_date_data.month()
105            {
106                show_date.set(selected_date);
107            }
108        }
109    });
110
111    let dates = Memo::new(move |_| {
112        let show_date = show_date.get();
113        let show_date_month = show_date.month();
114        let mut dates = vec![];
115
116        let mut current_date = show_date;
117        let mut current_weekday_number = None::<u32>;
118        loop {
119            let date = current_date - Days::new(1);
120            if date.month() != show_date_month {
121                if current_weekday_number.is_none() {
122                    current_weekday_number = Some(
123                        current_date.weekday().days_since(
124                            Weekday::try_from(first_day_of_the_week.get() as u8)
125                                .unwrap_or(Weekday::Mon),
126                        ),
127                    );
128                }
129                let weekday_number = current_weekday_number.unwrap();
130                if weekday_number == 0 {
131                    break;
132                }
133                current_weekday_number = Some(weekday_number - 1);
134
135                dates.push(CalendarDate::Previous(date));
136            } else {
137                dates.push(CalendarDate::Current(date));
138            }
139            current_date = date;
140        }
141        dates.reverse();
142        dates.push(CalendarDate::Current(show_date));
143        current_date = show_date;
144        current_weekday_number = None;
145        loop {
146            let date = current_date + Days::new(1);
147            if date.month() != show_date_month {
148                if current_weekday_number.is_none() {
149                    current_weekday_number = Some(
150                        current_date.weekday().days_since(
151                            Weekday::try_from(first_day_of_the_week.get() as u8)
152                                .unwrap_or(Weekday::Mon),
153                        ),
154                    );
155                }
156                let weekday_number = current_weekday_number.unwrap();
157                if weekday_number == 6 {
158                    break;
159                }
160                current_weekday_number = Some(weekday_number + 1);
161                dates.push(CalendarDate::Next(date));
162            } else {
163                dates.push(CalendarDate::Current(date));
164            }
165            current_date = date;
166        }
167        dates
168    });
169
170    let weekdays = Memo::<Vec<usize>>::new(move |_| {
171        if Weekday::try_from(first_day_of_the_week.get() as u8).is_ok() {
172            let first_weekdays = first_day_of_the_week.get()..7;
173            let last_weekdays = 0..first_day_of_the_week.get();
174            first_weekdays.chain(last_weekdays).collect()
175        } else {
176            (0..7).collect()
177        }
178    });
179
180    UseCalendarReturn {
181        previous_month: move || {
182            show_date.update(|date| {
183                *date = *date - Months::new(1);
184            });
185        },
186        today: move || {
187            show_date.set(Local::now().date_naive());
188        },
189        month_by_date: move |new_date: &NaiveDate| {
190            show_date.set(*new_date);
191        },
192
193        next_month: move || {
194            show_date.update(|date| {
195                *date = *date + Months::new(1);
196            });
197        },
198        weekdays: weekdays.into(),
199        dates: dates.into(),
200    }
201}
202
203/// Options for [`use_calendar_with_options`].
204// #[doc(cfg(feature = "use_calendar"))]
205#[derive(DefaultBuilder)]
206pub struct UseCalendarOptions {
207    /// Date being used to initialize the calendar month to be displayed.
208    /// Optional [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html). Defaults to [`chrono::Local::now()`](https://docs.rs/chrono/latest/chrono/struct.Local.html#method.now).
209    #[builder(into)]
210    pub initial_date: MaybeRwSignal<Option<NaiveDate>>,
211    /// First day of the week as a number from 0 to 6. Defaults to 0 (Monday).
212    #[builder(into)]
213    pub first_day_of_the_week: Signal<usize>,
214}
215
216impl Default for UseCalendarOptions {
217    fn default() -> Self {
218        Self {
219            initial_date: Some(Local::now().date_naive()).into(),
220            first_day_of_the_week: 0.into(),
221        }
222    }
223}
224
225/// Return type of [`use_calendar`].
226// #[doc(cfg(feature = "use_calendar"))]
227pub struct UseCalendarReturn<PreviousMonthFn, TodayFn, MonthByDateFn, NextMonthFn>
228where
229    PreviousMonthFn: Fn() + Clone + Send + Sync,
230    TodayFn: Fn() + Clone + Send + Sync,
231    MonthByDateFn: Fn(&NaiveDate) + Clone + Send + Sync,
232    NextMonthFn: Fn() + Clone + Send + Sync,
233{
234    /// A function to go to the previous month.
235    pub previous_month: PreviousMonthFn,
236    /// A function to go to the current month.
237    pub today: TodayFn,
238    /// A function to go to the month by date.
239    pub month_by_date: MonthByDateFn,
240    /// A function to go to the next month.
241    pub next_month: NextMonthFn,
242    /// The first day of the week as a number from 0 to 6.
243    pub weekdays: Signal<Vec<usize>>,
244    /// A `Vec` of [`CalendarDate`]s representing the dates in the current month.
245    pub dates: Signal<Vec<CalendarDate>>,
246}
247
248/// Utility enum to represent a calendar date. Implements [`Deref`] to [`chrono::NaiveDate`](https://docs.rs/chrono/latest/chrono/struct.NaiveDate.html).
249#[derive(Clone, Copy, PartialEq)]
250pub enum CalendarDate {
251    Previous(NaiveDate),
252    Current(NaiveDate),
253    Next(NaiveDate),
254}
255
256impl CalendarDate {
257    pub fn is_other_month(&self) -> bool {
258        match self {
259            CalendarDate::Previous(_) | CalendarDate::Next(_) => true,
260            CalendarDate::Current(_) => false,
261        }
262    }
263    pub fn is_today(&self) -> bool {
264        let date = self.deref();
265        let now_date = Local::now().date_naive();
266        &now_date == date
267    }
268
269    pub fn is_selected(&self, selected_date: &NaiveDate) -> bool {
270        self.deref() == selected_date
271    }
272
273    pub fn is_before(&self, date: &NaiveDate) -> bool {
274        self.deref() < date
275    }
276
277    pub fn is_between(&self, start_date: &NaiveDate, end_date: &NaiveDate) -> bool {
278        let date = self.deref();
279        date >= start_date && date <= end_date
280    }
281
282    pub fn is_between_current_month(&self, start_date: &NaiveDate, end_date: &NaiveDate) -> bool {
283        match self {
284            CalendarDate::Current(date) => date >= start_date && date <= end_date,
285            CalendarDate::Next(date) => date > start_date && date <= end_date,
286            CalendarDate::Previous(date) => date >= start_date && date < end_date,
287        }
288    }
289
290    pub fn is_after(&self, date: &NaiveDate) -> bool {
291        self.deref() > date
292    }
293
294    pub fn is_first_day_of_month(&self) -> bool {
295        let date = self.deref();
296        if let Some(prev_date) = date.pred_opt() {
297            date.month() != prev_date.month()
298        } else {
299            true
300        }
301    }
302
303    pub fn is_last_day_of_month(&self) -> bool {
304        let date = self.deref();
305        if let Some(next_date) = date.succ_opt() {
306            date.month() != next_date.month()
307        } else {
308            true
309        }
310    }
311}
312
313impl Deref for CalendarDate {
314    type Target = NaiveDate;
315
316    fn deref(&self) -> &Self::Target {
317        match self {
318            CalendarDate::Previous(date)
319            | CalendarDate::Current(date)
320            | CalendarDate::Next(date) => date,
321        }
322    }
323}