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:
define("hello-world", () =>
span({ style: "color:red;font-size:20px" }, "Hello world!")
);
My first Van Element: <hello-world></hello-world>
Slots
Let's add children to our Van element. We can use the slot
for this.
const { span, slot } = van.tags;
define("hello-world", () =>
span({ style: "color:red;font-size:20px" }, slot())
);
Cool discovery: <hello-world>the slot</hello-world>
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.
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()
);
});
<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>
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!
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()),
];
});
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>
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:
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()
)
)
);
});
<tutorial-wrapper>Color sample</tutorial-wrapper>
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:
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,
];
});
<computed-size size="1.5em">1.5em</computed-size><br />
<computed-size size="1.2em" color="orange">1.2em</computed-size>
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:
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,
];
});
<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>
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
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,
];
});
<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>
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!