diff --git a/src/compiler/compile/render_dom/index.ts b/src/compiler/compile/render_dom/index.ts index 7d0dce831536..799821c29c04 100644 --- a/src/compiler/compile/render_dom/index.ts +++ b/src/compiler/compile/render_dom/index.ts @@ -462,10 +462,14 @@ export default function dom( class ${name} extends @SvelteElement { constructor(options) { super(); - + if (options) { + this.$$setup(options); + } + } + $$setup(options) { ${css.code && b`this.shadowRoot.innerHTML = \`\`;`} - @init(this, { target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); + @init(this, { props: options ? options.props : null, target: this.shadowRoot }, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); ${dev_props_check} @@ -517,7 +521,7 @@ export default function dom( constructor(options) { super(${options.dev && `options`}); ${should_add_css && b`if (!@_document.getElementById("${component.stylesheet.id}-style")) ${add_css}();`} - @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment': 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); + @init(this, options, ${definition}, ${has_create_fragment ? 'create_fragment' : 'null'}, ${not_equal}, ${prop_indexes}, ${dirty}); ${options.dev && b`@dispatch_dev("SvelteRegisterComponent", { component: this, tagName: "${name.name}", options, id: create_fragment.name });`} ${dev_props_check} diff --git a/src/runtime/internal/Component.ts b/src/runtime/internal/Component.ts index 459a78031a04..489623ac0d8f 100644 --- a/src/runtime/internal/Component.ts +++ b/src/runtime/internal/Component.ts @@ -174,7 +174,23 @@ if (typeof HTMLElement === 'function') { this.attachShadow({ mode: 'open' }); } + // placeholder object to allow props to be set pre-$$setup + $$initialProps: Record | null = {}; + $$setup: (options) => void; + connectedCallback() { + if (!this.$$) { + // wasn't set up from constructor as options were not ready + const options = is_empty(this.$$initialProps) ? + null: + { + props: this.$$initialProps + }; + + this.$$setup(options); + // clean up, prevent reuse of $$initialProps + this.$$initialProps = null; + } // @ts-ignore todo: improve typings for (const key in this.$$.slotted) { // @ts-ignore todo: improve typings @@ -182,7 +198,11 @@ if (typeof HTMLElement === 'function') { } } + // initial implementation of method, will be overridden on setup attributeChangedCallback(attr, _oldValue, newValue) { + if (this.$$initialProps) { + this.$$initialProps[attr] = newValue; + } this[attr] = newValue; } @@ -203,6 +223,11 @@ if (typeof HTMLElement === 'function') { } $set($$props) { + if (this.$$initialProps && $$props) { + for (const attr of Object.getOwnPropertyNames($$props)) { + this.$$initialProps[attr] = $$props[attr]; + } + } if (this.$$set && !is_empty($$props)) { this.$$.skip_bound = true; this.$$set($$props); diff --git a/test/custom-elements/samples/props-after-create/main.svelte b/test/custom-elements/samples/props-after-create/main.svelte new file mode 100644 index 000000000000..22c91751d099 --- /dev/null +++ b/test/custom-elements/samples/props-after-create/main.svelte @@ -0,0 +1,8 @@ + + + + +

{items.length} items

+

{items.join(', ')}

diff --git a/test/custom-elements/samples/props-after-create/test.js b/test/custom-elements/samples/props-after-create/test.js new file mode 100644 index 000000000000..c4dc6b31d0a8 --- /dev/null +++ b/test/custom-elements/samples/props-after-create/test.js @@ -0,0 +1,28 @@ +import * as assert from 'assert'; +import CustomElement from './main.svelte'; + +export default function (target) { + + // initialize without options to simulate instantiation within HTML + const el = new CustomElement(); + + assert.equal(el.outerHTML, ''); + + el.items = ['a', 'b', 'c']; + const p0 = el.shadowRoot.querySelector('p'); + + // shouldn't be instantitated yet + assert.equal(p0, undefined); + + // add to to DOM to trigger setup + target.appendChild(el); + + const [p1, p2] = el.shadowRoot.querySelectorAll('p'); + assert.equal(p1.textContent, '3 items'); + assert.equal(p2.textContent, 'a, b, c'); + + el.items = ['d', 'e', 'f', 'g', 'h']; + + assert.equal(p1.textContent, '5 items'); + assert.equal(p2.textContent, 'd, e, f, g, h'); +} diff --git a/test/js/samples/css-shadow-dom-keyframes/expected.js b/test/js/samples/css-shadow-dom-keyframes/expected.js index a0a0ebe0211b..91ef177ec63f 100644 --- a/test/js/samples/css-shadow-dom-keyframes/expected.js +++ b/test/js/samples/css-shadow-dom-keyframes/expected.js @@ -33,8 +33,26 @@ function create_fragment(ctx) { class Component extends SvelteElement { constructor(options) { super(); + + if (options) { + this.$$setup(options); + } + } + + $$setup(options) { this.shadowRoot.innerHTML = ``; - init(this, { target: this.shadowRoot }, null, create_fragment, safe_not_equal, {}); + + init( + this, + { + props: options ? options.props : null, + target: this.shadowRoot + }, + null, + create_fragment, + safe_not_equal, + {} + ); if (options) { if (options.target) { @@ -45,4 +63,4 @@ class Component extends SvelteElement { } customElements.define("custom-element", Component); -export default Component; \ No newline at end of file +export default Component;