Merge pull request #2996 from sveltejs/init-contenteditable

Initialise html/text bindings from DOM
pull/7738/head
Rich Harris 6 years ago committed by GitHub
commit a4d574fb30

@ -510,6 +510,14 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select> </select>
``` ```
---
Elements with the `contenteditable` attribute support `innerHTML` and `textContent` bindings.
```html
<div contenteditable="true" bind:innerHTML={html}></div>
```
##### Media element bindings ##### Media element bindings
--- ---

@ -0,0 +1,15 @@
<script>
let html = '<p>Write some text!</p>';
</script>
<div contenteditable="true"></div>
<pre>{html}</pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

@ -0,0 +1,18 @@
<script>
let html = '<p>Write some text!</p>';
</script>
<div
contenteditable="true"
bind:innerHTML={html}
></div>
<pre>{html}</pre>
<style>
[contenteditable] {
padding: 0.5em;
border: 1px solid #eee;
border-radius: 4px;
}
</style>

@ -0,0 +1,12 @@
---
title: Contenteditable bindings
---
Elements with a `contenteditable="true"` attribute support `textContent` and `innerHTML` bindings:
```html
<div
contenteditable="true"
bind:innerHTML={html}
></div>
```

@ -608,8 +608,8 @@ export default class Element extends Node {
}); });
} }
} else if ( } else if (
name === 'text' || name === 'textContent' ||
name === 'html' name === 'innerHTML'
) { ) {
const contenteditable = this.attributes.find( const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable' (attribute: Attribute) => attribute.name === 'contenteditable'
@ -618,7 +618,7 @@ export default class Element extends Node {
if (!contenteditable) { if (!contenteditable) {
component.error(binding, { component.error(binding, {
code: `missing-contenteditable-attribute`, code: `missing-contenteditable-attribute`,
message: `'contenteditable' attribute is required for text and html two-way bindings` message: `'contenteditable' attribute is required for textContent and innerHTML two-way bindings`
}); });
} else if (contenteditable && !contenteditable.is_static) { } else if (contenteditable && !contenteditable.is_static) {
component.error(contenteditable, { component.error(contenteditable, {

@ -133,6 +133,14 @@ export default class BindingWrapper {
break; break;
} }
case 'textContent':
update_conditions.push(`${this.snippet} !== ${parent.var}.textContent`);
break;
case 'innerHTML':
update_conditions.push(`${this.snippet} !== ${parent.var}.innerHTML`);
break;
case 'currentTime': case 'currentTime':
case 'playbackRate': case 'playbackRate':
case 'volume': case 'volume':
@ -162,7 +170,9 @@ export default class BindingWrapper {
); );
} }
if (!/(currentTime|paused)/.test(this.node.name)) { if (this.node.name === 'innerHTML' || this.node.name === 'textContent') {
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); block.builders.mount.add_block(update_dom);
} }
} }
@ -198,14 +208,6 @@ function get_dom_updater(
return `${element.var}.checked = ${condition};`; 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};`;
}
return `${element.var}.${binding.node.name} = ${binding.snippet};`; return `${element.var}.${binding.node.name} = ${binding.snippet};`;
} }
@ -318,14 +320,6 @@ function get_value_from_dom(
return `@time_ranges_to_array(this.${name})`; return `@time_ranges_to_array(this.${name})`;
} }
if (name === 'text') {
return `this.textContent`;
}
if (name === 'html') {
return `this.innerHTML`;
}
// everything else // everything else
return `this.${name}`; return `this.${name}`;
} }

@ -30,7 +30,7 @@ const events = [
{ {
event_names: ['input'], event_names: ['input'],
filter: (node: Element, name: string) => filter: (node: Element, name: string) =>
(name === 'text' || name === 'html') && (name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable') node.attributes.some(attribute => attribute.name === 'contenteditable')
}, },
{ {
@ -510,7 +510,19 @@ export default class ElementWrapper extends Wrapper {
.map(binding => `${binding.snippet} === void 0`) .map(binding => `${binding.snippet} === void 0`)
.join(' || '); .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 === 'textContent' ||
binding.node.name === 'innerHTML' ||
binding.is_readonly_media_attribute()
);
})
);
if (should_initialise) {
const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`; const callback = has_local_function ? handler : `() => ${callee}.call(${this.var})`;
block.builders.hydrate.add_line( block.builders.hydrate.add_line(
`if (${some_initial_state_is_undefined}) @add_render_callback(${callback});` `if (${some_initial_state_is_undefined}) @add_render_callback(${callback});`

@ -53,7 +53,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
slot_scopes: Map<any, any>; slot_scopes: Map<any, any>;
}) { }) {
let opening_tag = `<${node.name}`; let opening_tag = `<${node.name}`;
let node_contents; // awkward special case
// awkward special case
let node_contents;
let value;
const contenteditable = ( const contenteditable = (
node.name !== 'textarea' && node.name !== 'textarea' &&
node.name !== 'input' && node.name !== 'input' &&
@ -150,33 +154,34 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
if (name === 'group') { if (name === 'group') {
// TODO server-render group bindings // TODO server-render group bindings
} else if (contenteditable && (name === 'text' || name === 'html')) { } else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) {
const snippet = snip(expression); node_contents = snip(expression);
if (name == 'text') { value = name === 'textContent' ? '@escape($$value)' : '$$value';
node_contents = '${@escape(' + snippet + ')}';
} else {
// Do not escape HTML content
node_contents = '${' + snippet + '}';
}
} else if (binding.name === 'value' && node.name === 'textarea') { } else if (binding.name === 'value' && node.name === 'textarea') {
const snippet = snip(expression); const snippet = snip(expression);
node_contents='${(' + snippet + ') || ""}'; node_contents = '${(' + snippet + ') || ""}';
} else { } else {
const snippet = snip(expression); const snippet = snip(expression);
opening_tag += ' ${(v => v ? ("' + name + '" + (v === true ? "" : "=" + JSON.stringify(v))) : "")(' + snippet + ')}'; opening_tag += '${@add_attribute("' + name + '", ' + snippet + ')}';
} }
}); });
if (add_class_attribute) { if (add_class_attribute) {
opening_tag += `\${((v) => v ? ' class="' + v + '"' : '')([${class_expression}].join(' ').trim())}`; opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
} }
opening_tag += '>'; opening_tag += '>';
renderer.append(opening_tag); renderer.append(opening_tag);
if ((node.name === 'textarea' || contenteditable) && node_contents !== undefined) { if (node_contents !== undefined) {
renderer.append(node_contents); if (contenteditable) {
renderer.append('${($$value => $$value === void 0 ? `');
renderer.render(node.children, options);
renderer.append('` : ' + value + ')(' + node_contents + ')}');
} else {
renderer.append(node_contents);
}
} else { } else {
renderer.render(node.children, options); renderer.render(node.children, options);
} }

@ -119,3 +119,12 @@ export function get_store_value<T>(store: Readable<T>): T | undefined {
store.subscribe(_ => value = _)(); store.subscribe(_ => value = _)();
return 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}"` : ``;
}

@ -0,0 +1,40 @@
export default {
html: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello <b>world</b></p>
`,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello undefined</p>
`,
async test({ assert, component, target, window }) {
assert.equal(component.name, '<b>world</b>');
const el = target.querySelector('editor');
el.innerHTML = 'every<span>body</span>';
// No updates to data yet
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">every<span>body</span></editor>
<p>hello <b>world</b></p>
`);
// Handle user input
const event = new window.Event('input');
await el.dispatchEvent(event);
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">every<span>body</span></editor>
<p>hello every<span>body</span></p>
`);
component.name = 'good<span>bye</span>';
assert.equal(el.innerHTML, 'good<span>bye</span>');
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">good<span>bye</span></editor>
<p>hello good<span>bye</span></p>
`);
},
};

@ -0,0 +1,8 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:innerHTML={name}>
<b>world</b>
</editor>
<p>hello {@html name}</p>

@ -8,11 +8,6 @@ export default {
<p>hello <b>world</b></p> <p>hello <b>world</b></p>
`, `,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello <b>world</b></p>
`,
async test({ assert, component, target, window }) { async test({ assert, component, target, window }) {
const el = target.querySelector('editor'); const el = target.querySelector('editor');
assert.equal(el.innerHTML, '<b>world</b>'); assert.equal(el.innerHTML, '<b>world</b>');

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:innerHTML={name}></editor>
<p>hello {@html name}</p>

@ -0,0 +1,34 @@
export default {
html: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello world</p>
`,
ssrHtml: `
<editor contenteditable="true"><b>world</b></editor>
<p>hello undefined</p>
`,
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, `
<editor contenteditable="true">everybody</editor>
<p>hello everybody</p>
`);
component.name = 'goodbye';
assert.equal(el.textContent, 'goodbye');
assert.htmlEqual(target.innerHTML, `
<editor contenteditable="true">goodbye</editor>
<p>hello goodbye</p>
`);
},
};

@ -0,0 +1,8 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:textContent={name}>
<b>world</b>
</editor>
<p>hello {name}</p>

@ -8,11 +8,6 @@ export default {
<p>hello world</p> <p>hello world</p>
`, `,
ssrHtml: `
<editor contenteditable="true">world</editor>
<p>hello world</p>
`,
async test({ assert, component, target, window }) { async test({ assert, component, target, window }) {
const el = target.querySelector('editor'); const el = target.querySelector('editor');
assert.equal(el.textContent, 'world'); assert.equal(el.textContent, 'world');

@ -0,0 +1,6 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:textContent={name}></editor>
<p>hello {name}</p>

@ -1,6 +0,0 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:html={name}></editor>
<p>hello {@html name}</p>

@ -1,6 +0,0 @@
<script>
export let name;
</script>
<editor contenteditable="true" bind:text={name}></editor>
<p>hello {name}</p>

@ -3,4 +3,4 @@
let toggle = false; let toggle = false;
</script> </script>
<editor contenteditable={toggle} bind:html={name}></editor> <editor contenteditable={toggle} bind:innerHTML={name}></editor>

@ -1,6 +1,6 @@
[{ [{
"code": "missing-contenteditable-attribute", "code": "missing-contenteditable-attribute",
"message": "'contenteditable' attribute is required for text and html two-way bindings", "message": "'contenteditable' attribute is required for textContent and innerHTML two-way bindings",
"start": { "start": {
"line": 4, "line": 4,
"column": 8, "column": 8,
@ -8,8 +8,8 @@
}, },
"end": { "end": {
"line": 4, "line": 4,
"column": 24, "column": 31,
"character": 64 "character": 71
}, },
"pos": 48 "pos": 48
}] }]

@ -1,4 +1,4 @@
<script> <script>
export let name; export let name;
</script> </script>
<editor bind:text={name}></editor> <editor bind:textContent={name}></editor>

Loading…
Cancel
Save