Repository: melt-ui/melt-ui (ts)

Issue:
[Calendar & related] Updating options doesn't update values
### Describe the bug

When you try to update some value of some option that would change the calendar (specifically `numberOfMonths`, `fixedWeeks`, `weekStartsOn`) the calendar is not updated until the month or the date is changed.

I've already investigated this and it all boils down to 

https://github.com/melt-ui/melt-ui/blob/746ee7c65dea907f64af3314d51d7096fa0c0b7f/src/lib/builders/calendar/create.ts#L524-L530

the effect is responsible to recalculate the months but it bails out if the current placeholder is already in the visibleMonths. Removing this solves the issue but i suspect will do a bit more work in general. I think this check should be in place only if the placeholder is the only thing that changed.

I can also work on this and open a PR, just wanted a bit more insight on this if (wink wink @huntabyte )

### Reproduction

To test the fixed weeks not working obviously set to a month were fixed would work

https://www.sveltelab.dev/44fmd3k81k63jen

### Logs

_No response_

### System Info

```bash
  System:
    OS: Linux 5.0 undefined
    CPU: (8) x64 Intel(R) Core(TM) i9-9880H CPU @ 2.30GHz
    Memory: 0 Bytes / 0 Bytes
    Shell: 1.0 - /bin/jsh
  Binaries:
    Node: 18.18.0 - /usr/local/bin/node
    Yarn: 1.22.19 - /usr/local/bin/yarn
    npm: 10.2.3 - /usr/local/bin/npm
    pnpm: 8.15.3 - /usr/local/bin/pnpm
  npmPackages:
    @melt-ui/svelte: latest => 0.75.3 
    @sveltejs/kit: ^2.0.0 => 2.5.2 
    svelte: ^4.1.2 => 4.2.12 
```


### Severity

blocking an upgrade

Pull request description:
fix: months not updating when changing options
Closes #1069 

Patch:
diff --git a/.changeset/ninety-oranges-collect.md b/.changeset/ninety-oranges-collect.md
new file mode 100644
index 00000000..a4db71c0
--- /dev/null
+++ b/.changeset/ninety-oranges-collect.md
@@ -0,0 +1,5 @@
+---
+"@melt-ui/svelte": patch
+---
+
+fix: months not updating when changing options
diff --git a/src/lib/builders/calendar/create.ts b/src/lib/builders/calendar/create.ts
index b3073b0a..0c61962c 100644
--- a/src/lib/builders/calendar/create.ts
+++ b/src/lib/builders/calendar/create.ts
@@ -514,20 +514,48 @@ export function createCalendar<
 	 * Updates the displayed months based on changes in the placeholder value,
 	 * which determines the months to show in the calendar.
 	 */
-	effect(
-		[placeholder, weekStartsOn, locale, fixedWeeks, numberOfMonths],
-		([$placeholder, $weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
-			if (!isBrowser || !$placeholder) return;
+	effect([placeholder], ([$placeholder]) => {
+		if (!isBrowser || !$placeholder) return;
 
-			const $visibleMonths = visibleMonths.get();
+		const $visibleMonths = visibleMonths.get();
 
-			/**
-			 * If the placeholder's month is already in the visible months,
-			 * we don't need to do anything.
-			 */
-			if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
-				return;
-			}
+		/**
+		 * If the placeholder's month is already in the visible months,
+		 * we don't need to do anything.
+		 */
+		if ($visibleMonths.some((month) => isSameMonth(month, $placeholder))) {
+			return;
+		}
+
+		const $weekStartsOn = weekStartsOn.get();
+		const $locale = locale.get();
+		const $fixedWeeks = fixedWeeks.get();
+		const $numberOfMonths = numberOfMonths.get();
+
+		const defaultMonthProps = {
+			weekStartsOn: $weekStartsOn,
+			locale: $locale,
+			fixedWeeks: $fixedWeeks,
+			numberOfMonths: $numberOfMonths,
+		};
+
+		months.set(
+			createMonths({
+				...defaultMonthProps,
+				dateObj: $placeholder,
+			})
+		);
+	});
+
+	/**
+	 * Updates the displayed months based on changes in the the options values,
+	 * which determines the months to show in the calendar.
+	 */
+	effect(
+		[weekStartsOn, locale, fixedWeeks, numberOfMonths],
+		([$weekStartsOn, $locale, $fixedWeeks, $numberOfMonths]) => {
+			const $placeholder = placeholder.get();
+			if (!isBrowser || !$placeholder) return;
 
 			const defaultMonthProps = {
 				weekStartsOn: $weekStartsOn,


Test patch:
diff --git a/src/tests/calendar/Calendar.spec.ts b/src/tests/calendar/Calendar.spec.ts
index 9cca7573..d47f19e3 100644
--- a/src/tests/calendar/Calendar.spec.ts
+++ b/src/tests/calendar/Calendar.spec.ts
@@ -1,14 +1,14 @@
-import { testKbd as kbd } from '../utils.js';
+import type { CreateCalendarProps } from '$lib/builders/index.js';
+import { CalendarDate, CalendarDateTime, toZoned, type DateValue } from '@internationalized/date';
 import { render } from '@testing-library/svelte';
 import { userEvent } from '@testing-library/user-event';
 import { axe } from 'jest-axe';
-import { describe } from 'vitest';
-import CalendarTest from './CalendarTest.svelte';
-import { CalendarDate, CalendarDateTime, toZoned, type DateValue } from '@internationalized/date';
-import { writable } from 'svelte/store';
 import { tick } from 'svelte';
+import { writable } from 'svelte/store';
+import { describe } from 'vitest';
+import { testKbd as kbd } from '../utils.js';
 import CalendarMultiTest from './CalendarMultiTest.svelte';
-import type { CreateCalendarProps } from '$lib/builders/index.js';
+import CalendarTest from './CalendarTest.svelte';
 
 const calendarDate = new CalendarDate(1980, 1, 20);
 const calendarDateTime = new CalendarDateTime(1980, 1, 20, 12, 30, 0, 0);
@@ -679,6 +679,75 @@ describe('Calendar', () => {
 		}
 	});
 
+	test('dynamically change numberOfMonths option', async () => {
+		const { queryByTestId, getByTestId, user } = setup();
+
+		let grid0 = queryByTestId('grid-0');
+		let grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeNull();
+
+		const numberOfMonthsButton = getByTestId('numberOfMonths');
+		await user.click(numberOfMonthsButton);
+
+		grid0 = queryByTestId('grid-0');
+		grid1 = queryByTestId('grid-1');
+		expect(grid0).toBeInTheDocument();
+		expect(grid1).toBeInTheDocument();
+	});
+
+	test('dynamically change weekStartsOn option', async () => {
+		const { getByTestId, user } = setup();
+
+		const weekDaysCopy = [...narrowWeekdays];
+
+		let weekdayElement = getByTestId(`weekdays`);
+
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+
+		const weekStartsOnButton = getByTestId('weekStartsOn');
+		await user.click(weekStartsOnButton);
+		const first = weekDaysCopy.shift();
+		if (first) {
+			weekDaysCopy.push(first);
+		}
+
+		weekdayElement = getByTestId(`weekdays`);
+		for (let i = 0; i < weekDaysCopy.length; i++) {
+			expect(weekdayElement.children[i]).toHaveTextContent(weekDaysCopy[i]);
+		}
+	});
+
+	test('dynamically change weekStartsOn option', async () => {
+		const { getByTestId, queryByTestId, user } = setup();
+
+		const nextButton = getByTestId('next-button');
+
+		while (queryByTestId('week-6') !== null) {
+			await user.click(nextButton);
+		}
+
+		const fixedWeeksButton = getByTestId('fixedWeeks');
+		await user.click(fixedWeeksButton);
+
+		expect(queryByTestId('week-6')).not.toBeNull();
+	});
+
+	test('dynamically changing the locale option also update weekStartsOn', async () => {
+		const { getByTestId, user } = setup();
+
+		let weekdayElement = getByTestId(`weekday-0`);
+		expect(weekdayElement).toHaveTextContent('S');
+
+		const localeButton = getByTestId('locale');
+		await user.click(localeButton);
+		weekdayElement = getByTestId(`weekday-0`);
+		// L is italian for Lunedì which is Monday (which is different from S which is Sunday)
+		expect(weekdayElement).toHaveTextContent('L');
+	});
+
 	test('custom ids are applied when provided', async () => {
 		const ids = {
 			accessibleHeading: 'id-heading',
diff --git a/src/tests/calendar/CalendarTest.svelte b/src/tests/calendar/CalendarTest.svelte
index 7159efac..81b5246f 100644
--- a/src/tests/calendar/CalendarTest.svelte
+++ b/src/tests/calendar/CalendarTest.svelte
@@ -30,7 +30,13 @@
 	const {
 		elements: { calendar, heading, grid, cell, prevButton, nextButton },
 		states: { value: insideValue, months, headingValue, weekdays },
-		options: { weekdayFormat: weekdayFormatOption },
+		options: {
+			weekdayFormat: weekdayFormatOption,
+			numberOfMonths: numberOfMonthsOption,
+			fixedWeeks: fixedWeeksOption,
+			weekStartsOn: weekStartsOnOption,
+			locale: localeOption,
+		},
 	} = createCalendar(
 		removeUndefined({
 			value,
@@ -58,6 +64,10 @@
 		})
 	);
 
+	function cycleWeekStart() {
+		$weekStartsOnOption = (($weekStartsOnOption + 1) % 7) as typeof $weekStartsOnOption;
+	}
+
 	function cycleWeekdayFormat() {
 		weekdayFormatOption.update((prev) => {
 			switch (prev) {
@@ -95,7 +105,7 @@
 					{#each $months as month, i}
 						<table use:melt={$grid} class="w-full" data-testid="grid-{i}">
 							<thead aria-hidden="true">
-								<tr>
+								<tr data-testid="weekdays">
 									{#each $weekdays as day, idx}
 										<th class="text-sm font-semibold text-magnum-800">
 											<div
@@ -134,6 +144,28 @@
 	<button on:click={cycleWeekdayFormat} data-testid="cycle-weekday-format">
 		Cycle weekdayFormat
 	</button>
+	<button
+		data-testid="numberOfMonths"
+		on:click={() => {
+			$numberOfMonthsOption++;
+		}}>numberOfMonths</button
+	>
+	<br />
+	<button data-testid="weekStartsOn" on:click={cycleWeekStart}>weekStartsOn</button>
+	<br />
+	<button
+		data-testid="fixedWeeks"
+		on:click={() => {
+			$fixedWeeksOption = !$fixedWeeksOption;
+		}}>fixedWeeksOption</button
+	>
+	<br />
+	<button
+		data-testid="locale"
+		on:click={() => {
+			$localeOption = 'it';
+		}}>setLocaleToIt</button
+	>
 </main>
 
 <style lang="postcss">