Skip to content

feat: Alternative implementation for #if #14551

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

Closed
wants to merge 5 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
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
Original file line number Diff line number Diff line change
@@ -1,55 +1,54 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { ArrowFunctionExpression, BlockStatement, Expression, IfStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types' */
import * as b from '../../../../utils/builders.js';

/**
* @param {AST.IfBlock} node
* @param {ComponentContext} context
* @param {number} index
*/
function create_if(node, context, index) {
const test = /** @type {Expression} */ (context.visit(node.test));
const nb = b.literal(index);
const fn = b.arrow(
[b.id('$$anchor')],
/** @type {BlockStatement} */ (context.visit(node.consequent))
);

return b.if(test, b.block([b.return(b.array([nb, fn]))]));
}

/**
* @param {AST.IfBlock} node
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
context.state.template.push('<!>');

const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
let index = 0;

const args = [
context.state.node,
b.thunk(/** @type {Expression} */ (context.visit(node.test))),
b.arrow([b.id('$$anchor')], consequent)
];
/** @type {IfStatement} */
let if_statement = create_if(node, context, index++);

if (node.alternate || node.elseif) {
args.push(
node.alternate
? b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(node.alternate)))
: b.literal(null)
);
}
context.state.init.push(
b.stmt(b.call('$.choose', context.state.node, b.arrow([], b.block([if_statement]))))
);

if (node.elseif) {
// We treat this...
//
// {#if x}
// ...
// {:else}
// {#if y}
// <div transition:foo>...</div>
// {/if}
// {/if}
//
// ...slightly differently to this...
//
// {#if x}
// ...
// {:else if y}
// <div transition:foo>...</div>
// {/if}
//
// ...even though they're logically equivalent. In the first case, the
// transition will only play when `y` changes, but in the second it
// should play when `x` or `y` change — both are considered 'local'
args.push(b.literal(true));
let alt = node.alternate;
while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
if_statement = if_statement.alternate = create_if(elseif, context, index++);
alt = elseif.alternate;
}
if (alt) {
if_statement.alternate = b.block([
b.return(
b.array([
b.literal(index++),
b.arrow([b.id('$$anchor')], /** @type {BlockStatement} */ (context.visit(alt)))
])
)
]);
}

context.state.init.push(b.stmt(b.call('$.if', ...args)));
}
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { BlockStatement, Expression } from 'estree' */
/** @import { BlockStatement, Expression, IfStatement } from 'estree' */
/** @import { AST } from '#compiler' */
/** @import { ComponentContext } from '../types.js' */
import { BLOCK_OPEN_ELSE } from '../../../../../internal/server/hydration.js';
Expand All @@ -8,21 +8,52 @@ import { block_close, block_open } from './shared/utils.js';
/**
* @param {AST.IfBlock} node
* @param {ComponentContext} context
* @param {number} index
*/
export function IfBlock(node, context) {
function create_if(node, context, index) {
const test = /** @type {Expression} */ (context.visit(node.test));

const consequent = /** @type {BlockStatement} */ (context.visit(node.consequent));
consequent.body.unshift(
b.stmt(
b.assignment(
'+=',
b.id('$$payload.out'),
index === 0 ? block_open : b.literal(`<!--[${index}-->`)
)
)
);
return b.if(test, consequent);
}

/**
* @param {AST.IfBlock} node
* @param {ComponentContext} context
*/
export function IfBlock(node, context) {
let index = 0;

const alternate = node.alternate
? /** @type {BlockStatement} */ (context.visit(node.alternate))
: b.block([]);
/** @type {IfStatement} */
let if_statement = create_if(node, context, index++);

consequent.body.unshift(b.stmt(b.assignment('+=', b.id('$$payload.out'), block_open)));
context.state.template.push(if_statement, block_close);

alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(BLOCK_OPEN_ELSE)))
);
let alt = node.alternate;
while (alt && alt.nodes.length === 1 && alt.nodes[0].type === 'IfBlock' && alt.nodes[0].elseif) {
const elseif = alt.nodes[0];
if_statement = if_statement.alternate = create_if(elseif, context, index++);
alt = elseif.alternate;
}

context.state.template.push(b.if(test, consequent, alternate), block_close);
/** @type {string} */
let else_key;
if (alt) {
if_statement.alternate = /** @type {BlockStatement} */ (context.visit(alt));
else_key = `<!--[${index}-->`;
} else {
if_statement.alternate = b.block([]);
else_key = BLOCK_OPEN_ELSE;
}
if_statement.alternate.body.unshift(
b.stmt(b.assignment('+=', b.id('$$payload.out'), b.literal(else_key)))
);
}
78 changes: 77 additions & 1 deletion packages/svelte/src/internal/client/dom/blocks/if.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
/** @import { Effect, TemplateNode } from '#client' */
/** @import { Derived, Effect, TemplateNode } from '#client' */
import { EFFECT_TRANSPARENT } from '../../constants.js';
import {
hydrate_next,
Expand All @@ -11,6 +11,82 @@ import {
import { block, branch, pause_effect, resume_effect } from '../../reactivity/effects.js';
import { HYDRATION_START_ELSE } from '../../../../constants.js';

/**
* @param {TemplateNode} node
* @param {() => [number, (anchor: Node) => void] | undefined} get_fn
* @returns {void}
*/
export function choose(node, get_fn) {
if (hydrating) {
hydrate_next();
}

var anchor = node;

/** @type {Array<Effect|undefined>} */
const effects = [];

/** @type {number | null} */
var current_index = null;

block(() => {
const [new_index, new_fn] = get_fn() ?? [-1, null];

if (current_index === new_index) return;

const old_index = current_index;
current_index = new_index;

/** Whether or not there was a hydration mismatch. Needs to be a `let` or else it isn't treeshaken out */
let mismatch = false;

if (hydrating) {
const data = new_index < 0 ? '[!' : new_index === 0 ? '[' : '[' + new_index;
if (data !== /** @type {Comment} */ (anchor).data) {
// Hydration mismatch: remove everything inside the anchor and start fresh.
// This could happen with `{#if browser}...{/if}`, for example
anchor = remove_nodes();

set_hydrate_node(anchor);
set_hydrating(false);
mismatch = true;
}
}

if (effects[new_index]) {
resume_effect(effects[new_index]);
} else if (new_fn) {
let target_anchor = anchor;
if (old_index != -1) {
// search the first node after the current condition :
const len = effects.length;
for (let i = new_index + 1; i < len; i++) {
const effect = effects[i];
if (effect && effect.nodes_start) {
target_anchor = effect.nodes_start;
break;
}
}
}
effects[new_index] = branch(() => new_fn(target_anchor));
}

if (old_index != null && old_index >= 0 && effects[old_index]) {
pause_effect(effects[old_index], () => {
delete effects[old_index];
});
}
if (mismatch) {
// continue in hydration mode
set_hydrating(true);
}
});

if (hydrating) {
anchor = hydrate_node;
}
}

/**
* @param {TemplateNode} node
* @param {() => boolean} get_condition
Expand Down
3 changes: 2 additions & 1 deletion packages/svelte/src/internal/client/dom/hydration.js
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,8 @@ export function remove_nodes() {
if (data === HYDRATION_END) {
if (depth === 0) return node;
depth -= 1;
} else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) {
} else if (data.length && data[0] === '[') {
// } else if (data === HYDRATION_START || data === HYDRATION_START_ELSE) {
depth += 1;
}
}
Expand Down
2 changes: 1 addition & 1 deletion packages/svelte/src/internal/client/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ export {
export { check_target, legacy_api } from './dev/legacy.js';
export { inspect } from './dev/inspect.js';
export { await_block as await } from './dom/blocks/await.js';
export { if_block as if } from './dom/blocks/if.js';
export { if_block as if, choose } from './dom/blocks/if.js';
export { key_block as key } from './dom/blocks/key.js';
export { css_props } from './dom/blocks/css-props.js';
export { index, each } from './dom/blocks/each.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ export default test({
return Promise.resolve().then(() => {
raf.tick(0);

const [, div] = target.querySelectorAll('div');
const [div] = target.querySelectorAll('div');
// @ts-ignore
assert.equal(div.foo, 0);

Expand Down
Loading