Compile Svelte in your head ({#if})
Tan Li Hau
Posted on September 22, 2020
In this article, we are going to cover our first logic block, the if block.
To make sure we are on the same page, let's first explain how if block works.
The {#if}
block
To render content conditionally, you can wrap it with the {#if}
block:
{#if condition}
<div>Conditionally rendered content</div>
{/if}
If the condition
is truthy, you will see the <div>Conditionally rendered content</div>
, otherwise you will see nothing.
Like JavaScript, you can use else
and else if
in Svelte to test multiple conditions too:
{#if condition_a}
<div>Rendered due to condition_a</div>
{:else if condition_b}
<div>Rendered due to condition_b</div>
{:else}
<div>Otherwise</div>
{/if}
You can visit Svelte's interactive tutorial to learn more about the {#if}
logic block.
The Vanilla JS
So how do we implement an {#if}
logic without any framework?
As mentioned in the Part 1 of the series, we've seen how we can create elements without framework help.
Implementating the if block
Implementing an {#if}
logic block can be as follow:
function createElementsIfConditionA() {
// code to create `<div>Rendered due to condition_a</div>`
}
function createElementsIfConditionB() {
// code to create `<div>Rendered due to condition_b</div>`
}
function createElementsElse() {
// code to create `<div>Otherwise</div>`
}
function createIfBlock() {
if (condition_a) {
createElementsIfConditionA();
} else if (condition_b) {
createElementsIfConditionB();
} else {
createElementsElse();
}
}
The condition_a
and condition_b
could be dynamic, which means if the condition changed, we may need to call createIfBlock
again.
But before that, we need to remove the elements that we created previously. This depends on which conditions were met previously, and which elements were created previously.
So, let's store that information in a variable:
function destroyElementsIfConditionA() {
// code to destroy `<div>Rendered due to condition_a</div>`
}
function destroyElementsIfConditionB() {
// code to destroy `<div>Rendered due to condition_b</div>`
}
function destroyElementsElse() {
// code to destroy `<div>Otherwise</div>`
}
let previousDestroy;
function getPreviousDestroy() {
if (condition_a) {
previousDestroy = destroyElementsIfConditionA;
} else if (condition_b) {
previousDestroy = destroyElementsIfConditionB;
} else {
previousDestroy = destroyElementsElse;
}
}
function createIfBlock() {
// ...
getPreviousDestroy();
}
So, if conditions changed, we destroy the previously created elements, and create a new one:
function updateIfBlock() {
// if `condition_a` or `condition_b` changed
if (conditionChanged) {
previousDestroy();
createIfBlock();
}
}
However, if the condition does not change, but the content within the if block changes, for example, value_a
, value_b
or value_else
change in the following code:
{#if condition_a}
<div>{ value_a }</div>
{:else if condition_b}
<div>{ value_b }</div>
{:else}
<div>{ value_else }</div>
{/if}
Then we need to know how to update the elements as well:
function updateElementsIfConditionA() {
// code to update `<div>{ value_a }</div>`
}
function updateElementsIfConditionB() {
// code to update `<div>{ value_b }</div>`
}
function updateElementsElse() {
// code to update `<div>{ value_else }</div>`
}
function updateIfBlock() {
// if `condition_a` or `condition_b` changed
if (conditionChanged) {
previousDestroy();
createIfBlock();
} else {
if (condition_a) {
updateElementsIfConditionA();
} else if (condition_b) {
updateElementsIfConditionB();
} else {
updateElementsElse();
}
}
}
Finally to destroy the elements if we want to unmount the whole {#if}
block, we can use previousDestroy
, since it will be based on the conditions that the elements were created with:
function destroyIfBlock() {
previousDestroy();
}
Here we have createIfBlock
, updateIfBlock
and destroyIfBlock
. It looks unwieldy, as the if (condition)
logic is scattered across createIfBlock
, getPreviousDestroy
and updateIfBlock
.
So, let's refactor this. Let's shift code around to make it cleaner. ✨
Refactor the code
For each of the logic branch, we have functions to create, update and destroy its elements. For the first condition branch, we have:
createElementsIfConditionA
updateElementsIfConditionA
destroyElementsIfConditionA
It seems like we can employ some sort of Strategy Pattern over here.
We can group the operations for each condition branch together, where each operation has the same interface, { create(){}, update(){}, destroy(){} }
:
const operationConditionA = {
create: createElementsIfConditionA,
update: updateElementsIfConditionA,
destroy: destroyElementsIfConditionA,
};
const operationConditionB = {
create: createElementsIfConditionB,
update: updateElementsIfConditionB,
destroy: destroyElementsIfConditionB,
};
const operationConditionElse = {
create: createElementsElse,
update: updateElementsElse,
destroy: destroyElementsElse,
};
Now, we choose the operation based on the condition, since they have the same interface, they should be able to be used interchangeably:
function getOperation() {
if (condition_a) {
return operationConditionA;
} else if (condition_b) {
return operationConditionB;
} else {
return operationConditionElse;
}
}
Here, we can rewrite our createIfBlock
, updateIfBlock
and destroyIfBlock
:
let currentOperation = getOperation();
function createIfBlock() {
currentOperation.create();
}
function updateIfBlock() {
const previousOperation = currentOperation;
currentOperation = getOperation();
// if (conditionChanged)
if (currentOperation !== previousOperation) {
previousOperation.destroy();
currentOperation.create();
} else {
currentOperation.update();
}
}
function destroyIfBlock() {
currentOperation.destroy();
}
To determine whether the condition changed, we can compute the operation and compare it with the previous operation to see if it has changed.
The Compiled JS
Now let's take look at how Svelte compiles {#if}
into output JavaScript.
<script>
let loggedIn = false;
function toggle() {
loggedIn = !loggedIn;
}
</script>
{#if loggedIn}
<button on:click={toggle}>
Log out
</button>
{:else}
<button on:click={toggle}>
Log in
</button>
{/if}
The output code:
/* App.svelte generated by Svelte v3.25.1 */
// ...
function create_else_block(ctx) {
// ...
return {
c() { /* ... */ },
m(target, anchor) { /* ... */ },
p: noop,
d(detaching) { /* ... */ },
};
}
// (9:0) {#if loggedIn}
function create_if_block(ctx) {
// ...
return {
c() { /* ... */ },
m(target, anchor) { /* ... */ },
p: noop,
d(detaching) { /* ... */ },
};
}
function create_fragment(ctx) {
// ...
function select_block_type(ctx, dirty) {
if (/*loggedIn*/ ctx[0]) return create_if_block;
return create_else_block;
}
let current_block_type = select_block_type(ctx, -1);
let if_block = current_block_type(ctx);
return {
c() {
if_block.c();
if_block_anchor = empty();
},
m(target, anchor) {
if_block.m(target, anchor);
insert(target, if_block_anchor, anchor);
},
p(ctx, [dirty]) {
if (current_block_type === (current_block_type = select_block_type(ctx, dirty)) && if_block) {
if_block.p(ctx, dirty);
} else {
if_block.d(1);
if_block = current_block_type(ctx);
if (if_block) {
if_block.c();
if_block.m(if_block_anchor.parentNode, if_block_anchor);
}
}
},
i: noop,
o: noop,
d(detaching) {
if_block.d(detaching);
if (detaching) detach(if_block_anchor);
},
};
}
Some observations:
Observation 1: If you compare the Svelte's compiled output and the JS code we came out earlier, you may see some resemblance:
- For each logic branch, we have a
create_fragment
function, which in this case iscreate_else_block
andcreate_if_block
. As explain in the previous article, these functions return an instruction manual on how to build the DOM fragment for each logic branch.
This is similar to the operations we discussed earlier, eg: operationConditionA
, operationConditionB
and operationConditionElse
.
- To determine which
create_fragment
function to use, we have theselect_block_type
function.
This is similar to the getOperation
we discussed earlier.
- We then initialise the fragment for the current condition branch,
let current_block_type = select_block_type(ctx, -1);
let if_block = current_block_type(ctx);
- Now we can:
- create
if_block.c()
- mount
if_block.m(target, anchor)
- update
if_block.p(ctx, dirty)
- destroy
if_block.d(detaching)
- create
elements for the {#if}
block.
- In the
p
(u_p_date) method, we check if thecurrent_block_type
has changed, if not, then we callif_block.p(ctx, dirty)
to update as necessary.
If there's change, then we destroy if_block.d(1)
the previous elements, create a new fragment based on the current_block_type
, then create and mount the elements via if_block.c()
and if_block.m(...)
.
This is similar to how we call previousOperation.destroy()
and currentOperation.create()
or currentOperation.update()
.
Observation 2: There's a if_block_anchor
inserted after the if_block
if_block_anchor = empty()
empty()
creates an empty text node.
// https://github.com/sveltejs/svelte/blob/v3.25.1/src/runtime/internal/dom.ts#L56-L58
export function empty() {
return text('');
}
The if_block_anchor
is then used when mounting the if_block
in the u_p_date method.
if_block.m(if_block_anchor.parentNode, if_block_anchor)
So what is this extra empty text node for?
The extra text node
When we update the {#if}
block and notice that we need to change the fragment block type, we need to destroy the elements created previously, and insert newly created elements.
When we insert the new elements, we need to know where to insert them. The insertBefore
API allow us to specify which node the elements should be inserted before. So now it begs the question, which node?
The answer depends on the position {#if}
block is written in the component. There are 4 possible scenarios:
1. There's an element right after the {#if}
block
{#if condition}
<div />
{/if}
<span />
You'll see that
- Svelte does not create the extra text node
- Instead, Svelte uses the
<span />
node instead
if_block.m(span.parentNode, span)
When the
{#if}
condition changes,{#if}
block will replace and insert new elements before the<span />
element.
2. {#if}
block is the last child, {#if}
block has a parent
<div>
{#if condition}
<div />
{/if}
</div>
You'll see that
- Svelte does not create the extra text node
- Instead, Svelte inserts the
{#if}
block into the parent node,<div />
and insert beforenull
. (If you passnull
toinsertBefore
, it will append the element as the last child)
if_block.m(div, null);
When the
{#if}
condition changes,{#if}
block will replace and insert new elements as the last children of the parent<div />
element.
3. {#if}
block is the last child, {#if}
block does not have a parent
{#if condition}
<div />
{/if}
You'll see that
- Svelte creates an extra
anchor
element - The
anchor
element is inserted after the{#if}
block. - Subsequently in the u_p_date function, Svelte insert
{#if}
block before theanchor
element.
if_block.m(if_block_anchor.parentNode, if_block_anchor);
When the
{#if}
condition changes,{#if}
block will replace and insert new elements before theanchor
element.
But why?
This is because a Svelte component can be used in anywhere.
Let's take a look at the scenario below:
<!-- A.svelte -->
{#if condition}
<div id="a" />
{/if}
<!-- B.svelte -->
<div id="b" />
<!-- App.svelte -->
<script>
import A from './A.svelte';
import B from './B.svelte';
</script>
<div id="parent">
<A />
<B />
</div>
In the A.svelte
, the {#if}
block is the last child, it does not have any sibling elements after it.
Let's first assume we don't have the anchor
element. When the condition
changes from false
to true
, Svelte will have to insert the new element <div id="a">
into its parent. And because there's no next element after {#if}
block, and no anchor
element, we will have to insert before null
. In which, the <div id="a" />
will be inserted as the last child of the parent element, <div id="parent">
. And hey, we got ourselves a bug! Elements inside <A />
appears after <B />
!
<div id="parent">
<div id="b"></div>
<div id="a"></div> <!-- newly inserted element -->
</div>
We can prevent this from happening by adding an anchor
element.
When the condition
is false
, our DOM looks like this:
<div id="parent">
<#text /> <!-- an empty text node, not visible to the user -->
<div id="b"></div>
</div>
And when the condition
turns true
, we insert <div id="a" />
before the anchor
element:
<div id="parent">
<div id="a"></div> <!-- newly inserted element -->
<#text /> <!-- an empty text node, not visible to the user -->
<div id="b"></div>
</div>
Yay, we maintain the order of <A />
and <B />
🎉 !
The anchor
element to the {#if}
block, is like an anchor to a ship, "Here is where {#if}
block should insertBefore()
!"
4. {#if}
block followed by another logic block
The final scenario. {#if}
block followed by another logic block:
{#if condition}
<div id="a" />
{/if}
{#if condition2}
<div id="b" />
{/if}
The 2nd {#if}
block condition could be true
or false
. Which means <div id="b" />
could be there or not there.
So, to know where we should insert <div id="a" />
when chaging the condition
, we need an anchor
element after the 1st {#if}
block, before the 2nd {#if}
block.
Closing Note
We've covered how Svelte compiles an {#if}
block, as well as how and why an anchor
element is needed for the {#if}
block.
If you wish to learn more about Svelte, follow me on Twitter.
I'll post it on Twitter when the next part is ready, the next post will be about {#each}
logic block.
Posted on September 22, 2020
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.