Skip to content

Tutorial

Before starting, it is recommended that you take the VanJS tutorial 🙂

You can follow the tutorial with this CodePen template, or just read along if you prefer!

First element

Let's build our first Van Element! It will just be a span with inline styles:

js
define("hello-world", () =>
  span({ style: "color:red;font-size:20px" }, "Hello world!")
);
html
My first Van Element: <hello-world></hello-world>
Result My first Van Element: Hello world

Slots

Let's add children to our Van element. We can use the slot for this.

js
const { span, slot } = van.tags;

define("hello-world", () =>
  span({ style: "color:red;font-size:20px" }, slot())
);
html
Cool discovery: <hello-world>the slot</hello-world>
Result Cool discovery: the slot

TIP

Because they are Web components, Van Elements can use the slot tag as a way to inject children HTML elements. Learn more about slots here!

Attributes

It would be nice if we can change color and font-size from outside the Van Element, right?

Meet the first property provided by Van Element: attr(). It takes an attribute name and an optional default value and returns a VanJS State object.

js
define("hello-world", ({ attr }) => {
  const color = attr(
    "color", // name of the attribute
    "red" // default value (optional)
  );
  const size = attr("size", 20);
  return span(
    { style: () => `color:${color.val};font-size:${size.val}` },
    slot()
  );
});
html
<hello-world color="green" size="16px">I can be green </hello-world>
<hello-world color="orange" size="24px">or orange </hello-world>
<hello-world>or red by default</hello-world>
ResultI can be green or orange or default

Isolated styles

There is another way we can style our content instead of inline styles: by using a style tag.

Our Van Element is isolated in the Shadow DOM, so whatever we write in that inner style won't leak out to the rest of the page!

javascript
define("hello-world", ({ attr }) => {
  const color = attr("color", "red");
  const size = attr("size", "20px");
  return [
    // Styles won't leak out!
    style(() => `*{color:${color.val};font-size:${size.val}}`),
    span(slot()),
  ];
});
html
The styles in the normal DOM
<hello-world color="green">or in other Van Elements </hello-world>
<hello-world>won't be affected!</hello-world>
Result The styles in the normal DOM or in other Van Elements won't be affected!

Reactive Van Elements

This tutorial is way too static. Let's add a bit of reactivity.

Something nice about Van Elements is that you can reuse them... inside other Van Elements!

As an example, let's build some handles for our Van Element:

javascript
const RangePicker = (min: number, max: number, value: State<number>) =>
  input({
    type: "range",
    min,
    max,
    value,
    oninput: (e) => (value.val = e.target.value),
  });

define("tutorial-wrapper", () => {
  const color = van.state(0);
  const size = van.state(20);
  return div(
    div("Hue: ", RangePicker(0, 360, color), () => ` ${color.val}deg`),
    div("Size: ", RangePicker(20, 40, size), () => ` ${size.val / 20}em`),
    p(
      van.tags["hello-world"](
        {
          color: () => `hsl(${color.val} 100% 50%)`,
          size: () => `${size.val / 20}em`,
        },
        slot()
      )
    )
  );
});
html
<tutorial-wrapper>Color sample</tutorial-wrapper>
ResultColor sample

Lifecycle

Since em is not very visual, it would be nice to get the computed font-size in pixels. We could use window.getComputedStyle for this! Let's try it:

ts
define("computed-size", ({ attr }) => {
  const color = attr("color", "red");
  const size = attr("size", "20px");
  const dom = slot();
  return [
    style(
      () => `
      * {
        color: ${color.val};
        font-size: ${size.val};
      }
    `
    ),
    span(dom),
    window.getComputedStyle(dom, null).fontSize,
  ];
});
html
<computed-size size="1.5em">1.5em</computed-size><br />
<computed-size size="1.2em" color="orange">1.2em</computed-size>
Result1.5em
1.2em

That doesn't seem to work 🤔 the reason is that slots only get populated after the component has rendered.

For this, there is the mount hook: it registers a function that only runs when the component has mounted:

ts
define("computed-size-fixed", ({ attr, mount }) => {
  const color = attr("color", "red");
  const size = attr("size", "20px");
  const dom = slot();
  const computedFontSize = van.state(""); 
  mount(() => {
    computedFontSize.val = window.getComputedStyle(dom, null).fontSize;
  });
  return [
    style(
      () => `
      * {
        color: ${color.val};
        font-size: ${size.val};
      }
    `
    ),
    span(dom),
    computedFontSize,
  ];
});
html
<computed-size-fixed size="1.5em">1.5em</computed-size-fixed><br />
<computed-size-fixed size="1.2em" color="orange">1.2em</computed-size-fixed>
Result1.5em
1.2em

Now we get the proper font sizes!

Self-reference

There is one last thing we would want to do: we want to make sure our Van Element is used properly!

Currently people can use anything in the slot: plain text, any HTML tags, even script tags 🤔 this might be intended for some components, but here we want to make sure that the only child of our Van Element is:

  • plain text
  • not white space

We can access the reference of the Van Element using $this

ts
define("final-element", ({ attr, mount, $this }) => {
  if ($this.childElementCount || !$this.innerHTML.trim())
    return span({ style: "color:red" }, "ERROR - only text allowed");
  const color = attr("color", "red");
  const size = attr("size", "20px");
  const dom = slot();
  const computedFontSize = van.state("");
  mount(() => {
    computedFontSize.val = window.getComputedStyle(dom, null).fontSize;
  });
  return [
    style(
      () => `
      * {
        color: ${color.val};
        font-size: ${size.val};
      }
    `
    ),
    span(dom),
    computedFontSize,
  ];
});
html
<final-element color="orange" size="1.2em">Correct usage</final-element><br />
<final-element color="orange" size="1.2em"><p>Wrong usage</p></final-element>
ResultCorrect usage

Wrong usage

That's it!

You have reached the end of the tutorial! Now you know basically everything there is to know about Van Elements. You can now freely explore the wonders of the Web Component world... or disable the Shadow DOM if you prefer!