docs/tutorial/components-and-reactivity.md
[!TLDR]
We build a simple example which introduces tag variables, conditionals, and components
In this tutorial, we're going to build an app for converting temperature between fahrenheit and celsius.
As with many user interfaces, our first step is to gather input from the user. We can do so with HTML's <input> tag:
<input type="number">
Of course, right now we aren't keeping track of the value that this input contains. To do this, we need to introduce state. In Marko, the most common way to do this is with tag variables. Here, we will use Marko's <let> tag:
<let/degF=80>
<input type="number" value=degF>
<div>It's ${degF}°F</div>
Now the <input> has an initial value, but we still aren't keeping track of it when it changes. One way you may think to do this is by listening for the input event with an event handler:
// Warning: There's a better way to do this!
<let/degF=80>
<input type="number" value=degF onInput(e) {
degF = +e.target.value;
}>
<div>It's ${degF}°F</div>
This seems to work at first glance, but you'll find out quickly that the value of the input isn't fully synchronized. This is because in HTML, value= actually refers to the default value of the input and not its current value. This is why instead, we should leverage the controllable pattern with Change handlers.
<let/degF=80>
<input type="number" value=degF valueChange(value) { degF = parseFloat(value) }>
<div>It's ${degF}°F</div>
Because this is such a common pattern, Marko provides a shorthand for it!
<let/degF=80>
<input type="number" value:parseFloat:=degF>
<div>It's ${degF}°F</div>
Now we can use the <const> tag to convert to celsius!
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
Since degC is a tag variable, its changes also propagate every time degF is updated.
Now that we have a reactive variable, let's see what else we can do! Maybe some notes about the temperature, using conditional tags?
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
<if=(degF > 90)>
It's <strong>hot</strong> 🥵
</if>
<else if=(degF > 60)>
Lovely day! 😎
</else>
<else if=(degF < 32)>
Brrrrr 🥶
</else>
Or what about a temperature gauge, with some fancy CSS?
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
<div class="gauge">
<div class="needle" style={"--rotation": `${degF * 180 / 100}deg`}/>
</div>
<style>
.gauge {
position: relative;
width: 8rem;
height: 4rem;
border-radius: 4rem 4rem 0 0;
background: conic-gradient(from 270deg at 50% 100%, lightblue, blue, green, orange, red 180deg);
}
.needle {
position: absolute;
box-sizing: border-box;
width: 4rem;
height: 4px;
bottom: -2px;
background: black;
border: 1px solid white;
transform-origin: right;
transform: rotate(var(--rotation));
}
</style>
Actually, this is getting a little bit too complex to all put in one place. Maybe we should pull that temperature gauge out into a component:
/* index.marko */
<let/degF=80>
<const/degC=(degF - 32) * 5 / 9>
<input type="number" value:parseFloat:=degF>
<div>
${degF}°F ↔ ${degC.toFixed(1)}°C
</div>
<gauge temperature=degF/>
/* tags/gauge.marko */
<div class="gauge">
<div class="needle" style={"--rotation": `${input.temperature * 180 / 100}deg`}/>
</div>
<if=(input.temperature > 90)>
It's <strong>hot</strong> 🥵
</if>
<else if=(input.temperature > 60)>
Lovely day! 😎
</else>
<else if=(input.temperature < 32)>
Brrrrr 🥶
</else>
<style>
.gauge {
position: relative;
width: 8rem;
height: 4rem;
border-radius: 4rem 4rem 0 0;
background: conic-gradient(from 270deg at 50% 100%, lightblue, blue, green, orange, red 180deg);
}
.needle {
position: absolute;
box-sizing: border-box;
width: 4rem;
height: 4px;
bottom: -2px;
background: black;
border: 1px solid white;
transform-origin: right;
transform: rotate(var(--rotation));
}
</style>
<!-- markdownlint-disable MD026 allow exclamation point -->[!IMPORTANT] Make sure your
<gauge>component file is in atags/directory! Marko auto-discovers custom tags based on directory structure.
That's all we're going to build for now, but feel free to add more! Here are some ideas: