Skip to content

Commit

Permalink
checkbox hiddenInput improvement
Browse files Browse the repository at this point in the history
  • Loading branch information
TGlide committed May 10, 2024
1 parent 3f3fa65 commit caae3c0
Show file tree
Hide file tree
Showing 6 changed files with 77 additions and 52 deletions.
5 changes: 5 additions & 0 deletions .changeset/silver-pears-punch.md
Original file line number Diff line number Diff line change
@@ -0,0 1,5 @@
---
'@melt-ui/svelte': patch
---

Fix: checkbox hiddenInput not calling change events
35 changes: 11 additions & 24 deletions src/lib/builders/checkbox/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 10,10 @@ import {
toWritableStores,
} from '$lib/internal/helpers/index.js';
import type { Defaults, MeltActionReturn } from '$lib/internal/types.js';
import { derived, writable } from 'svelte/store';
import { derived, readable, writable } from 'svelte/store';
import type { CheckboxEvents } from './events.js';
import type { CreateCheckboxProps } from './types.js';
import { createHiddenInput } from '../hidden-input/create.js';

const defaults = {
disabled: false,
Expand Down Expand Up @@ -68,29 69,15 @@ export function createCheckbox(props?: CreateCheckboxProps) {
},
});

const input = makeElement('checkbox-input', {
stores: [checked, name, value, required, disabled],
returned: ([$checked, $name, $value, $required, $disabled]) => {
return {
type: 'checkbox',
'aria-hidden': true,
hidden: true,
tabindex: -1,
name: $name,
value: $value,
checked: $checked === 'indeterminate' ? false : $checked,
required: $required,
disabled: disabledAttr($disabled),
style: styleToString({
position: 'absolute',
opacity: 0,
'pointer-events': 'none',
margin: 0,
transform: 'translateX(-100%)',
}),
} as const;
},
});
const input = createHiddenInput({
value,
checked,
type: ('checkbox'),
name: name,
disabled: disabled,
required,
prefix: 'checkbox',
})

const isIndeterminate = derived(checked, ($checked) => $checked === 'indeterminate');
const isChecked = derived(checked, ($checked) => $checked === true);
Expand Down
40 changes: 33 additions & 7 deletions src/lib/builders/hidden-input/create.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 6,16 @@ import { toReadableStores } from '$lib/internal/helpers/store/toReadableStores.j
import { omit } from '$lib/internal/helpers/object.js';
import type { Readable } from 'svelte/motion';
import { removeUndefined } from '$lib/internal/helpers/object.js';
import { withGet } from '$lib/internal/helpers/withGet.js';
import { executeCallbacks } from '$lib/internal/helpers/callbacks.js';

const defaults = {
prefix: '',
disabled: readable(false),
required: readable(false),
name: readable(undefined),
type: readable(undefined),
checked: undefined,
} satisfies Partial<CreateHiddenInputProps>;

export function createHiddenInput(props: CreateHiddenInputProps) {
Expand All @@ -20,12 24,21 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
...removeUndefined(props),
} satisfies CreateHiddenInputProps;
const { name: elName } = createElHelpers(withDefaults.prefix);
const { value, name, disabled, required } = toReadableStores(omit(withDefaults, 'prefix'));
const { value, name, disabled, required, type, checked } = toReadableStores(
omit(withDefaults, 'prefix')
);
const nameStore = name as Readable<string | undefined>; // TODO: Remove this cast when types are fixed

const actualChecked = withGet.derived([checked, type], ([$checked, $type]) => {
if ($type === 'checkbox') {
return ($checked === 'indeterminate' ? false : $checked) as boolean;
}
return undefined;
});

const hiddenInput = makeElement(elName('hidden-input'), {
stores: [value, nameStore, disabled, required],
returned: ([$value, $name, $disabled, $required]) => {
stores: [value, nameStore, disabled, required, type, actualChecked],
returned: ([$value, $name, $disabled, $required, $type, $checked]) => {
return {
name: $name,
value: $value?.toString(),
Expand All @@ -34,6 47,8 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
disabled: $disabled,
required: $required,
tabIndex: -1,
type: $type,
checked: $checked,
style: styleToString({
position: 'absolute',
opacity: 0,
Expand All @@ -45,10 60,21 @@ export function createHiddenInput(props: CreateHiddenInputProps) {
},
action: (node: HTMLInputElement) => {
// When value changes, emit a change event
const unsub = value.subscribe((newValue) => {
node.value = newValue;
node.dispatchEvent(new Event('change', { bubbles: true }));
});
const unsub = executeCallbacks(
value.subscribe((newValue) => {
if (type.get() === 'checkbox') {
return;
}
node.value = newValue;
node.dispatchEvent(new Event('change', { bubbles: true }));
}),
actualChecked.subscribe(() => {
if (type.get() !== 'checkbox') {
return;
}
node.dispatchEvent(new Event('change', { bubbles: true }));
})
);

return {
destroy: unsub,
Expand Down
13 changes: 8 additions & 5 deletions src/lib/builders/hidden-input/types.ts
Original file line number Diff line number Diff line change
@@ -1,9 1,12 @@
import type { Readable } from 'svelte/store';
import type { MaybeReadable } from '$lib/internal/types.js';
import type { HTMLInputAttributes } from 'svelte/elements';

export type CreateHiddenInputProps = {
value: Readable<string>;
disabled?: Readable<boolean>;
name?: Readable<string | undefined> | undefined;
required?: Readable<boolean>;
value: MaybeReadable<string>;
disabled?: MaybeReadable<boolean>;
name?: MaybeReadable<string | undefined> | undefined;
required?: MaybeReadable<boolean>;
prefix?: string;
type?: MaybeReadable<HTMLInputAttributes['type']>;
checked?: MaybeReadable<boolean | undefined | 'indeterminate'>;
};
5 changes: 3 additions & 2 deletions src/lib/internal/helpers/store/toReadableStores.ts
Original file line number Diff line number Diff line change
@@ -1,11 1,12 @@
import { readable, type Readable } from 'svelte/store';
import { isReadable } from '../is.js';
import { withGet, type WithGet } from '../withGet.js';
import type { MaybeReadable } from '$lib/internal/types.js';

type TODO = any;

export type ToReadableStores<T extends Record<string, unknown>> = {
[K in keyof T]: T[K] extends Readable<TODO> ? WithGet<T[K]> : WithGet<Readable<T[K]>>;
[K in keyof T]: T[K] extends Readable<TODO> ? WithGet<T[K]> : T[K] extends MaybeReadable<infer U> ? WithGet<Readable<U>> : WithGet<Readable<T[K]>>;
};

/**
Expand All @@ -29,5 30,5 @@ export function toReadableStores<T extends Record<string, unknown>>(
}
});

return result;
return result as any;
}
31 changes: 17 additions & 14 deletions src/lib/internal/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 1,5 @@
import type { ActionReturn } from 'svelte/action';
import type { Readable } from 'svelte/store';

// Check if type are equal or just extends
export type IfEquals<T, U, Y = unknown, N = never> = (<G>() => G extends T ? 1 : 2) extends <
Expand Down Expand Up @@ -39,14 40,14 @@ export type Prettify<T> = { [K in keyof T]: T[K] } & {};

export type Expand<T> = T extends object
? T extends infer O
? { [K in keyof O]: O[K] }
: never
? { [K in keyof O]: O[K] }
: never
: T;

export type ExpandDeep<T> = T extends object
? T extends infer O
? { [K in keyof O]: ExpandDeep<O[K]> }
: never
? { [K in keyof O]: ExpandDeep<O[K]> }
: never
: T;

type Without<T, U> = { [P in Exclude<keyof T, keyof U>]?: never };
Expand All @@ -69,21 70,21 @@ export type MeltActionReturn<Events extends keyof HTMLElementEventMap> = ActionR
undefined,
{
[K in Events as `on:m-${string & K}`]?: K extends keyof HTMLElementEventMap
? MeltEventHandler<HTMLElementEventMap[K]>
: never;
? MeltEventHandler<HTMLElementEventMap[K]>
: never;
}
>;

type CustomMeltComponentEvents<Events extends keyof HTMLElementEventMap> = {
[K in Events as `m-${string & K}`]?: K extends keyof HTMLElementEventMap
? MeltEventHandler<HTMLElementEventMap[K]>
: never;
? MeltEventHandler<HTMLElementEventMap[K]>
: never;
};

export type InternalCustomEvents<Events extends keyof HTMLElementEventMap> = {
[K in Events as K]?: K extends keyof HTMLElementEventMap
? EventHandler<HTMLElementEventMap[K]>
: never;
? EventHandler<HTMLElementEventMap[K]>
: never;
};

type ElementEvents<T> = T extends ReadonlyArray<infer U> ? U : never;
Expand Down Expand Up @@ -118,10 119,12 @@ export type WhenTrue<TrueOrFalse, IfTrue, IfFalse, IfNeither = IfTrue | IfFalse>

export type RenameProperties<T, NewNames extends Partial<Record<keyof T, string>>> = Expand<{
[K in keyof T as K extends keyof NewNames
? NewNames[K] extends PropertyKey
? NewNames[K]
: K
: K]: T[K];
? NewNames[K] extends PropertyKey
? NewNames[K]
: K
: K]: T[K];
}>;

export type NonEmptyArray<T> = [T, ...T[]];

export type MaybeReadable<T> = T | Readable<T>;

0 comments on commit caae3c0

Please sign in to comment.