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

spread condition for input element #5004

Merged
merged 3 commits into from
Jul 7, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
177 changes: 107 additions & 70 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,9 @@ import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import handle_select_value_binding from './handle_select_value_binding';
import { Identifier, Node } from 'estree';

export default class AttributeWrapper {
export class BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;

Expand All @@ -21,7 +22,29 @@ export default class AttributeWrapper {
parent.not_static_content();

block.add_dependencies(node.dependencies);
}
}

render(_block: Block) {}
}

export default class AttributeWrapper extends BaseAttributeWrapper {
node: Attribute;
parent: ElementWrapper;
metadata: any;
name: string;
property_name: string;
is_indirectly_bound_value: boolean;
is_src: boolean;
is_select_value_attribute: boolean;
is_input_value: boolean;
should_cache: boolean;
last: Identifier;

constructor(parent: ElementWrapper, block: Block, node: Attribute) {
super(parent, block, node);

if (node.dependencies.size > 0) {
// special case — <option value={foo}> — see below
if (this.parent.node.name === 'option' && node.name === 'value') {
let select: ElementWrapper = this.parent;
Expand All @@ -42,31 +65,22 @@ export default class AttributeWrapper {
handle_select_value_binding(this, node.dependencies);
}
}
}

is_indirectly_bound_value() {
const element = this.parent;
const name = fix_attribute_casing(this.node.name);
return name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.some(
(binding) =>
/checked|group/.test(binding.name)
)));
this.name = fix_attribute_casing(this.node.name);
this.metadata = this.get_metadata();
this.is_indirectly_bound_value = is_indirectly_bound_value(this);
this.property_name = this.is_indirectly_bound_value
? '__value'
: this.metadata && this.metadata.property_name;
this.is_src = this.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
this.is_select_value_attribute = this.name === 'value' && this.parent.node.name === 'select';
this.is_input_value = this.name === 'value' && this.parent.node.name === 'input';
this.should_cache = should_cache(this);
}

render(block: Block) {
const element = this.parent;
const name = fix_attribute_casing(this.node.name);

const metadata = this.get_metadata();

const is_indirectly_bound_value = this.is_indirectly_bound_value();

const property_name = is_indirectly_bound_value
? '__value'
: metadata && metadata.property_name;
const { name, property_name, should_cache, is_indirectly_bound_value } = this;

// xlink is a special case... we could maybe extend this to generic
// namespaced attributes but I'm not sure that's applicable in
Expand All @@ -82,29 +96,15 @@ export default class AttributeWrapper {
const dependencies = this.get_dependencies();
const value = this.get_value(block);

const is_src = this.node.name === 'src'; // TODO retire this exception in favour of https://github.com/sveltejs/svelte/issues/3750
const is_select_value_attribute =
name === 'value' && element.node.name === 'select';

const is_input_value = name === 'value' && element.node.name === 'input';

const should_cache = is_src || this.node.should_cache();

const last = should_cache && block.get_unique_name(
`${element.var.name}_${name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);

if (should_cache) block.add_variable(last);

let updater;
const init = should_cache ? x`${last} = ${value}` : value;
const init = this.get_init(block, value);

if (is_legacy_input_type) {
block.chunks.hydrate.push(
b`@set_input_type(${element.var}, ${init});`
);
updater = b`@set_input_type(${element.var}, ${should_cache ? last : value});`;
} else if (is_select_value_attribute) {
updater = b`@set_input_type(${element.var}, ${should_cache ? this.last : value});`;
} else if (this.is_select_value_attribute) {
// annoying special case
const is_multiple_select = element.node.get_static_attribute_value('multiple');

Expand All @@ -117,45 +117,37 @@ export default class AttributeWrapper {
block.chunks.mount.push(b`
${updater}
`);
} else if (is_src) {
} else if (this.is_src) {
block.chunks.hydrate.push(
b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${last});`
b`if (${element.var}.src !== ${init}) ${method}(${element.var}, "${name}", ${this.last});`
);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
} else if (property_name) {
block.chunks.hydrate.push(
b`${element.var}.${property_name} = ${init};`
);
updater = block.renderer.options.dev
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? last : value});`
: b`${element.var}.${property_name} = ${should_cache ? last : value};`;
? b`@prop_dev(${element.var}, "${property_name}", ${should_cache ? this.last : value});`
: b`${element.var}.${property_name} = ${should_cache ? this.last : value};`;
} else {
block.chunks.hydrate.push(
b`${method}(${element.var}, "${name}", ${init});`
);
updater = b`${method}(${element.var}, "${name}", ${should_cache ? last : value});`;
updater = b`${method}(${element.var}, "${name}", ${should_cache ? this.last : value});`;
}

if (dependencies.length > 0) {
let condition = block.renderer.dirty(dependencies);

if (should_cache) {
condition = is_src
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}

if (is_input_value) {
const type = element.node.get_static_attribute_value('type');
if (is_indirectly_bound_value) {
const update_value = b`${element.var}.value = ${element.var}.__value;`;
block.chunks.hydrate.push(update_value);

if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
}
}
updater = b`
${updater}
${update_value};
`;
}

if (block.has_outros) {
condition = x`!#current || ${condition}`;
}
if (dependencies.length > 0) {
const condition = this.get_dom_update_conditions(block, block.renderer.dirty(dependencies));

block.chunks.update.push(b`
if (${condition}) {
Expand All @@ -167,13 +159,44 @@ export default class AttributeWrapper {
if (this.node.is_true && name === 'autofocus') {
block.autofocus = element.var;
}
}

if (is_indirectly_bound_value) {
const update_value = b`${element.var}.value = ${element.var}.__value;`;
get_init(block: Block, value) {
this.last = this.should_cache && block.get_unique_name(
`${this.parent.var.name}_${this.name.replace(/[^a-zA-Z_$]/g, '_')}_value`
);

block.chunks.hydrate.push(update_value);
if (dependencies.length > 0) block.chunks.update.push(update_value);
if (this.should_cache) block.add_variable(this.last);

return this.should_cache ? x`${this.last} = ${value}` : value;
}

get_dom_update_conditions(block: Block, dependency_condition: Node) {
const { property_name, should_cache, last } = this;
const element = this.parent;
const value = this.get_value(block);

let condition = dependency_condition;

if (should_cache) {
condition = this.is_src
? x`${condition} && (${element.var}.src !== (${last} = ${value}))`
: x`${condition} && (${last} !== (${last} = ${value}))`;
}

if (this.is_input_value) {
const type = element.node.get_static_attribute_value('type');

if (type === null || type === "" || type === "text" || type === "email" || type === "password") {
condition = x`${condition} && ${element.var}.${property_name} !== ${should_cache ? last : value}`;
}
}

if (block.has_outros) {
condition = x`!#current || ${condition}`;
}

return condition;
}

get_dependencies() {
Expand All @@ -194,15 +217,14 @@ export default class AttributeWrapper {

get_metadata() {
if (this.parent.node.namespace) return null;
const metadata = attribute_lookup[fix_attribute_casing(this.node.name)];
const metadata = attribute_lookup[this.name];
if (metadata && metadata.applies_to && !metadata.applies_to.includes(this.parent.node.name)) return null;
return metadata;
}

get_value(block) {
if (this.node.is_true) {
const metadata = this.get_metadata();
if (metadata && boolean_attribute.has(metadata.property_name.toLowerCase())) {
if (this.metadata && boolean_attribute.has(this.metadata.property_name.toLowerCase())) {
return x`true`;
}
return x`""`;
Expand Down Expand Up @@ -350,4 +372,19 @@ const boolean_attribute = new Set([
'required',
'reversed',
'selected'
]);
]);

function should_cache(attribute: AttributeWrapper) {
return attribute.is_src || attribute.node.should_cache();
}

function is_indirectly_bound_value(attribute: AttributeWrapper) {
const element = attribute.parent;
return attribute.name === 'value' &&
(element.node.name === 'option' || // TODO check it's actually bound
(element.node.name === 'input' &&
element.node.bindings.some(
(binding) =>
/checked|group/.test(binding.name)
)));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { BaseAttributeWrapper } from "./Attribute";

export default class SpreadAttributeWrapper extends BaseAttributeWrapper {}
27 changes: 15 additions & 12 deletions src/compiler/compile/render_dom/wrappers/Element/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import { b, x, p } from 'code-red';
import { namespaces } from '../../../../utils/namespaces';
import AttributeWrapper from './Attribute';
import StyleAttributeWrapper from './StyleAttribute';
import SpreadAttributeWrapper from './SpreadAttribute';
import { dimensions } from '../../../../utils/patterns';
import Binding from './Binding';
import InlineComponentWrapper from '../InlineComponent';
Expand Down Expand Up @@ -136,7 +137,7 @@ const events = [
export default class ElementWrapper extends Wrapper {
node: Element;
fragment: FragmentWrapper;
attributes: AttributeWrapper[];
attributes: Array<AttributeWrapper | StyleAttributeWrapper | SpreadAttributeWrapper>;
bindings: Binding[];
event_handlers: EventHandler[];
class_dependencies: string[];
Expand Down Expand Up @@ -220,6 +221,9 @@ export default class ElementWrapper extends Wrapper {
if (attribute.name === 'style') {
return new StyleAttributeWrapper(this, block, attribute);
}
if (attribute.type === 'Spread') {
return new SpreadAttributeWrapper(this, block, attribute);
}
return new AttributeWrapper(this, block, attribute);
});

Expand Down Expand Up @@ -418,7 +422,7 @@ export default class ElementWrapper extends Wrapper {
return x`@_document.createElementNS("${namespace}", "${name}")`;
}

const is = this.attributes.find(attr => attr.node.name === 'is');
const is: AttributeWrapper = this.attributes.find(attr => attr.node.name === 'is') as any;
if (is) {
return x`@element_is("${name}", ${is.render_chunks(block).reduce((lhs, rhs) => x`${lhs} + ${rhs}`)})`;
}
Expand Down Expand Up @@ -664,25 +668,24 @@ export default class ElementWrapper extends Wrapper {

this.attributes
.forEach(attr => {
const condition = attr.node.dependencies.size > 0
? block.renderer.dirty(Array.from(attr.node.dependencies))
const dependencies = attr.node.get_dependencies();

const condition = dependencies.length > 0
? block.renderer.dirty(dependencies)
: null;

if (attr.node.is_spread) {
if (attr instanceof SpreadAttributeWrapper) {
const snippet = attr.node.expression.manipulate(block);

initial_props.push(snippet);

updates.push(condition ? x`${condition} && ${snippet}` : snippet);
} else {
const metadata = attr.get_metadata();
const name = attr.is_indirectly_bound_value()
? '__value'
: (metadata && metadata.property_name) || fix_attribute_casing(attr.node.name);
const snippet = x`{ ${name}: ${attr.get_value(block)} }`;
initial_props.push(snippet);
const name = attr.property_name || attr.name;
initial_props.push(x`{ ${name}: ${attr.get_init(block, attr.get_value(block))} }`);
const snippet = x`{ ${name}: ${attr.should_cache ? attr.last : attr.get_value(block)} }`;

updates.push(condition ? x`${condition} && ${snippet}` : snippet);
updates.push(condition ? x`${attr.get_dom_update_conditions(block, condition)} && ${snippet}` : snippet);
}
});

Expand Down
18 changes: 18 additions & 0 deletions test/runtime/samples/spread-element-input-value/InputOne.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<script>
import { omit } from './utils.js';

export let value;

function onInput(e) {
value = e.target.value;
}

$: props = omit($$props, 'value');
</script>

<input
type="text"
{...props}
on:input={onInput}
{value}
/>
19 changes: 19 additions & 0 deletions test/runtime/samples/spread-element-input-value/InputTwo.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<script>
import { omit } from './utils.js';

export let value;

function onInput(e) {
value = e.target.value;
}

$: props = omit($$props, 'value', 'minlength');
</script>

<input
type="text"
minlength="10"
value={value}
{...props}
on:input={onInput}
/>
Loading