|
| 1 | +/** Set up the document to render a component. */ |
| 2 | +import { addCleanupTask } from './cleanup.js' |
| 3 | +import { IS_MODERN_SVELTE } from './svelte-version.js' |
| 4 | + |
| 5 | +/** Allowed options to the `mount` call or legacy component constructor. */ |
| 6 | +const ALLOWED_MOUNT_OPTIONS = IS_MODERN_SVELTE |
| 7 | + ? ['target', 'anchor', 'props', 'events', 'context', 'intro'] |
| 8 | + : ['target', 'accessors', 'anchor', 'props', 'hydrate', 'intro', 'context'] |
| 9 | + |
| 10 | +class UnknownSvelteOptionsError extends TypeError { |
| 11 | + constructor(unknownOptions) { |
| 12 | + super(`Unknown options. |
| 13 | +
|
| 14 | + Unknown: [ ${unknownOptions.join(', ')} ] |
| 15 | + Allowed: [ ${ALLOWED_MOUNT_OPTIONS.join(', ')} ] |
| 16 | +
|
| 17 | + To pass both Svelte options and props to a component, |
| 18 | + or to use props that share a name with a Svelte option, |
| 19 | + you must place all your props under the \`props\` key: |
| 20 | +
|
| 21 | + render(Component, { props: { /** props here **/ } }) |
| 22 | +`) |
| 23 | + this.name = 'UnknownSvelteOptionsError' |
| 24 | + } |
| 25 | +} |
| 26 | + |
| 27 | +/** |
| 28 | + * Validate a component's mount options. |
| 29 | + * |
| 30 | + * @template {import('./types.js').Component} C |
| 31 | + * @param {import('./types.js').ComponentOptions<C>} options - props or mount options |
| 32 | + * @returns {Partial<import('./types.js').MountOptions<C>>} |
| 33 | + */ |
| 34 | +const validateOptions = (options) => { |
| 35 | + const isProps = !Object.keys(options).some((option) => |
| 36 | + ALLOWED_MOUNT_OPTIONS.includes(option) |
| 37 | + ) |
| 38 | + |
| 39 | + if (isProps) { |
| 40 | + return { props: options } |
| 41 | + } |
| 42 | + |
| 43 | + // Check if any props and Svelte options were accidentally mixed. |
| 44 | + const unknownOptions = Object.keys(options).filter( |
| 45 | + (option) => !ALLOWED_MOUNT_OPTIONS.includes(option) |
| 46 | + ) |
| 47 | + |
| 48 | + if (unknownOptions.length > 0) { |
| 49 | + throw new UnknownSvelteOptionsError(unknownOptions) |
| 50 | + } |
| 51 | + |
| 52 | + return options |
| 53 | +} |
| 54 | + |
| 55 | +/** |
| 56 | + * Set up the document to render a component. |
| 57 | + * |
| 58 | + * @template {import('./types.js').Component} C |
| 59 | + * @param {import('./types.js').ComponentOptions<C>} componentOptions - props or mount options |
| 60 | + * @param {{ baseElement?: HTMLElement | undefined }} setupOptions - base element of the document to bind any queries |
| 61 | + * @returns {{ |
| 62 | + * baseElement: HTMLElement, |
| 63 | + * target: HTMLElement, |
| 64 | + * mountOptions: import('./types.js).MountOptions<C> |
| 65 | + * }} |
| 66 | + */ |
| 67 | +const setup = (componentOptions, setupOptions) => { |
| 68 | + const mountOptions = validateOptions(componentOptions) |
| 69 | + |
| 70 | + const baseElement = |
| 71 | + setupOptions.baseElement ?? mountOptions.target ?? document.body |
| 72 | + |
| 73 | + const target = |
| 74 | + mountOptions.target ?? |
| 75 | + baseElement.appendChild(document.createElement('div')) |
| 76 | + |
| 77 | + addCleanupTask(() => { |
| 78 | + if (target.parentNode === document.body) { |
| 79 | + target.remove() |
| 80 | + } |
| 81 | + }) |
| 82 | + |
| 83 | + return { baseElement, target, mountOptions: { ...mountOptions, target } } |
| 84 | +} |
| 85 | + |
| 86 | +export { setup, UnknownSvelteOptionsError } |
0 commit comments