svelte/src/compiler/compile/render_dom/Block.ts

419 lines
10 KiB

import CodeBuilder from '../utils/CodeBuilder';
import deindent from '../utils/deindent';
import Renderer from './Renderer';
import Wrapper from './wrappers/shared/Wrapper';
import { escape } from '../utils/stringify';
export interface BlockOptions {
parent?: Block;
name: string;
renderer?: Renderer;
comment?: string;
key?: string;
bindings?: Map<string, { object: string; property: string; snippet: string }>;
dependencies?: Set<string>;
}
export default class Block {
parent?: Block;
renderer: Renderer;
name: string;
comment?: string;
wrappers: Wrapper[];
key: string;
first: string;
dependencies: Set<string>;
bindings: Map<string, { object: string; property: string; snippet: string }>;
builders: {
init: CodeBuilder;
create: CodeBuilder;
claim: CodeBuilder;
hydrate: CodeBuilder;
mount: CodeBuilder;
measure: CodeBuilder;
fix: CodeBuilder;
animate: CodeBuilder;
intro: CodeBuilder;
update: CodeBuilder;
outro: CodeBuilder;
destroy: CodeBuilder;
};
event_listeners: string[] = [];
maintain_context: boolean;
has_animation: boolean;
has_intros: boolean;
has_outros: boolean;
has_intro_method: boolean; // could have the method without the transition, due to siblings
has_outro_method: boolean;
outros: number;
aliases: Map<string, string>;
variables: Map<string, string>;
get_unique_name: (name: string) => string;
has_update_method = false;
autofocus: string;
constructor(options: BlockOptions) {
this.parent = options.parent;
this.renderer = options.renderer;
this.name = options.name;
this.comment = options.comment;
this.wrappers = [];
// for keyed each blocks
this.key = options.key;
this.first = null;
this.dependencies = new Set();
this.bindings = options.bindings;
this.builders = {
init: new CodeBuilder(),
create: new CodeBuilder(),
claim: new CodeBuilder(),
hydrate: new CodeBuilder(),
mount: new CodeBuilder(),
measure: new CodeBuilder(),
fix: new CodeBuilder(),
animate: new CodeBuilder(),
intro: new CodeBuilder(),
update: new CodeBuilder(),
outro: new CodeBuilder(),
destroy: new CodeBuilder(),
};
this.has_animation = false;
this.has_intro_method = false; // a block could have an intro method but not intro transitions, e.g. if a sibling block has intros
this.has_outro_method = false;
this.outros = 0;
this.get_unique_name = this.renderer.component.get_unique_name_maker();
this.variables = new Map();
this.aliases = new Map().set('ctx', this.get_unique_name('ctx'));
if (this.key) this.aliases.set('key', this.get_unique_name('key'));
}
assign_variable_names() {
const seen = new Set();
const dupes = new Set();
let i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (wrapper.parent && wrapper.parent.can_use_innerhtml) continue;
if (seen.has(wrapper.var)) {
dupes.add(wrapper.var);
}
seen.add(wrapper.var);
}
const counts = new Map();
i = this.wrappers.length;
while (i--) {
const wrapper = this.wrappers[i];
if (!wrapper.var) continue;
if (dupes.has(wrapper.var)) {
const i = counts.get(wrapper.var) || 0;
counts.set(wrapper.var, i + 1);
wrapper.var = this.get_unique_name(wrapper.var + i);
} else {
wrapper.var = this.get_unique_name(wrapper.var);
}
}
}
add_dependencies(dependencies: Set<string>) {
dependencies.forEach(dependency => {
this.dependencies.add(dependency);
});
this.has_update_method = true;
}
add_element(
name: string,
render_statement: string,
claim_statement: string,
parent_node: string,
no_detach?: boolean
) {
this.add_variable(name);
this.builders.create.add_line(`${name} = ${render_statement};`);
if (this.renderer.options.hydratable) {
this.builders.claim.add_line(`${name} = ${claim_statement || render_statement};`);
}
if (parent_node) {
this.builders.mount.add_line(`@append(${parent_node}, ${name});`);
if (parent_node === '@_document.head' && !no_detach) this.builders.destroy.add_line(`@detach(${name});`);
} else {
this.builders.mount.add_line(`@insert(#target, ${name}, anchor);`);
if (!no_detach) this.builders.destroy.add_conditional('detaching', `@detach(${name});`);
}
}
add_intro(local?: boolean) {
this.has_intros = this.has_intro_method = true;
if (!local && this.parent) this.parent.add_intro();
}
add_outro(local?: boolean) {
this.has_outros = this.has_outro_method = true;
this.outros += 1;
if (!local && this.parent) this.parent.add_outro();
}
add_animation() {
this.has_animation = true;
}
add_variable(name: string, init?: string) {
if (name[0] === '#') {
name = this.alias(name.slice(1));
}
if (this.variables.has(name) && this.variables.get(name) !== init) {
throw new Error(
`Variable '${name}' already initialised with a different value`
);
}
this.variables.set(name, init);
}
alias(name: string) {
if (!this.aliases.has(name)) {
this.aliases.set(name, this.get_unique_name(name));
}
return this.aliases.get(name);
}
child(options: BlockOptions) {
return new Block(Object.assign({}, this, { key: null }, options, { parent: this }));
}
get_contents(local_key?: string) {
const { dev } = this.renderer.options;
if (this.has_outros) {
this.add_variable('#current');
if (!this.builders.intro.is_empty()) {
this.builders.intro.add_line(`#current = true;`);
this.builders.mount.add_line(`#current = true;`);
}
if (!this.builders.outro.is_empty()) {
this.builders.outro.add_line(`#current = false;`);
}
}
if (this.autofocus) {
this.builders.mount.add_line(`${this.autofocus}.focus();`);
}
this.render_listeners();
const properties = new CodeBuilder();
const method_name = (short: string, long: string) => dev ? `${short}: function ${this.get_unique_name(long)}` : short;
if (local_key) {
properties.add_block(`key: ${local_key},`);
}
if (this.first) {
properties.add_block(`first: null,`);
this.builders.hydrate.add_line(`this.first = ${this.first};`);
}
if (this.builders.create.is_empty() && this.builders.hydrate.is_empty()) {
properties.add_line(`c: @noop,`);
} else {
const hydrate = !this.builders.hydrate.is_empty() && (
this.renderer.options.hydratable
? `this.h()`
: this.builders.hydrate
);
properties.add_block(deindent`
${method_name('c', 'create')}() {
${this.builders.create}
${hydrate}
},
`);
}
if (this.renderer.options.hydratable || !this.builders.claim.is_empty()) {
if (this.builders.claim.is_empty() && this.builders.hydrate.is_empty()) {
properties.add_line(`l: @noop,`);
} else {
properties.add_block(deindent`
${method_name('l', 'claim')}(nodes) {
${this.builders.claim}
${this.renderer.options.hydratable && !this.builders.hydrate.is_empty() && `this.h();`}
},
`);
}
}
if (this.renderer.options.hydratable && !this.builders.hydrate.is_empty()) {
properties.add_block(deindent`
${method_name('h', 'hydrate')}() {
${this.builders.hydrate}
},
`);
}
if (this.builders.mount.is_empty()) {
properties.add_line(`m: @noop,`);
} else {
properties.add_block(deindent`
${method_name('m', 'mount')}(#target, anchor) {
${this.builders.mount}
},
`);
}
if (this.has_update_method || this.maintain_context) {
if (this.builders.update.is_empty() && !this.maintain_context) {
properties.add_line(`p: @noop,`);
} else {
properties.add_block(deindent`
${method_name('p', 'update')}(changed, ${this.maintain_context ? 'new_ctx' : 'ctx'}) {
${this.maintain_context && `ctx = new_ctx;`}
${this.builders.update}
},
`);
}
}
if (this.has_animation) {
properties.add_block(deindent`
${method_name('r', 'measure')}() {
${this.builders.measure}
},
${method_name('f', 'fix')}() {
${this.builders.fix}
},
${method_name('a', 'animate')}() {
${this.builders.animate}
},
`);
}
if (this.has_intro_method || this.has_outro_method) {
if (this.builders.intro.is_empty()) {
properties.add_line(`i: @noop,`);
} else {
properties.add_block(deindent`
${method_name('i', 'intro')}(#local) {
${this.has_outros && `if (#current) return;`}
${this.builders.intro}
},
`);
}
if (this.builders.outro.is_empty()) {
properties.add_line(`o: @noop,`);
} else {
properties.add_block(deindent`
${method_name('o', 'outro')}(#local) {
${this.builders.outro}
},
`);
}
}
if (this.builders.destroy.is_empty()) {
properties.add_line(`d: @noop`);
} else {
properties.add_block(deindent`
${method_name('d', 'destroy')}(detaching) {
${this.builders.destroy}
}
`);
}
/* eslint-disable @typescript-eslint/indent,indent */
return deindent`
${this.variables.size > 0 &&
`var ${Array.from(this.variables.keys())
.map(key => {
const init = this.variables.get(key);
return init !== undefined ? `${key} = ${init}` : key;
})
.join(', ')};`}
${!this.builders.init.is_empty() && this.builders.init}
return {
${properties}
};
`.replace(/(#+)(\w*)/g, (_match: string, sigil: string, name: string) => {
return sigil === '#' ? this.alias(name) : sigil.slice(1) + name;
});
/* eslint-enable @typescript-eslint/indent,indent */
}
render_listeners(chunk: string = '') {
if (this.event_listeners.length > 0) {
this.add_variable(`#dispose${chunk}`);
if (this.event_listeners.length === 1) {
this.builders.hydrate.add_line(
`#dispose${chunk} = ${this.event_listeners[0]};`
);
this.builders.destroy.add_line(
`#dispose${chunk}();`
);
} else {
this.builders.hydrate.add_block(deindent`
#dispose${chunk} = [
${this.event_listeners.join(',\n')}
];
`);
this.builders.destroy.add_line(
`@run_all(#dispose${chunk});`
);
}
}
}
toString() {
const local_key = this.key && this.get_unique_name('key');
return deindent`
${this.comment && `// ${escape(this.comment, { only_escape_at_symbol: true })}`}
function ${this.name}(${this.key ? `${local_key}, ` : ''}ctx) {
${this.get_contents(local_key)}
}
`;
}
}