From cf7c7d15eda52e0c6da08e8dd7b10805e6679910 Mon Sep 17 00:00:00 2001 From: Vlad Glushchuk Date: Mon, 8 Apr 2019 20:28:16 +0200 Subject: [PATCH 01/10] Add bind:text and bind:html support for contenteditable elements Fixes #310 --- src/compile/nodes/Element.ts | 21 ++++++++- .../render-dom/wrappers/Element/Binding.ts | 16 +++++++ .../render-dom/wrappers/Element/index.ts | 6 +++ src/compile/render-ssr/handlers/Element.ts | 23 +++++++--- .../samples/contenteditable-html/_config.js | 43 +++++++++++++++++++ .../samples/contenteditable-html/main.svelte | 6 +++ .../samples/contenteditable-text/_config.js | 37 ++++++++++++++++ .../samples/contenteditable-text/main.svelte | 6 +++ .../contenteditable-dynamic/errors.json | 15 +++++++ .../contenteditable-dynamic/input.svelte | 6 +++ .../contenteditable-missing/errors.json | 15 +++++++ .../contenteditable-missing/input.svelte | 4 ++ 12 files changed, 192 insertions(+), 6 deletions(-) create mode 100644 test/runtime/samples/contenteditable-html/_config.js create mode 100644 test/runtime/samples/contenteditable-html/main.svelte create mode 100644 test/runtime/samples/contenteditable-text/_config.js create mode 100644 test/runtime/samples/contenteditable-text/main.svelte create mode 100644 test/validator/samples/contenteditable-dynamic/errors.json create mode 100644 test/validator/samples/contenteditable-dynamic/input.svelte create mode 100644 test/validator/samples/contenteditable-missing/errors.json create mode 100644 test/validator/samples/contenteditable-missing/input.svelte diff --git a/src/compile/nodes/Element.ts b/src/compile/nodes/Element.ts index bc1991c57e83..2838129e367e 100644 --- a/src/compile/nodes/Element.ts +++ b/src/compile/nodes/Element.ts @@ -570,7 +570,26 @@ export default class Element extends Node { message: `'${binding.name}' is not a valid binding on void elements like <${this.name}>. Use a wrapper element instead` }); } - } else if (name !== 'this') { + } else if ( + name === 'text' || + name === 'html' + ){ + const contenteditable = this.attributes.find( + (attribute: Attribute) => attribute.name === 'contenteditable' + ); + + if (!contenteditable) { + component.error(binding, { + code: `missing-contenteditable-attribute`, + message: `'contenteditable' attribute is required for text and html two-way bindings` + }); + } else if (contenteditable && !contenteditable.is_static) { + component.error(contenteditable, { + code: `dynamic-contenteditable-attribute`, + message: `'contenteditable' attribute cannot be dynamic if element uses two-way binding` + }); + } + } else if (name !== 'this') { component.error(binding, { code: `invalid-binding`, message: `'${binding.name}' is not a valid binding` diff --git a/src/compile/render-dom/wrappers/Element/Binding.ts b/src/compile/render-dom/wrappers/Element/Binding.ts index 828d664001ec..d0e5375aeff8 100644 --- a/src/compile/render-dom/wrappers/Element/Binding.ts +++ b/src/compile/render-dom/wrappers/Element/Binding.ts @@ -201,6 +201,14 @@ function get_dom_updater( return `${element.var}.checked = ${condition};` } + if (binding.node.name === 'text') { + return `${element.var}.textContent = ${binding.snippet};`; + } + + if (binding.node.name === 'html') { + return `${element.var}.innerHTML = ${binding.snippet};`; + } + return `${element.var}.${binding.node.name} = ${binding.snippet};`; } @@ -313,6 +321,14 @@ function get_value_from_dom( return `@time_ranges_to_array(this.${name})` } + if (name === 'text') { + return `this.textContent`; + } + + if (name === 'html') { + return `this.innerHTML`; + } + // everything else return `this.${name}`; } diff --git a/src/compile/render-dom/wrappers/Element/index.ts b/src/compile/render-dom/wrappers/Element/index.ts index 80a8308b9351..10a56eaace8c 100644 --- a/src/compile/render-dom/wrappers/Element/index.ts +++ b/src/compile/render-dom/wrappers/Element/index.ts @@ -29,6 +29,12 @@ const events = [ node.name === 'textarea' || node.name === 'input' && !/radio|checkbox|range/.test(node.get_static_attribute_value('type')) }, + { + event_names: ['input'], + filter: (node: Element, name: string) => + (name === 'text' || name === 'html') && + node.attributes.some(attribute => attribute.name === 'contenteditable') + }, { event_names: ['change'], filter: (node: Element, name: string) => diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts index 0c2cdc489ea7..f38bc0e15102 100644 --- a/src/compile/render-ssr/handlers/Element.ts +++ b/src/compile/render-ssr/handlers/Element.ts @@ -48,7 +48,12 @@ const boolean_attributes = new Set([ export default function(node, renderer, options) { let opening_tag = `<${node.name}`; - let textarea_contents; // awkward special case + let node_contents; // awkward special case + const contenteditable = ( + node.name !== 'textarea' && + node.name !== 'input' && + node.attributes.some((attribute: Node) => attribute.name === 'contenteditable') + ); const slot = node.get_static_attribute_value('slot'); if (slot && node.has_ancestor('InlineComponent')) { @@ -77,7 +82,7 @@ export default function(node, renderer, options) { args.push(snip(attribute.expression)); } else { if (attribute.name === 'value' && node.name === 'textarea') { - textarea_contents = stringify_attribute(attribute, true); + node_contents = stringify_attribute(attribute, true); } else if (attribute.is_true) { args.push(`{ ${quote_name_if_necessary(attribute.name)}: true }`); } else if ( @@ -99,7 +104,7 @@ export default function(node, renderer, options) { if (attribute.type !== 'Attribute') return; if (attribute.name === 'value' && node.name === 'textarea') { - textarea_contents = stringify_attribute(attribute, true); + node_contents = stringify_attribute(attribute, true); } else if (attribute.is_true) { opening_tag += ` ${attribute.name}`; } else if ( @@ -128,6 +133,14 @@ export default function(node, renderer, options) { if (name === 'group') { // TODO server-render group bindings + } else if (contenteditable && (node === 'text' || node === 'html')) { + const snippet = snip(expression) + if (name == 'text') { + node_contents = '${@escape(' + snippet + ')}' + } else { + // Do not escape HTML content + node_contents = '${' + snippet + '}' + } } else { const snippet = snip(expression); opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}'; @@ -142,8 +155,8 @@ export default function(node, renderer, options) { renderer.append(opening_tag); - if (node.name === 'textarea' && textarea_contents !== undefined) { - renderer.append(textarea_contents); + if ((node.name === 'textarea' || contenteditable) && node_contents !== undefined) { + renderer.append(node_contents); } else { renderer.render(node.children, options); } diff --git a/test/runtime/samples/contenteditable-html/_config.js b/test/runtime/samples/contenteditable-html/_config.js new file mode 100644 index 000000000000..cd2a82265547 --- /dev/null +++ b/test/runtime/samples/contenteditable-html/_config.js @@ -0,0 +1,43 @@ +export default { + props: { + name: 'world', + }, + + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello world

+ `, + + async test({ assert, component, target, window }) { + const el = target.querySelector('editor'); + assert.equal(el.innerHTML, 'world'); + + el.innerHTML = 'everybody'; + + // No updates to data yet + assert.htmlEqual(target.innerHTML, ` + everybody +

hello world

+ `); + + // Handle user input + const event = new window.Event('input'); + await el.dispatchEvent(event); + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.innerHTML, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/contenteditable-html/main.svelte b/test/runtime/samples/contenteditable-html/main.svelte new file mode 100644 index 000000000000..53b4e81c8876 --- /dev/null +++ b/test/runtime/samples/contenteditable-html/main.svelte @@ -0,0 +1,6 @@ + + + +

hello {@html name}

\ No newline at end of file diff --git a/test/runtime/samples/contenteditable-text/_config.js b/test/runtime/samples/contenteditable-text/_config.js new file mode 100644 index 000000000000..4935a3a9a751 --- /dev/null +++ b/test/runtime/samples/contenteditable-text/_config.js @@ -0,0 +1,37 @@ +export default { + props: { + name: 'world', + }, + + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello world

+ `, + + async test({ assert, component, target, window }) { + const el = target.querySelector('editor'); + assert.equal(el.textContent, 'world'); + + const event = new window.Event('input'); + + el.textContent = 'everybody'; + await el.dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.textContent, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/contenteditable-text/main.svelte b/test/runtime/samples/contenteditable-text/main.svelte new file mode 100644 index 000000000000..a71d9f0c5b2e --- /dev/null +++ b/test/runtime/samples/contenteditable-text/main.svelte @@ -0,0 +1,6 @@ + + + +

hello {name}

\ No newline at end of file diff --git a/test/validator/samples/contenteditable-dynamic/errors.json b/test/validator/samples/contenteditable-dynamic/errors.json new file mode 100644 index 000000000000..0c4c5585a65e --- /dev/null +++ b/test/validator/samples/contenteditable-dynamic/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "dynamic-contenteditable-attribute", + "message": "'contenteditable' attribute cannot be dynamic if element uses two-way binding", + "start": { + "line": 6, + "column": 8, + "character": 73 + }, + "end": { + "line": 6, + "column": 32, + "character": 97 + }, + "pos": 73 +}] \ No newline at end of file diff --git a/test/validator/samples/contenteditable-dynamic/input.svelte b/test/validator/samples/contenteditable-dynamic/input.svelte new file mode 100644 index 000000000000..97d2c9228c89 --- /dev/null +++ b/test/validator/samples/contenteditable-dynamic/input.svelte @@ -0,0 +1,6 @@ + + diff --git a/test/validator/samples/contenteditable-missing/errors.json b/test/validator/samples/contenteditable-missing/errors.json new file mode 100644 index 000000000000..9cadb20629a1 --- /dev/null +++ b/test/validator/samples/contenteditable-missing/errors.json @@ -0,0 +1,15 @@ +[{ + "code": "missing-contenteditable-attribute", + "message": "'contenteditable' attribute is required for text and html two-way bindings", + "start": { + "line": 4, + "column": 8, + "character": 48 + }, + "end": { + "line": 4, + "column": 24, + "character": 64 + }, + "pos": 48 +}] \ No newline at end of file diff --git a/test/validator/samples/contenteditable-missing/input.svelte b/test/validator/samples/contenteditable-missing/input.svelte new file mode 100644 index 000000000000..47f125894a1b --- /dev/null +++ b/test/validator/samples/contenteditable-missing/input.svelte @@ -0,0 +1,4 @@ + + From 8deee95f141f32cd027467eb7c1d2bb8ff597007 Mon Sep 17 00:00:00 2001 From: Vlad Glushchuk Date: Mon, 8 Apr 2019 20:56:52 +0200 Subject: [PATCH 02/10] Fix a typo --- src/compile/render-ssr/handlers/Element.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/compile/render-ssr/handlers/Element.ts b/src/compile/render-ssr/handlers/Element.ts index f38bc0e15102..cefe44ede815 100644 --- a/src/compile/render-ssr/handlers/Element.ts +++ b/src/compile/render-ssr/handlers/Element.ts @@ -133,7 +133,7 @@ export default function(node, renderer, options) { if (name === 'group') { // TODO server-render group bindings - } else if (contenteditable && (node === 'text' || node === 'html')) { + } else if (contenteditable && (name === 'text' || name === 'html')) { const snippet = snip(expression) if (name == 'text') { node_contents = '${@escape(' + snippet + ')}' From e3de705cb821de586e5f78fabc4b09692e846276 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sun, 9 Jun 2019 23:31:20 -0400 Subject: [PATCH 03/10] initialise text/html bindings if necessary --- src/compiler/compile/nodes/Element.ts | 4 +- .../render-dom/wrappers/Element/Binding.ts | 28 +++++-------- .../render-dom/wrappers/Element/index.ts | 20 ++++++---- .../compile/render-ssr/handlers/Element.ts | 24 +++++++---- .../_config.js | 40 +++++++++++++++++++ .../main.svelte | 8 ++++ .../_config.js | 6 +-- .../main.svelte | 2 +- .../_config.js | 34 ++++++++++++++++ .../main.svelte | 8 ++++ .../_config.js | 0 .../main.svelte | 2 +- 12 files changed, 138 insertions(+), 38 deletions(-) create mode 100644 test/runtime/samples/binding-contenteditable-html-initial/_config.js create mode 100644 test/runtime/samples/binding-contenteditable-html-initial/main.svelte rename test/runtime/samples/{contenteditable-html => binding-contenteditable-html}/_config.js (89%) rename test/runtime/samples/{contenteditable-html => binding-contenteditable-html}/main.svelte (83%) create mode 100644 test/runtime/samples/binding-contenteditable-text-initial/_config.js create mode 100644 test/runtime/samples/binding-contenteditable-text-initial/main.svelte rename test/runtime/samples/{contenteditable-text => binding-contenteditable-text}/_config.js (100%) rename test/runtime/samples/{contenteditable-text => binding-contenteditable-text}/main.svelte (82%) diff --git a/src/compiler/compile/nodes/Element.ts b/src/compiler/compile/nodes/Element.ts index c37ea5020a1a..3e883200a77b 100644 --- a/src/compiler/compile/nodes/Element.ts +++ b/src/compiler/compile/nodes/Element.ts @@ -610,7 +610,7 @@ export default class Element extends Node { } else if ( name === 'text' || name === 'html' - ){ + ) { const contenteditable = this.attributes.find( (attribute: Attribute) => attribute.name === 'contenteditable' ); @@ -626,7 +626,7 @@ export default class Element extends Node { message: `'contenteditable' attribute cannot be dynamic if element uses two-way binding` }); } - } else if (name !== 'this') { + } else if (name !== 'this') { component.error(binding, { code: `invalid-binding`, message: `'${binding.name}' is not a valid binding` diff --git a/src/compiler/compile/render-dom/wrappers/Element/Binding.ts b/src/compiler/compile/render-dom/wrappers/Element/Binding.ts index 22f1f682e436..7de661e3d186 100644 --- a/src/compiler/compile/render-dom/wrappers/Element/Binding.ts +++ b/src/compiler/compile/render-dom/wrappers/Element/Binding.ts @@ -133,6 +133,14 @@ export default class BindingWrapper { break; } + case 'text': + update_conditions.push(`${this.snippet} !== ${parent.var}.textContent`); + break; + + case 'html': + update_conditions.push(`${this.snippet} !== ${parent.var}.innerHTML`); + break; + case 'currentTime': case 'playbackRate': case 'volume': @@ -162,7 +170,9 @@ export default class BindingWrapper { ); } - if (!/(currentTime|paused)/.test(this.node.name)) { + if (this.node.name === 'html' || this.node.name === 'text') { + block.builders.mount.add_block(`if (${this.snippet} !== void 0) ${update_dom}`); + } else if (!/(currentTime|paused)/.test(this.node.name)) { block.builders.mount.add_block(update_dom); } } @@ -198,14 +208,6 @@ function get_dom_updater( return `${element.var}.checked = ${condition};`; } - if (binding.node.name === 'text') { - return `if (${binding.snippet} !== ${element.var}.textContent) ${element.var}.textContent = ${binding.snippet};`; - } - - if (binding.node.name === 'html') { - return `if (${binding.snippet} !== ${element.var}.innerHTML) ${element.var}.innerHTML = ${binding.snippet};`; - } - if (binding.node.name === 'text') { return `${element.var}.textContent = ${binding.snippet};`; } @@ -334,14 +336,6 @@ function get_value_from_dom( return `this.innerHTML`; } - if (name === 'text') { - return `this.textContent`; - } - - if (name === 'html') { - return `this.innerHTML`; - } - // everything else return `this.${name}`; } diff --git a/src/compiler/compile/render-dom/wrappers/Element/index.ts b/src/compiler/compile/render-dom/wrappers/Element/index.ts index c63ebf16933b..527a101f6afe 100644 --- a/src/compiler/compile/render-dom/wrappers/Element/index.ts +++ b/src/compiler/compile/render-dom/wrappers/Element/index.ts @@ -33,12 +33,6 @@ const events = [ (name === 'text' || name === 'html') && node.attributes.some(attribute => attribute.name === 'contenteditable') }, - { - event_names: ['change'], - filter: (node: Element, name: string) => - (name === 'text' || name === 'html') && - node.attributes.some(attribute => attribute.name === 'contenteditable') - }, { event_names: ['change'], filter: (node: Element, _name: string) => @@ -514,7 +508,19 @@ export default class ElementWrapper extends Wrapper { .map(binding => `${binding.snippet} === void 0`) .join(' || '); - if (this.node.name === 'select' || group.bindings.find(binding => binding.node.name === 'indeterminate' || binding.is_readonly_media_attribute())) { + const should_initialise = ( + this.node.name === 'select' || + group.bindings.find(binding => { + return ( + binding.node.name === 'indeterminate' || + binding.node.name === 'text' || + binding.node.name === 'html' || + binding.is_readonly_media_attribute() + ); + }) + ); + + if (should_initialise) { const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`; block.builders.hydrate.add_line( `if (${some_initial_state_is_undefined}) @add_render_callback(${callback});` diff --git a/src/compiler/compile/render-ssr/handlers/Element.ts b/src/compiler/compile/render-ssr/handlers/Element.ts index 681e0d4c7b48..fb9b935f9a3e 100644 --- a/src/compiler/compile/render-ssr/handlers/Element.ts +++ b/src/compiler/compile/render-ssr/handlers/Element.ts @@ -53,7 +53,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption slot_scopes: Map; }) { let opening_tag = `<${node.name}`; - let node_contents; // awkward special case + + // awkward special case + let node_contents; + let value; + const contenteditable = ( node.name !== 'textarea' && node.name !== 'input' && @@ -151,16 +155,16 @@ export default function(node: Element, renderer: Renderer, options: RenderOption if (name === 'group') { // TODO server-render group bindings } else if (contenteditable && (name === 'text' || name === 'html')) { - const snippet = snip(expression) + node_contents = snip(expression); if (name == 'text') { - node_contents = '${@escape(' + snippet + ')}' + value = '@escape($$value)'; } else { // Do not escape HTML content - node_contents = '${' + snippet + '}' + value = '$$value'; } } else if (binding.name === 'value' && node.name === 'textarea') { const snippet = snip(expression); - node_contents='${(' + snippet + ') || ""}'; + node_contents = '${(' + snippet + ') || ""}'; } else { const snippet = snip(expression); opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}'; @@ -175,8 +179,14 @@ export default function(node: Element, renderer: Renderer, options: RenderOption renderer.append(opening_tag); - if ((node.name === 'textarea' || contenteditable) && node_contents !== undefined) { - renderer.append(node_contents); + if (node_contents !== undefined) { + if (contenteditable) { + renderer.append('${($$value => $$value === void 0 ? `'); + renderer.render(node.children, options); + renderer.append('` : ' + value + ')(' + node_contents + ')}'); + } else { + renderer.append(node_contents); + } } else { renderer.render(node.children, options); } diff --git a/test/runtime/samples/binding-contenteditable-html-initial/_config.js b/test/runtime/samples/binding-contenteditable-html-initial/_config.js new file mode 100644 index 000000000000..0b1f656a54ef --- /dev/null +++ b/test/runtime/samples/binding-contenteditable-html-initial/_config.js @@ -0,0 +1,40 @@ +export default { + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello undefined

+ `, + + async test({ assert, component, target, window }) { + assert.equal(component.name, 'world'); + + const el = target.querySelector('editor'); + + el.innerHTML = 'everybody'; + + // No updates to data yet + assert.htmlEqual(target.innerHTML, ` + everybody +

hello world

+ `); + + // Handle user input + const event = new window.Event('input'); + await el.dispatchEvent(event); + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.innerHTML, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/binding-contenteditable-html-initial/main.svelte b/test/runtime/samples/binding-contenteditable-html-initial/main.svelte new file mode 100644 index 000000000000..1c05a0950d47 --- /dev/null +++ b/test/runtime/samples/binding-contenteditable-html-initial/main.svelte @@ -0,0 +1,8 @@ + + + + world + +

hello {@html name}

\ No newline at end of file diff --git a/test/runtime/samples/contenteditable-html/_config.js b/test/runtime/samples/binding-contenteditable-html/_config.js similarity index 89% rename from test/runtime/samples/contenteditable-html/_config.js rename to test/runtime/samples/binding-contenteditable-html/_config.js index cd2a82265547..013fa30f3912 100644 --- a/test/runtime/samples/contenteditable-html/_config.js +++ b/test/runtime/samples/binding-contenteditable-html/_config.js @@ -19,14 +19,14 @@ export default { el.innerHTML = 'everybody'; - // No updates to data yet + // No updates to data yet assert.htmlEqual(target.innerHTML, ` everybody

hello world

`); - // Handle user input - const event = new window.Event('input'); + // Handle user input + const event = new window.Event('input'); await el.dispatchEvent(event); assert.htmlEqual(target.innerHTML, ` everybody diff --git a/test/runtime/samples/contenteditable-html/main.svelte b/test/runtime/samples/binding-contenteditable-html/main.svelte similarity index 83% rename from test/runtime/samples/contenteditable-html/main.svelte rename to test/runtime/samples/binding-contenteditable-html/main.svelte index 53b4e81c8876..09195ed55873 100644 --- a/test/runtime/samples/contenteditable-html/main.svelte +++ b/test/runtime/samples/binding-contenteditable-html/main.svelte @@ -1,5 +1,5 @@ diff --git a/test/runtime/samples/binding-contenteditable-text-initial/_config.js b/test/runtime/samples/binding-contenteditable-text-initial/_config.js new file mode 100644 index 000000000000..7345687d29d9 --- /dev/null +++ b/test/runtime/samples/binding-contenteditable-text-initial/_config.js @@ -0,0 +1,34 @@ +export default { + html: ` + world +

hello world

+ `, + + ssrHtml: ` + world +

hello undefined

+ `, + + async test({ assert, component, target, window }) { + assert.equal(component.name, 'world'); + + const el = target.querySelector('editor'); + + const event = new window.Event('input'); + + el.textContent = 'everybody'; + await el.dispatchEvent(event); + + assert.htmlEqual(target.innerHTML, ` + everybody +

hello everybody

+ `); + + component.name = 'goodbye'; + assert.equal(el.textContent, 'goodbye'); + assert.htmlEqual(target.innerHTML, ` + goodbye +

hello goodbye

+ `); + }, +}; diff --git a/test/runtime/samples/binding-contenteditable-text-initial/main.svelte b/test/runtime/samples/binding-contenteditable-text-initial/main.svelte new file mode 100644 index 000000000000..633d268f435c --- /dev/null +++ b/test/runtime/samples/binding-contenteditable-text-initial/main.svelte @@ -0,0 +1,8 @@ + + + + world + +

hello {name}

\ No newline at end of file diff --git a/test/runtime/samples/contenteditable-text/_config.js b/test/runtime/samples/binding-contenteditable-text/_config.js similarity index 100% rename from test/runtime/samples/contenteditable-text/_config.js rename to test/runtime/samples/binding-contenteditable-text/_config.js diff --git a/test/runtime/samples/contenteditable-text/main.svelte b/test/runtime/samples/binding-contenteditable-text/main.svelte similarity index 82% rename from test/runtime/samples/contenteditable-text/main.svelte rename to test/runtime/samples/binding-contenteditable-text/main.svelte index a71d9f0c5b2e..c47f5ee47735 100644 --- a/test/runtime/samples/contenteditable-text/main.svelte +++ b/test/runtime/samples/binding-contenteditable-text/main.svelte @@ -1,5 +1,5 @@ From 4f626c45a9f520414d395543b2104b645f3688d0 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sun, 9 Jun 2019 23:35:09 -0400 Subject: [PATCH 04/10] tidy up, prevent collisions --- src/compiler/compile/render-ssr/handlers/Element.ts | 11 +++-------- src/runtime/internal/ssr.ts | 2 +- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/src/compiler/compile/render-ssr/handlers/Element.ts b/src/compiler/compile/render-ssr/handlers/Element.ts index fb9b935f9a3e..4029a7f5793a 100644 --- a/src/compiler/compile/render-ssr/handlers/Element.ts +++ b/src/compiler/compile/render-ssr/handlers/Element.ts @@ -156,23 +156,18 @@ export default function(node: Element, renderer: Renderer, options: RenderOption // TODO server-render group bindings } else if (contenteditable && (name === 'text' || name === 'html')) { node_contents = snip(expression); - if (name == 'text') { - value = '@escape($$value)'; - } else { - // Do not escape HTML content - value = '$$value'; - } + value = name === 'text' ? '@escape($$value)' : '$$value'; } else if (binding.name === 'value' && node.name === 'textarea') { const snippet = snip(expression); node_contents = '${(' + snippet + ') || ""}'; } else { const snippet = snip(expression); - opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}'; + opening_tag += ' ${($$value => $$value ? ("' + name + '" + ($$value === true ? "" : "=" + JSON.stringify($$value))) : "")(' + snippet + ')}'; } }); if (add_class_attribute) { - opening_tag += `\${((v) => v ? ' class="' + v + '"' : '')([${class_expression}].join(' ').trim())}`; + opening_tag += `\${(($$value) => $$value ? ' class="' + $$value + '"' : '')([${class_expression}].join(' ').trim())}`; } opening_tag += '>'; diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index c80b4d9f45a9..f88f70477985 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -118,4 +118,4 @@ export function get_store_value(store: Readable): T | undefined { let value; store.subscribe(_ => value = _)(); return value; -} +} \ No newline at end of file From b589289b557e441f337647d00fcd239367804ed7 Mon Sep 17 00:00:00 2001 From: Richard Harris Date: Sun, 9 Jun 2019 23:50:04 -0400 Subject: [PATCH 05/10] use helpers --- src/compiler/compile/render-ssr/handlers/Element.ts | 4 ++-- src/runtime/internal/ssr.ts | 9 +++++++++ 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/compiler/compile/render-ssr/handlers/Element.ts b/src/compiler/compile/render-ssr/handlers/Element.ts index 4029a7f5793a..0c2181a6054a 100644 --- a/src/compiler/compile/render-ssr/handlers/Element.ts +++ b/src/compiler/compile/render-ssr/handlers/Element.ts @@ -162,12 +162,12 @@ export default function(node: Element, renderer: Renderer, options: RenderOption node_contents = '${(' + snippet + ') || ""}'; } else { const snippet = snip(expression); - opening_tag += ' ${($$value => $$value ? ("' + name + '" + ($$value === true ? "" : "=" + JSON.stringify($$value))) : "")(' + snippet + ')}'; + opening_tag += '${@add_attribute("' + name + '", ' + snippet + ')}'; } }); if (add_class_attribute) { - opening_tag += `\${(($$value) => $$value ? ' class="' + $$value + '"' : '')([${class_expression}].join(' ').trim())}`; + opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`; } opening_tag += '>'; diff --git a/src/runtime/internal/ssr.ts b/src/runtime/internal/ssr.ts index f88f70477985..91d6e452fe27 100644 --- a/src/runtime/internal/ssr.ts +++ b/src/runtime/internal/ssr.ts @@ -118,4 +118,13 @@ export function get_store_value(store: Readable): T | undefined { let value; store.subscribe(_ => value = _)(); return value; +} + +export function add_attribute(name, value) { + if (!value) return ''; + return ` ${name}${value === true ? '' : `=${JSON.stringify(value)}`}`; +} + +export function add_classes(classes) { + return classes ? ` class="${classes}"` : ``; } \ No newline at end of file From dddc69ec7f0e5b6bcac991275fdeda4ed873b210 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jun 2019 11:40:41 -0400 Subject: [PATCH 06/10] update tests --- .../binding-contenteditable-html-initial/_config.js | 8 ++++---- .../samples/binding-contenteditable-html/_config.js | 5 ----- .../binding-contenteditable-text-initial/_config.js | 6 +++--- .../samples/binding-contenteditable-text/_config.js | 5 ----- 4 files changed, 7 insertions(+), 17 deletions(-) diff --git a/test/runtime/samples/binding-contenteditable-html-initial/_config.js b/test/runtime/samples/binding-contenteditable-html-initial/_config.js index 0b1f656a54ef..9eac2c9b17ac 100644 --- a/test/runtime/samples/binding-contenteditable-html-initial/_config.js +++ b/test/runtime/samples/binding-contenteditable-html-initial/_config.js @@ -1,6 +1,6 @@ export default { html: ` - world + world

hello world

`, @@ -18,7 +18,7 @@ export default { // No updates to data yet assert.htmlEqual(target.innerHTML, ` - everybody + everybody

hello world

`); @@ -26,14 +26,14 @@ export default { const event = new window.Event('input'); await el.dispatchEvent(event); assert.htmlEqual(target.innerHTML, ` - everybody + everybody

hello everybody

`); component.name = 'goodbye'; assert.equal(el.innerHTML, 'goodbye'); assert.htmlEqual(target.innerHTML, ` - goodbye + goodbye

hello goodbye

`); }, diff --git a/test/runtime/samples/binding-contenteditable-html/_config.js b/test/runtime/samples/binding-contenteditable-html/_config.js index 285512b6c98c..ceb6a75c7098 100644 --- a/test/runtime/samples/binding-contenteditable-html/_config.js +++ b/test/runtime/samples/binding-contenteditable-html/_config.js @@ -8,11 +8,6 @@ export default {

hello world

`, - ssrHtml: ` - world -

hello world

- `, - async test({ assert, component, target, window }) { const el = target.querySelector('editor'); assert.equal(el.innerHTML, 'world'); diff --git a/test/runtime/samples/binding-contenteditable-text-initial/_config.js b/test/runtime/samples/binding-contenteditable-text-initial/_config.js index 7345687d29d9..4899f30f1243 100644 --- a/test/runtime/samples/binding-contenteditable-text-initial/_config.js +++ b/test/runtime/samples/binding-contenteditable-text-initial/_config.js @@ -1,6 +1,6 @@ export default { html: ` - world + world

hello world

`, @@ -20,14 +20,14 @@ export default { await el.dispatchEvent(event); assert.htmlEqual(target.innerHTML, ` - everybody + everybody

hello everybody

`); component.name = 'goodbye'; assert.equal(el.textContent, 'goodbye'); assert.htmlEqual(target.innerHTML, ` - goodbye + goodbye

hello goodbye

`); }, diff --git a/test/runtime/samples/binding-contenteditable-text/_config.js b/test/runtime/samples/binding-contenteditable-text/_config.js index 059cda7cfeac..9f8645724d6d 100644 --- a/test/runtime/samples/binding-contenteditable-text/_config.js +++ b/test/runtime/samples/binding-contenteditable-text/_config.js @@ -8,11 +8,6 @@ export default {

hello world

`, - ssrHtml: ` - world -

hello world

- `, - async test({ assert, component, target, window }) { const el = target.querySelector('editor'); assert.equal(el.textContent, 'world'); From 86c5086c54481f7572765b73c4a3fe93c79c08d5 Mon Sep 17 00:00:00 2001 From: Rich Harris Date: Mon, 24 Jun 2019 13:00:16 -0400 Subject: [PATCH 07/10] add some docs, rename to textContent and innerHTML --- site/content/docs/02-template-syntax.md | 8 +++++++ src/compiler/compile/nodes/Element.ts | 6 ++--- .../render-dom/wrappers/Element/Binding.ts | 22 +++---------------- .../render-dom/wrappers/Element/index.ts | 6 ++--- .../compile/render-ssr/handlers/Element.ts | 4 ++-- .../main.svelte | 2 +- .../binding-contenteditable-html/main.svelte | 2 +- .../main.svelte | 2 +- .../binding-contenteditable-text/main.svelte | 2 +- .../contenteditable-dynamic/input.svelte | 2 +- .../contenteditable-missing/errors.json | 2 +- .../contenteditable-missing/input.svelte | 2 +- 12 files changed, 26 insertions(+), 34 deletions(-) diff --git a/site/content/docs/02-template-syntax.md b/site/content/docs/02-template-syntax.md index 682012d88beb..ceaa19a39877 100644 --- a/site/content/docs/02-template-syntax.md +++ b/site/content/docs/02-template-syntax.md @@ -510,6 +510,14 @@ When the value of an `