import Wrapper from '../shared/Wrapper'; import BindingWrapper from '../Element/Binding'; import Renderer from '../../Renderer'; import Block from '../../Block'; import InlineComponent from '../../../nodes/InlineComponent'; import FragmentWrapper from '../Fragment'; import SlotTemplateWrapper from '../SlotTemplate'; import { sanitize } from '../../../../utils/names'; import add_to_set from '../../../utils/add_to_set'; import { b, x, p } from 'code-red'; import Attribute from '../../../nodes/Attribute'; import TemplateScope from '../../../nodes/shared/TemplateScope'; import is_dynamic from '../shared/is_dynamic'; import bind_this from '../shared/bind_this'; import { Node, Identifier, ObjectExpression } from 'estree'; import EventHandler from '../Element/EventHandler'; import { extract_names } from 'periscopic'; import mark_each_block_bindings from '../shared/mark_each_block_bindings'; import { string_to_member_expression } from '../../../utils/string_to_member_expression'; import SlotTemplate from '../../../nodes/SlotTemplate'; import { is_head } from '../shared/is_head'; import compiler_warnings from '../../../compiler_warnings'; import { namespaces } from '../../../../utils/namespaces'; type SlotDefinition = { block: Block; scope: TemplateScope; get_context?: Node; get_changes?: Node }; const regex_invalid_variable_identifier_characters = /[^a-zA-Z_$]/g; export default class InlineComponentWrapper extends Wrapper { var: Identifier; slots: Map<string, SlotDefinition> = new Map(); node: InlineComponent; fragment: FragmentWrapper; children: Array<Wrapper | FragmentWrapper> = []; constructor( renderer: Renderer, block: Block, parent: Wrapper, node: InlineComponent, strip_whitespace: boolean, next_sibling: Wrapper ) { super(renderer, block, parent, node); this.cannot_use_innerhtml(); this.not_static_content(); if (this.node.expression) { block.add_dependencies(this.node.expression.dependencies); } this.node.attributes.forEach(attr => { block.add_dependencies(attr.dependencies); }); this.node.bindings.forEach(binding => { if (binding.is_contextual) { mark_each_block_bindings(this, binding); } block.add_dependencies(binding.expression.dependencies); }); this.node.handlers.forEach(handler => { if (handler.expression) { block.add_dependencies(handler.expression.dependencies); } }); this.node.css_custom_properties.forEach(attr => { block.add_dependencies(attr.dependencies); }); this.var = { type: 'Identifier', name: ( this.node.name === 'svelte:self' ? renderer.component.name.name : this.node.name === 'svelte:component' ? 'switch_instance' : sanitize(this.node.name) ).toLowerCase() }; if (this.node.children.length) { this.node.lets.forEach(l => { extract_names(l.value || l.name).forEach(name => { renderer.add_to_context(name, true); }); }); this.children = this.node.children.map(child => new SlotTemplateWrapper(renderer, block, this, child as SlotTemplate, strip_whitespace, next_sibling)); } block.add_outro(); } set_slot(name: string, slot_definition: SlotDefinition) { if (this.slots.has(name)) { if (name === 'default') { throw new Error('Found elements without slot attribute when using slot="default"'); } throw new Error(`Duplicate slot name "${name}" in <${this.node.name}>`); } this.slots.set(name, slot_definition); } warn_if_reactive() { const { name } = this.node; const variable = this.renderer.component.var_lookup.get(name); if (!variable) { return; } if (variable.reassigned || variable.export_name || variable.is_reactive_dependency) { this.renderer.component.warn(this.node, compiler_warnings.reactive_component(name)); } } render( block: Block, parent_node: Identifier, parent_nodes: Identifier ) { this.warn_if_reactive(); const { renderer } = this; const { component } = renderer; const name = this.var; block.add_variable(name); const component_opts = x`{}` as ObjectExpression; const statements: Array<Node | Node[]> = []; const updates: Array<Node | Node[]> = []; this.children.forEach((child) => { this.renderer.add_to_context('$$scope', true); child.render(block, null, x`#nodes` as Identifier); }); let props: Identifier | undefined; const name_changes = block.get_unique_name(`${name.name}_changes`); const uses_spread = !!this.node.attributes.find(a => a.is_spread); // removing empty slot for (const slot of this.slots.keys()) { if (!this.slots.get(slot).block.has_content()) { this.renderer.remove_block(this.slots.get(slot).block); this.slots.delete(slot); } } const has_css_custom_properties = this.node.css_custom_properties.length > 0; const is_svg_namespace = this.node.namespace === namespaces.svg; const css_custom_properties_wrapper_element = is_svg_namespace ? 'g' : 'div'; const css_custom_properties_wrapper = has_css_custom_properties ? block.get_unique_name(css_custom_properties_wrapper_element) : null; if (has_css_custom_properties) { block.add_variable(css_custom_properties_wrapper); } const initial_props = this.slots.size > 0 ? [ p`$$slots: { ${Array.from(this.slots).map(([name, slot]) => { return p`${name}: [${slot.block.name}, ${slot.get_context || null}, ${slot.get_changes || null}]`; })} }`, p`$$scope: { ctx: #ctx }` ] : []; const attribute_object = uses_spread ? x`{ ${initial_props} }` : x`{ ${this.node.attributes.map(attr => p`${attr.name}: ${attr.get_value(block)}`)}, ${initial_props} }`; if (this.node.attributes.length || this.node.bindings.length || initial_props.length) { if (!uses_spread && this.node.bindings.length === 0) { component_opts.properties.push(p`props: ${attribute_object}`); } else { props = block.get_unique_name(`${name.name}_props`); component_opts.properties.push(p`props: ${props}`); } } if (component.compile_options.dev) { // TODO this is a terrible hack, but without it the component // will complain that options.target is missing. This would // work better if components had separate public and private // APIs component_opts.properties.push(p`$$inline: true`); } const fragment_dependencies = new Set(this.slots.size ? ['$$scope'] : []); this.slots.forEach(slot => { slot.block.dependencies.forEach(name => { const is_let = slot.scope.is_let(name); const variable = renderer.component.var_lookup.get(name); if (is_let || is_dynamic(variable)) fragment_dependencies.add(name); }); }); const dynamic_attributes = this.node.attributes.filter(a => a.get_dependencies().length > 0); if (!uses_spread && (dynamic_attributes.length > 0 || this.node.bindings.length > 0 || fragment_dependencies.size > 0)) { updates.push(b`const ${name_changes} = {};`); } if (this.node.attributes.length) { if (uses_spread) { const levels = block.get_unique_name(`${this.var.name}_spread_levels`); const initial_props = []; const changes = []; const all_dependencies: Set<string> = new Set(); this.node.attributes.forEach(attr => { add_to_set(all_dependencies, attr.dependencies); }); this.node.attributes.forEach((attr, i) => { const { name, dependencies } = attr; const condition = dependencies.size > 0 && (dependencies.size !== all_dependencies.size) ? renderer.dirty(Array.from(dependencies)) : null; const unchanged = dependencies.size === 0; let change_object: Node | ReturnType<typeof x>; if (attr.is_spread) { const value = attr.expression.manipulate(block); initial_props.push(value); let value_object = value; if (attr.expression.node.type !== 'ObjectExpression') { value_object = x`@get_spread_object(${value})`; } change_object = value_object; } else { const obj = x`{ ${name}: ${attr.get_value(block)} }`; initial_props.push(obj); change_object = obj; } changes.push( unchanged ? x`${levels}[${i}]` : condition ? x`${condition} && ${change_object}` : change_object ); }); block.chunks.init.push(b` const ${levels} = [ ${initial_props} ]; `); statements.push(b` for (let #i = 0; #i < ${levels}.length; #i += 1) { ${props} = @assign(${props}, ${levels}[#i]); } `); if (all_dependencies.size) { const condition = renderer.dirty(Array.from(all_dependencies)); updates.push(b` const ${name_changes} = ${condition} ? @get_spread_update(${levels}, [ ${changes} ]) : {} `); } else { updates.push(b` const ${name_changes} = {}; `); } } else { dynamic_attributes.forEach((attribute: Attribute) => { const dependencies = attribute.get_dependencies(); if (dependencies.length > 0) { const condition = renderer.dirty(dependencies); updates.push(b` if (${condition}) ${name_changes}.${attribute.name} = ${attribute.get_value(block)}; `); } }); } } if (fragment_dependencies.size > 0) { updates.push(b` if (${renderer.dirty(Array.from(fragment_dependencies))}) { ${name_changes}.$$scope = { dirty: #dirty, ctx: #ctx }; }`); } const munged_bindings = this.node.bindings.map(binding => { component.has_reactive_assignments = true; if (binding.name === 'this') { return bind_this(component, block, new BindingWrapper(block, binding, this), this.var); } const id = component.get_unique_name(`${this.var.name}_${binding.name}_binding`); renderer.add_to_context(id.name); const callee = renderer.reference(id); const updating = block.get_unique_name(`updating_${binding.name}`); block.add_variable(updating); const snippet = binding.expression.manipulate(block); statements.push(b` if (${snippet} !== void 0) { ${props}.${binding.name} = ${snippet}; }` ); updates.push(b` if (!${updating} && ${renderer.dirty(Array.from(binding.expression.dependencies))}) { ${updating} = true; ${name_changes}.${binding.name} = ${snippet}; @add_flush_callback(() => ${updating} = false); } `); const contextual_dependencies = Array.from(binding.expression.contextual_dependencies); const dependencies = Array.from(binding.expression.dependencies); let lhs = binding.raw_expression; if (binding.is_contextual && binding.expression.node.type === 'Identifier') { // bind:x={y} — we can't just do `y = x`, we need to // to `array[index] = x; const { name } = binding.expression.node; const { object, property, snippet } = block.bindings.get(name); lhs = snippet; contextual_dependencies.push(object.name, property.name); } const params: Identifier[] = [x`#value` as Identifier]; const args = [x`#value`]; if (contextual_dependencies.length > 0) { contextual_dependencies.forEach(name => { params.push({ type: 'Identifier', name }); renderer.add_to_context(name, true); args.push(renderer.reference(name)); }); block.maintain_context = true; // TODO put this somewhere more logical } block.chunks.init.push(b` function ${id}(#value) { ${callee}(${args}); } `); let invalidate_binding = b` ${lhs} = #value; ${renderer.invalidate(dependencies[0])}; `; if (binding.expression.node.type === 'MemberExpression') { invalidate_binding = b` if ($$self.$$.not_equal(${lhs}, #value)) { ${invalidate_binding} } `; } const body = b` function ${id}(${params}) { ${invalidate_binding} } `; component.partly_hoisted.push(body); return b`@binding_callbacks.push(() => @bind(${this.var}, '${binding.name}', ${id}));`; }); const munged_handlers = this.node.handlers.map(handler => { const event_handler = new EventHandler(handler, this); let snippet = event_handler.get_snippet(block); if (handler.modifiers.has('once')) snippet = x`@once(${snippet})`; return b`${name}.$on("${handler.name}", ${snippet});`; }); const mount_target = has_css_custom_properties ? css_custom_properties_wrapper : (parent_node || '#target'); const mount_anchor = has_css_custom_properties ? 'null' : (parent_node ? 'null' : '#anchor'); const to_claim = parent_nodes && this.renderer.options.hydratable; let claim_nodes = parent_nodes; if (this.node.name === 'svelte:component') { const switch_value = block.get_unique_name('switch_value'); const switch_props = block.get_unique_name('switch_props'); const snippet = this.node.expression.manipulate(block); if (has_css_custom_properties) { this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace); } block.chunks.init.push(b` var ${switch_value} = ${snippet}; function ${switch_props}(#ctx) { ${(this.node.attributes.length > 0 || this.node.bindings.length > 0) && b` ${props && b`let ${props} = ${attribute_object};`}`} ${statements} return ${component_opts}; } if (${switch_value}) { ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); ${munged_bindings} ${munged_handlers} } `); block.chunks.create.push( b`if (${name}) @create_component(${name}.$$.fragment);` ); if (css_custom_properties_wrapper) this.create_css_custom_properties_wrapper_mount_chunk(block, parent_node, css_custom_properties_wrapper); block.chunks.mount.push(b`if (${name}) @mount_component(${name}, ${mount_target}, ${mount_anchor});`); if (to_claim) { if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace); block.chunks.claim.push(b`if (${name}) @claim_component(${name}.$$.fragment, ${claim_nodes});`); } if (updates.length) { block.chunks.update.push(b` ${updates} `); } const tmp_anchor = this.get_or_create_anchor(block, parent_node, parent_nodes); const anchor = has_css_custom_properties ? 'null' : tmp_anchor; const update_mount_node = has_css_custom_properties ? css_custom_properties_wrapper : this.get_update_mount_node(tmp_anchor); const update_insert = css_custom_properties_wrapper && (tmp_anchor.name !== 'null' ? b`@insert(${tmp_anchor}.parentNode, ${css_custom_properties_wrapper}, ${tmp_anchor});` : b`@insert(${parent_node}, ${css_custom_properties_wrapper}, ${tmp_anchor});`); block.chunks.update.push(b` if (${switch_value} !== (${switch_value} = ${snippet})) { if (${name}) { @group_outros(); const old_component = ${name}; @transition_out(old_component.$$.fragment, 1, 0, () => { @destroy_component(old_component, 1); ${has_css_custom_properties ? b`@detach(${update_mount_node})` : null} }); @check_outros(); } if (${switch_value}) { ${update_insert} ${name} = @construct_svelte_component(${switch_value}, ${switch_props}(#ctx)); ${munged_bindings} ${munged_handlers} @create_component(${name}.$$.fragment); @transition_in(${name}.$$.fragment, 1); @mount_component(${name}, ${update_mount_node}, ${anchor}); } else { ${name} = null; } } else if (${switch_value}) { ${updates.length > 0 && b`${name}.$set(${name_changes});`} } `); block.chunks.intro.push(b` if (${name}) @transition_in(${name}.$$.fragment, #local); `); block.chunks.outro.push( b`if (${name}) @transition_out(${name}.$$.fragment, #local);` ); block.chunks.destroy.push(b`if (${name}) @destroy_component(${name}, ${parent_node ? null : 'detaching'});`); } else { const expression = this.node.name === 'svelte:self' ? component.name : this.renderer.reference(string_to_member_expression(this.node.name)); block.chunks.init.push(b` ${(this.node.attributes.length > 0 || this.node.bindings.length > 0) && b` ${props && b`let ${props} = ${attribute_object};`}`} ${statements} ${name} = new ${expression}(${component_opts}); ${munged_bindings} ${munged_handlers} `); if (has_css_custom_properties) { this.set_css_custom_properties(block, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace); } block.chunks.create.push(b`@create_component(${name}.$$.fragment);`); if (css_custom_properties_wrapper) this.create_css_custom_properties_wrapper_mount_chunk(block, parent_node, css_custom_properties_wrapper); block.chunks.mount.push(b`@mount_component(${name}, ${mount_target}, ${mount_anchor});`); if (to_claim) { if (css_custom_properties_wrapper) claim_nodes = this.create_css_custom_properties_wrapper_claim_chunk(block, claim_nodes, css_custom_properties_wrapper, css_custom_properties_wrapper_element, is_svg_namespace); block.chunks.claim.push(b`@claim_component(${name}.$$.fragment, ${claim_nodes});`); } block.chunks.intro.push(b` @transition_in(${name}.$$.fragment, #local); `); if (updates.length) { block.chunks.update.push(b` ${updates} ${name}.$set(${name_changes}); `); } block.chunks.destroy.push(b` @destroy_component(${name}, ${parent_node ? null : 'detaching'}); `); block.chunks.outro.push( b`@transition_out(${name}.$$.fragment, #local);` ); } } private create_css_custom_properties_wrapper_mount_chunk( block: Block, parent_node: Identifier, css_custom_properties_wrapper: Identifier | null ) { if (parent_node) { block.chunks.mount.push(b`@append(${parent_node}, ${css_custom_properties_wrapper})`); if (is_head(parent_node)) { block.chunks.destroy.push(b`@detach(${css_custom_properties_wrapper});`); } } else { block.chunks.mount.push(b`@insert(#target, ${css_custom_properties_wrapper}, #anchor);`); // TODO we eventually need to consider what happens to elements // that belong to the same outgroup as an outroing element... block.chunks.destroy.push(b`if (detaching && ${this.var}) @detach(${css_custom_properties_wrapper});`); } } private create_css_custom_properties_wrapper_claim_chunk( block: Block, parent_nodes: Identifier, css_custom_properties_wrapper: Identifier | null, css_custom_properties_wrapper_element: string, is_svg_namespace: boolean ) { const nodes = block.get_unique_name(`${css_custom_properties_wrapper.name}_nodes`); const claim_element = is_svg_namespace ? x`@claim_svg_element` : x`@claim_element`; block.chunks.claim.push(b` ${css_custom_properties_wrapper} = ${claim_element}(${parent_nodes}, "${css_custom_properties_wrapper_element.toUpperCase()}", { style: true }) var ${nodes} = @children(${css_custom_properties_wrapper}); `); return nodes; } private set_css_custom_properties( block: Block, css_custom_properties_wrapper: Identifier, css_custom_properties_wrapper_element: string, is_svg_namespace: boolean ) { const element = is_svg_namespace ? x`@svg_element` : x`@element`; block.chunks.create.push(b`${css_custom_properties_wrapper} = ${element}("${css_custom_properties_wrapper_element}");`); if (!is_svg_namespace) block.chunks.hydrate.push(b`@set_style(${css_custom_properties_wrapper}, "display", "contents");`); this.node.css_custom_properties.forEach((attr) => { const dependencies = attr.get_dependencies(); const should_cache = attr.should_cache(); const last = should_cache && block.get_unique_name(`${attr.name.replace(regex_invalid_variable_identifier_characters, '_')}_last`); if (should_cache) block.add_variable(last); const value = attr.get_value(block); const init = should_cache ? x`${last} = ${value}` : value; block.chunks.hydrate.push( b`@set_style(${css_custom_properties_wrapper}, "${attr.name}", ${init});` ); if (dependencies.length > 0) { let condition = block.renderer.dirty(dependencies); if (should_cache) condition = x`${condition} && (${last} !== (${last} = ${value}))`; block.chunks.update.push(b` if (${condition}) { @set_style(${css_custom_properties_wrapper}, "${attr.name}", ${should_cache ? last : value}); } `); } }); } }