Components and Reactivity

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.

Introducing a Tag

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">
input type="number"
<input type="number">
input type="number"

Adding State

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>
let/degF=80

input type="number" value=degF
div -- It's ${degF}°F
<let/degF=80>

<input type="number" value=degF>
<div>It's ${degF}°F</div>
let/degF=80

input type="number" value=degF
div -- It's ${degF}°F

Syncing State

Now the <input> has an initial value, but we still aren't keeping track of it when it changes. One way to do this is by listening for the input event with an event handler:

<let/degF=80>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>It's ${degF}°F</div>
let/degF=80

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- It's ${degF}°F
<let/degF=80>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>It's ${degF}°F</div>
let/degF=80

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- It's ${degF}°F

Aha! Now we have a reactive variable that keeps track of our value for degrees (in fahrenheit). Let's convert it to celsius!

Note

For more control over the <input> value, we could have used Marko's controllable pattern.

Adding Computed Values

To do this, we can use a <const> tag:

<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>${degF}°F ↔ ${degC.toFixed(1)}°C</div>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C
<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>${degF}°F ↔ ${degC.toFixed(1)}°C</div>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

Using Conditionals

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=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<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>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

if=(degF > 90)
  -- It's${" "}
  strong -- hot
  -- ${" "}🥵
else if=(degF > 60) -- Lovely day! 😎
else if=degF < 32 -- Brrrrr 🥶
<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<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>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

if=(degF > 90)
  -- It's${" "}
  strong -- hot
  -- ${" "}🥵
else if=(degF > 60) -- Lovely day! 😎
else if=degF < 32 -- Brrrrr 🥶

Adding Styles and Visualization

Or what about a temperature gauge, with some fancy CSS?

<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<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>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

div.gauge
  div.needle style={ "--rotation": `${(degF * 180) / 100}deg` }

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));
  }
<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<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>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

div.gauge
  div.needle style={ "--rotation": `${(degF * 180) / 100}deg` }

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));
  }

Creating Reusable Components

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=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>${degF}°F ↔ ${degC.toFixed(1)}°C</div>

<gauge temperature=degF/>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

gauge temperature=degF
<let/degF=80>
<const/degC=(((degF - 32) * 5) / 9)>

<input
  type="number"
  value=degF
  onInput(e) {
    degF = +e.target.value;
  }>
<div>${degF}°F ↔ ${degC.toFixed(1)}°C</div>

<gauge temperature=degF/>
let/degF=80
const/degC=(((degF - 32) * 5) / 9)

input
  ,type="number"
  ,value=degF
  ,onInput(e) {
    degF = +e.target.value;
  }
div -- ${degF}°F ↔ ${degC.toFixed(1)}°C

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>
div.gauge
  div.needle style={ "--rotation": `${(input.temperature * 180) / 100}deg` }

if=(input.temperature > 90)
  -- It's${" "}
  strong -- hot
  -- ${" "}🥵
else if=(input.temperature > 60) -- Lovely day! 😎
else if=input.temperature < 32 -- Brrrrr 🥶

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));
  }
<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>
div.gauge
  div.needle style={ "--rotation": `${(input.temperature * 180) / 100}deg` }

if=(input.temperature > 90)
  -- It's${" "}
  strong -- hot
  -- ${" "}🥵
else if=(input.temperature > 60) -- Lovely day! 😎
else if=input.temperature < 32 -- Brrrrr 🥶

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));
  }

Note

Make sure your <gauge> component file is in a tags/ directory! Marko auto-discovers custom tags based on directory structure.

Your Turn!

That's all we're going to build for now, but feel free to add more! Here are some ideas:

  • How about a new temperature unit? Maybe Kelvin or Delisle?
  • Most of the world actually uses celsius 😅, maybe users should be able pick which unit to start with
  • What about wind chill? Apparently there are standard formulas if "wind velocity" is known
  • Converting between temperatures is cool, but this system could be generalized. What if it converted between weights, volumes, or distances?
  • Anything else! The opportunities are limitless!

Next Steps


Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.