Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Initialise html/text bindings from DOM #2996

Merged
merged 14 commits into from
Jun 24, 2019
Merged
8 changes: 8 additions & 0 deletions site/content/docs/02-template-syntax.md
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,14 @@ When the value of an `<option>` matches its text content, the attribute can be o
</select>
```

---

Elements with the `contenteditable` attribute support `innerHTML` and `textContent` bindings.

```html
<div contenteditable="true" bind:innerHTML={html}></div>
```

##### Media element bindings

---
Expand Down
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>
```
6 changes: 3 additions & 3 deletions src/compiler/compile/nodes/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -608,8 +608,8 @@ export default class Element extends Node {
});
}
} else if (
name === 'text' ||
name === 'html'
name === 'textContent' ||
name === 'innerHTML'
) {
const contenteditable = this.attributes.find(
(attribute: Attribute) => attribute.name === 'contenteditable'
Expand All @@ -618,7 +618,7 @@ export default class Element extends Node {
if (!contenteditable) {
component.error(binding, {
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) {
component.error(contenteditable, {
Expand Down
28 changes: 11 additions & 17 deletions src/compiler/compile/render-dom/wrappers/Element/Binding.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,14 @@ export default class BindingWrapper {
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 'playbackRate':
case 'volume':
Expand Down Expand Up @@ -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);
}
}
Expand Down Expand Up @@ -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};`;
}

return `${element.var}.${binding.node.name} = ${binding.snippet};`;
}

Expand Down Expand Up @@ -318,14 +320,6 @@ 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}`;
}
16 changes: 14 additions & 2 deletions src/compiler/compile/render-dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ const events = [
{
event_names: ['input'],
filter: (node: Element, name: string) =>
(name === 'text' || name === 'html') &&
(name === 'textContent' || name === 'innerHTML') &&
node.attributes.some(attribute => attribute.name === 'contenteditable')
},
{
Expand Down Expand Up @@ -510,7 +510,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 === 'textContent' ||
binding.node.name === 'innerHTML' ||
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});`
Expand Down
33 changes: 19 additions & 14 deletions src/compiler/compile/render-ssr/handlers/Element.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,11 @@ export default function(node: Element, renderer: Renderer, options: RenderOption
slot_scopes: Map<any, any>;
}) {
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' &&
Expand Down Expand Up @@ -150,33 +154,34 @@ 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);
if (name == 'text') {
node_contents = '${@escape(' + snippet + ')}';
} else {
// Do not escape HTML content
node_contents = '${' + snippet + '}';
}
} else if (contenteditable && (name === 'textContent' || name === 'innerHTML')) {
node_contents = snip(expression);
value = name === 'textContent' ? '@escape($$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 + ')}';
opening_tag += '${@add_attribute("' + name + '", ' + snippet + ')}';
}
});

if (add_class_attribute) {
opening_tag += `\${((v) => v ? ' class="' + v + '"' : '')([${class_expression}].join(' ').trim())}`;
opening_tag += `\${@add_classes([${class_expression}].join(' ').trim())}`;
}

opening_tag += '>';

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);
}
Expand Down
9 changes: 9 additions & 0 deletions src/runtime/internal/ssr.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,3 +119,12 @@ export function get_store_value<T>(store: Readable<T>): T | undefined {
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}"` : ``;
}
Original file line number Diff line number Diff line change
@@ -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>
`);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
export let name;
</script>

<editor contenteditable="true" bind:innerHTML={name}>
<b>world</b>
</editor>
<p>hello {@html name}</p>
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ export default {
<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 }) {
const el = target.querySelector('editor');
assert.equal(el.innerHTML, '<b>world</b>');
Expand Down
6 changes: 6 additions & 0 deletions test/runtime/samples/binding-contenteditable-html/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
export let name;
</script>

<editor contenteditable="true" bind:innerHTML={name}></editor>
<p>hello {@html name}</p>
Original file line number Diff line number Diff line change
@@ -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>
`);
},
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<script>
export let name;
</script>

<editor contenteditable="true" bind:textContent={name}>
<b>world</b>
</editor>
<p>hello {name}</p>
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,6 @@ export default {
<p>hello world</p>
`,

ssrHtml: `
<editor contenteditable="true">world</editor>
<p>hello world</p>
`,

async test({ assert, component, target, window }) {
const el = target.querySelector('editor');
assert.equal(el.textContent, 'world');
Expand Down
6 changes: 6 additions & 0 deletions test/runtime/samples/binding-contenteditable-text/main.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<script>
export let name;
</script>

<editor contenteditable="true" bind:textContent={name}></editor>
<p>hello {name}</p>
6 changes: 0 additions & 6 deletions test/runtime/samples/contenteditable-html/main.svelte

This file was deleted.

6 changes: 0 additions & 6 deletions test/runtime/samples/contenteditable-text/main.svelte

This file was deleted.

Loading