Skip to content

Commit

Permalink
spread condition for input element
Browse files Browse the repository at this point in the history
  • Loading branch information
tanhauhau committed Jun 11, 2020
1 parent 1c05785 commit 3e930a0
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 84 deletions.
183 changes: 110 additions & 73 deletions src/compiler/compile/render_dom/wrappers/Element/Attribute.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { string_literal } from '../../../utils/stringify';
import { b, x } from 'code-red';
import Expression from '../../../nodes/shared/Expression';
import Text from '../../../nodes/Text';
import { Identifier, Node } from 'estree';

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

Expand All @@ -20,7 +21,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 @@ -37,31 +60,22 @@ export default class AttributeWrapper {
}
}
}
}

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 @@ -77,81 +91,59 @@ export default class AttributeWrapper {
const dependencies = this.node.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() || is_select_value_attribute; // TODO is this necessary?

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');

if (is_multiple_select) {
updater = b`@select_options(${element.var}, ${last});`;
updater = b`@select_options(${element.var}, ${this.last});`;
} else {
updater = b`@select_option(${element.var}, ${last});`;
updater = b`@select_option(${element.var}, ${this.last});`;
}

block.chunks.mount.push(b`
${last} = ${value};
${this.last} = ${value};
${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 @@ -163,26 +155,56 @@ 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: Node) {
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 (this.node.get_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_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 @@ -330,4 +352,19 @@ const boolean_attribute = new Set([
'required',
'reversed',
'selected'
]);
]);

function should_cache(attribute: AttributeWrapper) {
return attribute.is_src || attribute.node.should_cache() || attribute.is_select_value_attribute; // TODO is this necessary?
}

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 {}
25 changes: 14 additions & 11 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 @@ -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

0 comments on commit 3e930a0

Please sign in to comment.