diff --git a/.eslintrc.js b/.eslintrc.js index a093de610b35..0d3fb5b3c4ed 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -7,6 +7,7 @@ module.exports = { 'svelte/internal', 'svelte/store', 'svelte/easing', + 'svelte/slot', 'estree' ], 'svelte3/compiler': require('./compiler') diff --git a/.gitignore b/.gitignore index a471c3aaa29c..f1873bc62530 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ node_modules /motion /transition /animate +/slot /scratch/ /coverage/ /coverage.lcov diff --git a/package-lock.json b/package-lock.json index 9af7f71cc6f9..6ebec157b7c7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -179,9 +179,9 @@ } }, "@rollup/plugin-sucrase": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@rollup/plugin-sucrase/-/plugin-sucrase-3.0.0.tgz", - "integrity": "sha512-sUQkoAXdw+bnd/cNZHGy5yQKW6OYYU7QlYBGhReI95uZljxO8t1LlbqCO2viIMV/u9pcCjgi8N9PcApcrJCA8Q==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@rollup/plugin-sucrase/-/plugin-sucrase-3.1.0.tgz", + "integrity": "sha512-PZ70LDNgIj8rL+3pKwKwTBOQ2c9JofXeLbWz+2V4/nCt4LqwYTNqxJJf1riTJsVARVzJdA0woIzUzjKZvL8TfA==", "dev": true, "requires": { "@rollup/pluginutils": "^3.0.1", @@ -996,9 +996,9 @@ } }, "commander": { - "version": "2.20.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.0.tgz", - "integrity": "sha512-7j2y+40w61zy6YC2iRNpUe/NwhNyoXrYpHMrSunaMG64nRnaf96zO/KMQR4OyN/UnE5KLyEBnKHd4aG3rskjpQ==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", "dev": true }, "concat-map": { @@ -4330,15 +4330,33 @@ "dev": true }, "sucrase": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.10.1.tgz", - "integrity": "sha512-nMOs6rFWwkYRxcKHHDjyQmC5CmLbHN2LwRyWF1n2i0kb/pq0xcB9M19TdY5Ivfcj1BsWfs+az9Ga5B0tFdE5ww==", + "version": "3.16.0", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.16.0.tgz", + "integrity": "sha512-ovVuswxV5TayCPXfTk8bgBgk6uNRvsinIkEpq0J6zS1xXCx5N/LLGcbsKdRhqn/ToZylMX6+yXaR1LSn1I42Pg==", "dev": true, "requires": { - "commander": "^2.19.0", + "commander": "^4.0.0", + "glob": "7.1.6", "lines-and-columns": "^1.1.6", "mz": "^2.7.0", - "pirates": "^4.0.0" + "pirates": "^4.0.1", + "ts-interface-checker": "^0.1.9" + }, + "dependencies": { + "glob": { + "version": "7.1.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", + "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + } + } } }, "supports-color": { @@ -4427,9 +4445,9 @@ "dev": true }, "thenify": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.0.tgz", - "integrity": "sha1-5p44obq+lpsBCCB5eLn2K4hgSDk=", + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", "dev": true, "requires": { "any-promise": "^1.0.0" @@ -4483,6 +4501,12 @@ "punycode": "^2.1.0" } }, + "ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true + }, "tsconfig-paths": { "version": "3.9.0", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.9.0.tgz", @@ -4547,9 +4571,9 @@ "dev": true }, "typescript": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.5.3.tgz", - "integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==", + "version": "3.9.7", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.7.tgz", + "integrity": "sha512-BLbiRkiBzAwsjut4x/dsibSTB6yWpwT5qWmC2OfuCg3GgVQCSgMs4vEctYPhsaGtd0AeuuHMkjZ2h2WG8MSzRw==", "dev": true }, "uri-js": { diff --git a/package.json b/package.json index d31f368e5261..15e2a7c2b81f 100644 --- a/package.json +++ b/package.json @@ -99,7 +99,7 @@ "@rollup/plugin-json": "^4.0.1", "@rollup/plugin-node-resolve": "^6.0.0", "@rollup/plugin-replace": "^2.3.0", - "@rollup/plugin-sucrase": "^3.0.0", + "@rollup/plugin-sucrase": "^3.1.0", "@rollup/plugin-typescript": "^2.0.1", "@rollup/plugin-virtual": "^2.0.0", "@sveltejs/eslint-config": "github:sveltejs/eslint-config#v5.6.0", @@ -131,7 +131,7 @@ "sourcemap-codec": "^1.4.8", "tiny-glob": "^0.2.6", "tslib": "^1.10.0", - "typescript": "^3.5.3" + "typescript": "^3.9.7" }, "nyc": { "include": [ diff --git a/site/content/docs/03-run-time.md b/site/content/docs/03-run-time.md index 386c18d5fd75..607256225d1a 100644 --- a/site/content/docs/03-run-time.md +++ b/site/content/docs/03-run-time.md @@ -906,6 +906,7 @@ The following initialisation options can be provided: | `props` | `{}` | An object of properties to supply to the component | `hydrate` | `false` | See below | `intro` | `false` | If `true`, will play transitions on initial render, rather than waiting for subsequent state changes +| `slots` | `{}` | An object with keys - slot names, values - element or array of elements Existing children of `target` are left where they are. diff --git a/src/runtime/index.ts b/src/runtime/index.ts index b3451ed5cbf0..c768c6323e4b 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -13,3 +13,5 @@ export { SvelteComponentDev as SvelteComponent, SvelteComponentTyped } from 'svelte/internal'; + +export { createSlot, slot } from 'svelte/slot'; diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index c503a507ffd2..d0e1dd0be9d5 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -4,27 +4,30 @@ import { blank_object, is_empty, is_function, run, run_all, noop } from './utils import { children, detach } from './dom'; import { transition_in } from './transitions'; -interface Fragment { - key: string|null; - first: null; +export interface FragmentMinimal { /* create */ c: () => void; /* claim */ l: (nodes: any) => void; + /* mount */ m: (target: HTMLElement, anchor: HTMLElement) => void; + /* destroy */ d: (detaching: 0|1) => void; + +} +interface Fragment extends FragmentMinimal { + key: string|null; + first: null; /* hydrate */ h: () => void; - /* mount */ m: (target: HTMLElement, anchor: any) => void; /* update */ p: (ctx: any, dirty: any) => void; /* measure */ r: () => void; /* fix */ f: () => void; /* animate */ a: () => void; /* intro */ i: (local: any) => void; /* outro */ o: (local: any) => void; - /* destroy */ d: (detaching: 0|1) => void; } interface T$$ { dirty: number[]; - ctx: null|any; + ctx?: any; bound: any; update: () => void; - callbacks: any; + callbacks: Record; after_update: any[]; props: Record; fragment: null|false|Fragment; @@ -36,7 +39,7 @@ interface T$$ { skip_bound: boolean; } -export function bind(component, name, callback) { +export function bind(component: SvelteComponent, name, callback) { const index = component.$$.props[name]; if (index !== undefined) { component.$$.bound[index] = callback; @@ -52,7 +55,7 @@ export function claim_component(block, parent_nodes) { block && block.l(parent_nodes); } -export function mount_component(component, target, anchor) { +export function mount_component(component: SvelteComponent, target, anchor) { const { fragment, on_mount, on_destroy, after_update } = component.$$; fragment && fragment.m(target, anchor); @@ -73,7 +76,7 @@ export function mount_component(component, target, anchor) { after_update.forEach(add_render_callback); } -export function destroy_component(component, detaching) { +export function destroy_component(component: SvelteComponent, detaching) { const $$ = component.$$; if ($$.fragment !== null) { run_all($$.on_destroy); @@ -87,7 +90,7 @@ export function destroy_component(component, detaching) { } } -function make_dirty(component, i) { +function make_dirty(component: SvelteComponent, i) { if (component.$$.dirty[0] === -1) { dirty_components.push(component); schedule_update(); @@ -96,11 +99,33 @@ function make_dirty(component, i) { component.$$.dirty[(i / 31) | 0] |= (1 << (i % 31)); } -export function init(component, options, instance, create_fragment, not_equal, props, dirty = [-1]) { +export type Props = Record; + +export type SvelteSlotOptions = { + props?: Props; +} + +export type SvelteComponentOptions = { + target: Element; + anchor?: Element; + hydrate?: boolean; + intro?: boolean; + slots?: unknown; +} & SvelteSlotOptions; + +export type SvelteComponentOptionsPrivate = { + target?: Element; + $$inline?: boolean; +} & SvelteComponentOptions; + +export function init(component: SvelteComponent, options: SvelteComponentOptions, instance, create_fragment, not_equal, props: Props, dirty = [-1]) { const parent_component = current_component; set_current_component(component); const prop_values = options.props || {}; + if (options.slots) { + prop_values.$$slots = options.slots; + } const $$: T$$ = component.$$ = { fragment: null, @@ -164,9 +189,7 @@ export function init(component, options, instance, create_fragment, not_equal, p set_current_component(parent_component); } -export let SvelteElement; -if (typeof HTMLElement === 'function') { - SvelteElement = class extends HTMLElement { +export class SvelteElement extends HTMLElement { $$: T$$; $$set?: ($$props: any) => void; constructor() { @@ -209,8 +232,7 @@ if (typeof HTMLElement === 'function') { this.$$.skip_bound = false; } } - }; -} + } /** * Base class for Svelte components. Used when dev=false. @@ -224,7 +246,7 @@ export class SvelteComponent { this.$destroy = noop; } - $on(type, callback) { + $on(type: string, callback: CallableFunction) { const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = [])); callbacks.push(callback); diff --git a/src/runtime/internal/dev.ts b/src/runtime/internal/dev.ts index 53f6ef3bf1ba..e36ca7f6f717 100644 --- a/src/runtime/internal/dev.ts +++ b/src/runtime/internal/dev.ts @@ -1,5 +1,5 @@ import { custom_event, append, insert, detach, listen, attr } from './dom'; -import { SvelteComponent } from './Component'; +import { SvelteComponent, Props, SvelteComponentOptions, SvelteComponentOptionsPrivate } from './Component'; export function dispatch_dev(type: string, detail?: T) { document.dispatchEvent(custom_event(type, { version: '__VERSION__', ...detail })); @@ -97,7 +97,6 @@ export function validate_slots(name, slot, keys) { } } -type Props = Record; export interface SvelteComponentDev { $set(props?: Props): void; $on(event: string, callback: (event: any) => void): () => void; @@ -116,15 +115,9 @@ export class SvelteComponentDev extends SvelteComponent { */ $$prop_def: Props; - constructor(options: { - target: Element; - anchor?: Element; - props?: Props; - hydrate?: boolean; - intro?: boolean; - $$inline?: boolean; - }) { - if (!options || (!options.target && !options.$$inline)) { + constructor(options: SvelteComponentOptions) { + const privateOptions: SvelteComponentOptionsPrivate = options; + if (!privateOptions || (!privateOptions.target && !privateOptions.$$inline)) { throw new Error("'target' is a required option"); } diff --git a/src/runtime/internal/utils.ts b/src/runtime/internal/utils.ts index 084019fb5989..4b8e7e5444c9 100644 --- a/src/runtime/internal/utils.ts +++ b/src/runtime/internal/utils.ts @@ -79,10 +79,10 @@ export function create_slot(definition, ctx, $$scope, fn) { } } -export function get_slot_context(definition, ctx, $$scope, fn) { +export function get_slot_context(definition, ctx, $$scope: {ctx: unknown[]} | undefined, fn) { return definition[1] && fn - ? assign($$scope.ctx.slice(), definition[1](fn(ctx))) - : $$scope.ctx; + ? assign($$scope?.ctx.slice(), definition[1](fn(ctx))) + : $$scope?.ctx; } export function get_slot_changes(definition, $$scope, dirty, fn) { diff --git a/src/runtime/slot/index.ts b/src/runtime/slot/index.ts new file mode 100644 index 000000000000..6f40fd661bd0 --- /dev/null +++ b/src/runtime/slot/index.ts @@ -0,0 +1,41 @@ +import { noop, insert, detach, FragmentMinimal, SvelteSlotOptions, SvelteComponentOptionsPrivate } from 'svelte/internal'; +import { SvelteComponent } from '..'; + +function create_root_slot_fn(elements: Node[]) { + return function (): FragmentMinimal { + return { + c: noop, + + m: function mount(target, anchor) { + elements.forEach(element => { + insert(target, element, anchor); + }); + }, + + d: function destroy(detaching) { + if (detaching) { + elements.forEach(detach); + } + }, + + l: noop + }; + }; +} + +export function createSlot(input: Record) { + const slots: Record>> = {}; + for (const key in input) { + const nodeOrNodeList = input[key]; + const nodeList = Array.isArray(nodeOrNodeList) ? nodeOrNodeList : [nodeOrNodeList]; + slots[key] = [create_root_slot_fn(nodeList)]; + } + return slots; +} + +export function slot(componentClass: typeof SvelteComponent, options: SvelteSlotOptions): Element[] { + const wrapper = document.createElement('div'); + new componentClass({...options, target: wrapper} as SvelteComponentOptionsPrivate) as any; + // @TODO this is a workaround until src/compiler/compile/render_dom/Block.ts is extended to expose created HTML element + return Array.from(wrapper.children); +} diff --git a/test/helpers.ts b/test/helpers.ts index 78f000c274ff..3f810cd6611b 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -61,6 +61,7 @@ global.navigator = window.navigator; global.getComputedStyle = window.getComputedStyle; global.requestAnimationFrame = null; // placeholder, filled in using set_raf global.window = window; +global.HTMLElement = window.HTMLElement; // add missing ecmascript globals to window for (const key of Object.getOwnPropertyNames(global)) { diff --git a/test/js/samples/root-component-slot/expected.js b/test/js/samples/root-component-slot/expected.js new file mode 100644 index 000000000000..e0cbe8da13ab --- /dev/null +++ b/test/js/samples/root-component-slot/expected.js @@ -0,0 +1,100 @@ +/* generated by Svelte vX.Y.Z */ +import { + SvelteComponent, + append, + create_slot, + detach, + element, + init, + insert, + safe_not_equal, + space, + transition_in, + transition_out, + update_slot +} from "svelte/internal"; + +const get_slot1_slot_changes = dirty => ({}); +const get_slot1_slot_context = ctx => ({}); + +function create_fragment(ctx) { + let div; + let t; + let current; + const default_slot_template = /*#slots*/ ctx[1].default; + const default_slot = create_slot(default_slot_template, ctx, /*$$scope*/ ctx[0], null); + const slot1_slot_template = /*#slots*/ ctx[1].slot1; + const slot1_slot = create_slot(slot1_slot_template, ctx, /*$$scope*/ ctx[0], get_slot1_slot_context); + + return { + c() { + div = element("div"); + if (default_slot) default_slot.c(); + t = space(); + if (slot1_slot) slot1_slot.c(); + }, + m(target, anchor) { + insert(target, div, anchor); + + if (default_slot) { + default_slot.m(div, null); + } + + append(div, t); + + if (slot1_slot) { + slot1_slot.m(div, null); + } + + current = true; + }, + p(ctx, [dirty]) { + if (default_slot) { + if (default_slot.p && dirty & /*$$scope*/ 1) { + update_slot(default_slot, default_slot_template, ctx, /*$$scope*/ ctx[0], dirty, null, null); + } + } + + if (slot1_slot) { + if (slot1_slot.p && dirty & /*$$scope*/ 1) { + update_slot(slot1_slot, slot1_slot_template, ctx, /*$$scope*/ ctx[0], dirty, get_slot1_slot_changes, get_slot1_slot_context); + } + } + }, + i(local) { + if (current) return; + transition_in(default_slot, local); + transition_in(slot1_slot, local); + current = true; + }, + o(local) { + transition_out(default_slot, local); + transition_out(slot1_slot, local); + current = false; + }, + d(detaching) { + if (detaching) detach(div); + if (default_slot) default_slot.d(detaching); + if (slot1_slot) slot1_slot.d(detaching); + } + }; +} + +function instance($$self, $$props, $$invalidate) { + let { $$slots: slots = {}, $$scope } = $$props; + + $$self.$$set = $$props => { + if ("$$scope" in $$props) $$invalidate(0, $$scope = $$props.$$scope); + }; + + return [$$scope, slots]; +} + +class Component extends SvelteComponent { + constructor(options) { + super(); + init(this, options, instance, create_fragment, safe_not_equal, {}); + } +} + +export default Component; \ No newline at end of file diff --git a/test/js/samples/root-component-slot/input.svelte b/test/js/samples/root-component-slot/input.svelte new file mode 100644 index 000000000000..0507e5c658b8 --- /dev/null +++ b/test/js/samples/root-component-slot/input.svelte @@ -0,0 +1,4 @@ +
+ + +
diff --git a/test/runtime/index.ts b/test/runtime/index.ts index 153ac9dbb813..25d822750180 100644 --- a/test/runtime/index.ts +++ b/test/runtime/index.ts @@ -24,8 +24,8 @@ let compile = null; const sveltePath = process.cwd().split('\\').join('/'); -let unhandled_rejection = false; -process.on('unhandledRejection', err => { +let unhandled_rejection: Error | null = null; +process.on('unhandledRejection', (err: Error) => { unhandled_rejection = err; }); @@ -159,12 +159,13 @@ describe('runtime', () => { warnings.push(warning); }; + const configOptions = typeof config.options === 'function' ? config.options(window) : config.options; const options = Object.assign({}, { target, hydrate, props: config.props, intro: config.intro - }, config.options || {}); + }, configOptions || {}); const component = new SvelteComponent(options); diff --git a/test/runtime/samples/root-component-slot/_config.js b/test/runtime/samples/root-component-slot/_config.js new file mode 100644 index 000000000000..07536b17cc25 --- /dev/null +++ b/test/runtime/samples/root-component-slot/_config.js @@ -0,0 +1,52 @@ +import { createSlot, slot } from 'svelte'; + +export default { + options(window) { + const default_el = window.document.createElement('div'); + default_el.innerHTML = 'default slot custom content'; + const my_slot_el1 = window.document.createElement('div'); + my_slot_el1.innerHTML = 'first my slot element'; + const my_slot_el2 = window.document.createElement('div'); + my_slot_el2.innerHTML = 'second my slot element'; + const my_slot_els = [my_slot_el1, my_slot_el2]; + const another_slot_el = window.document.createTextNode('another slot'); + const conditional_slot_el = window.document.createElement('div'); + conditional_slot_el.innerHTML = 'conditional slot'; + const Nested = require('./nested.svelte').default; + return { + slots: createSlot({ + default: default_el, + 'my-slot': my_slot_els, + 'another-slot-with-content': another_slot_el, + 'conditional-slot': conditional_slot_el, + 'slot-with-content-from-nested': slot(Nested) + }) + }; + }, + + test({ assert, component, target }) { + const expectedHtmlWhenSlotIsVisible = ` + default slot:
default slot custom content
+ named slot:
first my slot element
second my slot element
+ slot with default content: default content + another slot with content: another slot + slot with content from nested:
this div is in nested.svelte
this span is in nested.svelte + conditional slot:
conditional slot
+ conditional slot with content: default content`; + + assert.htmlEqual(target.innerHTML, expectedHtmlWhenSlotIsVisible); + + component.is_slot_visible = false; + assert.htmlEqual(target.innerHTML, ` + default slot:
default slot custom content
+ named slot:
first my slot element
second my slot element
+ slot with default content: default content + another slot with content: another slot + slot with content from nested:
this div is in nested.svelte
this span is in nested.svelte`); + + component.is_slot_visible = true; + assert.htmlEqual(target.innerHTML, expectedHtmlWhenSlotIsVisible); + // @TODO once src/compiler/compile/render_dom/Block.ts is extended to expose created HTML elements + // and nested component can be referenced directly, test mutating nested child props + } +}; diff --git a/test/runtime/samples/root-component-slot/main.svelte b/test/runtime/samples/root-component-slot/main.svelte new file mode 100644 index 000000000000..f2a4b7d6f3e7 --- /dev/null +++ b/test/runtime/samples/root-component-slot/main.svelte @@ -0,0 +1,12 @@ +default slot: +named slot: +slot with default content: default content +another slot with content: default content +slot with content from nested: default content +{#if is_slot_visible} + conditional slot: + conditional slot with content: default content +{/if} + diff --git a/test/runtime/samples/root-component-slot/nested.svelte b/test/runtime/samples/root-component-slot/nested.svelte new file mode 100644 index 000000000000..8fae32b10fa9 --- /dev/null +++ b/test/runtime/samples/root-component-slot/nested.svelte @@ -0,0 +1,2 @@ +
this div is in nested.svelte
+this span is in nested.svelte \ No newline at end of file