🔥 (#142) Slots, state, and loops
Hey there!
It's December, and Christmas is just around the corner.
I've had my tree up for a couple weeks now, and I've been enjoying all the cheesy Christmas movies!
And lots of hot chocolate, too.
As always, I've got some tips for you.
Have a great week!
— Michael
🔥 What are all these loops for?
I always forget this, so this tip is mostly for me — hopefully, I won't have to keep looking this up!
We have 3 types of for
loops in Javascript:
for...in
for...of
for
But how do you know which one to use?
For iterating through properties of an object, use for...in
:
const numbers = { 'one': 1, 'two': 2, 'three': 3, }; // We get the properties of the object, not the values for (const number in numbers) { console.log(number); } // Prints: 'one' 'two' 'three'
Items in a list (also called an iterable object) like an Array or Set, we use for...of
:
const numbers = ['one', 'two', 'three']; // We get each of the elements in the Array for (const number of numbers) { console.log(number); } // Prints: 'one' 'two' 'three'
You can use for...in
with an Array since all the indices are just the object's properties. But you may not get them in the correct order, and you'll also get any other properties the Array has :/
And you know how to use a regular old for
loop, which lets you have a lot more control with some extra typing.
🔥 Conditionally Rendering Slots
First, I'll show you how, then we'll get into why you'd want to hide slots.
Every Vue component has a special $slots
object with all of your slots in it. The default slot has the key default
, and any named slots use their name as the key:
const $slots = { default: <default slot>, icon: <icon slot>, button: <button slot>, };
But this $slots
object only has the slots that are applied to the component, not every slot that is defined.
Take this component that defines several slots, including a couple named ones:
<!-- Slots.vue --> <template> <div> <h2>Here are some slots</h2> <slot /> <slot name="second" /> <slot name="third" /> </div> </template>
If we only apply one slot to the component, only that slot will show up in our $slots
object:
<template> <Slots> <template #second> This will be applied to the second slot. </template> </Slots> </template>
$slots = { second: <vnode> }
We can use this in our components to detect which slots have been applied to the component, for example, by hiding the wrapper element for the slot:
<template> <div> <h2>A wrapped slot</h2> <div v-if="$slots.default" class="styles"> <slot /> </div> </div> </template>
Now the wrapper div
that applies the styling will only be rendered if we actually fill that slot with something.
If we don't use the v-if
, we will have an empty and unnecessary div
if we don't have a slot. Depending on what styling that div
has, this could mess up our layout and make things look weird.
So why do we want to be able to conditionally render slots?
There are three main reasons to use a conditional slot:
- When using wrapper `div's to add default styles
- The slot is empty
- If we're combining default content with nested slots
For example, when we're adding default styles, we're adding a div
around a slot:
<template> <div> <h2>This is a pretty great component, amirite?</h2> <div class="default-styling"> <slot > </div> <button @click="$emit('click')">Click me!</button> </div> </template>
However, if no content is applied to that slot by the parent component, we'll end up with an empty div
rendered to the page:
<div> <h2>This is a pretty great component, amirite?</h2> <div class="default-styling"> <!-- No content in the slot, but this div is still rendered. Oops. --> </div> <button @click="$emit('click')">Click me!</button> </div>
Adding that v-if
on the wrapping div
solves the problem though. No content applied to the slot? No problem:
<div> <h2>This is a pretty great component, amirite?</h2> <button @click="$emit('click')">Click me!</button> </div>
Here's a Codesandbox with a working demo if you want to take a look: https://codesandbox.io/s/reactive-slots-bth28?file=/src/components/HasSlot.vue
I wrote more tips on slots in this article: Tips to Supercharge Your Slots (Named, Scoped, and Dynamic)
🔥 Controlled Props — or How to Override Internal State
Here we have a simple Toggle
component that can show or hide content:
<template> <Toggle title="Toggled Content"> This content can be hidden by clicking on the toggle. </Toggle> </template>
It keeps track of its own open
state internally right now.
But what if we want to override that internal state, but only some of the time?
To do this, we have to dynamically switch between relying on props and events, and relying on the internal state:
export default { name: 'Toggle', props: { title: { type: String, required: true, }, hidden: { type: Boolean, // Must be set to `undefined` and not `false` default: undefined, } }, data() { return { // Internal state _hidden: false, }; }, methods: { toggleHidden() { // Switch between emitting an event and toggling state if (this.hidden !== undefined) { this.$emit('toggle-hidden'); } else { this._hidden = !this._hidden; } }, }, computed: { $hidden() { // Dynamically switch between state or prop return this.hidden !== undefined ? this.hidden : this._hidden; }, }, };
In the Toggle
component we now have to use the $hidden
computed prop:
<template> <div> <div class="title" @click="toggleHidden" > {{ title }} </div> <slot v-if="$hidden" /> </div> </template>
You can check out a more detailed tutorial on my blog.
📜 3 Ways to Create Inline Composables
Composables are great, except that it seems we always need to create a new file for them.
In this article I explore some ways we can create inline composables — no need to create new files all over the place!
Check it out here: 3 Ways to Create Inline Composables
💬 Mass producing software
"You can mass-produce hardware; you cannot mass-produce software; you cannot mass-produce the human mind." — Michio Kaku
🧠 Spaced-repetition: Restrict a prop to a list of types
The best way to commit something to long-term memory is to periodically review it, gradually increasing the time between reviews 👨🔬
Actually remembering these tips is much more useful than just a quick distraction, so here's a tip from a couple weeks ago to jog your memory.
With the Composition API we get fantastic TypeScript support, so this is quite straightforward:
defineProps<{ src: string; style: 'square' | 'rounded'; }>();
Doing this in the Options API is more complicated, and not as powerful as TypeScript.
Using the validator
option in a prop definition you can restrict a prop to a specific set of values:
export default { name: 'Image', props: { src: { type: String, }, style: { type: String, validator: s => ['square', 'rounded'].includes(s) } } };
This validator function takes in a prop and returns either true
or false
— if the prop is valid or not.
I often restrict props like this when I need more options than a boolean
will allow but still want to restrict what can be set.
Button types or alert types (info, success, danger, warning) are some of the most common uses — at least in what I work on. Colours, too, are a really great use case for this.
p.s. I also have four products/courses: Clean Components Toolkit, Vue Tips Collection, Mastering Nuxt 3, and Reusable Components
评论
发表评论