Read this on my blog Hey! I've only got 17 hardcovers of Vue Tips Collection left. So this is your last chance to grab one if you wanted one! If you're interested, go here to buy and learn more: Vue Tips Collection But if you just want your tips in this newsletter, you can keep reading! — Michael 🔥 Component Metadata Not every bit of info you add to a component is state. For example, sometimes, you need to add metadata that gives other components more information. For example, if you're building a bunch of different widgets for an analytics dashboard like Google Analytics or Stripe. If you want the layout to know how many columns each widget should take up, you can add that directly on the component as metadata: <script setup> defineOptions({ columns: 3, }); </script>
Or if you're using the Options API: export default { name: 'LiveUsersWidget', // Just add it as an extra property columns: 3, props: { // ... }, data() { return { //... }; }, };
You'll find this metadata as a property on the component: import LiveUsersWidget from './LiveUsersWidget.vue'; const { columns } = LiveUsersWidget;
With the Composition API we can't access this value directly, because there's no concept of a "current instance". Instead, we can make our value a constant: <script setup> const columns = 3; defineOptions({ columns, }); </script>
But this value cannot change, because defineOptions is a compiler macro and the value is used at compile time. If you're using the Options API you can access the metadata from within the component through the special $options property: export default { name: 'LiveUsersWidget', columns: 3, created() { // `$options` contains all the metadata for a component console.log(`Using ${this.$options.columns} columns`); }, };
Just keep in mind that this metadata is the same for each component instance and is not reactive. Other uses for this include (but are not limited to): - Keeping version numbers for individual components
- Custom flags for build tools to treat components differently
- Adding custom features to components beyond computed props, data, watchers, etc.
- and many more I can't think of!
I used this technique to build my Totally Unnecessary If/Else Component if you want to see it in action. 🔥 Smooth dragging (and other mouse movements) If you ever need to implement dragging or to move something along with the mouse, here's how you do it: - Always throttle your mouse events using
requestAnimationFrame . Lodash's throttle method with no wait parameter will do this. If you don't throttle, your event will fire faster than the screen can even refresh, and you'll waste CPU cycles and the smoothness of the movement. - Don't use absolute values of the mouse position. Instead, you should check how far the mouse has moved between frames. This is a more reliable and smoother method. If you use absolute values, the element's top-left corner will jump to where the mouse is when you first start dragging. Not a great UX if you grab the element from the middle.
Here's a basic example of tracking mouse movements using the Composition API. I didn't include throttling in order to keep things clearer: // In your setup() function window.addEventListener("mousemove", (e) => { // Only move the element when we're holding down the mouse if (dragging.value) { // Calculate how far the mouse moved since the last // time we checked const diffX = e.clientX - mouseX.value; const diffY = e.clientY - mouseY.value; // Move the element exactly how far the mouse moved x.value += diffX; y.value += diffY; } // Always keep track of where the mouse is mouseX.value = e.clientX; mouseY.value = e.clientY; });
<template> <div class="drag-container"> <img alt="Vue logo" src="./assets/logo.png" :style="{ left: `${x}px`, top: `${y}px`, cursor: dragging ? 'grabbing' : 'grab', }" draggable="false" @mousedown="dragging = true" /> </div> </template>
<script setup> import { ref } from "vue"; const dragging = ref(false); const mouseX = ref(0); const mouseY = ref(0); const x = ref(100); const y = ref(100); window.addEventListener("mousemove", (e) => { if (dragging.value) { const diffX = e.clientX - mouseX.value; const diffY = e.clientY - mouseY.value; x.value += diffX; y.value += diffY; } mouseX.value = e.clientX; mouseY.value = e.clientY; }); window.addEventListener("mouseup", () => { dragging.value = false; }); </script>
🔥 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) 🎙️ #056 — Snapshot Testing and Beyond (with The Jared Wilcurt) In this episode of DejaVue, Alex and Michael are joined by Jared Wilcurt, UI architect and open source contributor, to get knee-deep into the world of testing in Vue.js, especially Snapshot Testing. Jared shares his journey from React frustration to Vue enthusiasm, and explains how he identified gaps in Vue's testing ecosystem that led to the creation of his Vue 3 Snapshot Serializer library. No matter if you are a testing novice, wondering why you should bother with tests at all, or an experienced developer looking to improve your snapshot testing workflow, this episode got something for you - from reducing test noise, improving readability, and gaining confidence in your Vue applications and components. Discover how snapshot testing complements traditional assertion-based tests and why it might be the missing piece in your testing strategy. Watch on YouTube or listen on your favorite podcast platform. Chapters: In case you missed them: 📜 Compressing Images with Vite and VSharp Images are one of the biggest causes of slow webpages. We have a bunch of strategies for dealing with this, but the most basic one is to make sure each and every image in your app is compressed as much as possible. Fortunately for us, setting up Nuxt to automatically compress our images only takes a few minutes. Check it out here: Compressing Images with Vite and VSharp 📜 Async and Sync: How useAsyncData Does It All Have you ever noticed that useAsyncData can be used both sync and async? What's up with that? In this article, I explain how it works, and how you can use this pattern to your advantage. Check it out here: Async and Sync: How useAsyncData Does It All 📅 Upcoming Events Here are some upcoming events you might be interested in. Let me know if I've missed any! VueConf US 2025 — (May 13, 2025 to May 15, 2025) VueConf US 2025 is a great Vue conference, this year held in Tampa from May 13–15, with two days of talks and a day of workshops. Unfortunately, I am no longer able to make it to the conference this year. I hope everyone attending has an amazing time and I look forward to joining in the future! Check it out here MadVue 2025 — (May 29, 2025) It's time to get together in Madrid. Join for a full day of talks, activities, and networking with the Vue.js community and ecosystem. Check it out here 💬 Great Developers "Every great developer you know got there by solving problems they were unqualified to solve until they actually did it." — Patrick McKenzie 🧠 Spaced-repetition: Understanding scoped slots 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. Here's the best way to think about scoped slots: Scoped slots are like functions that are passed to a child component that returns HTML. Once the template is compiled, they are functions that return HTML (technically vnodes ) that the parent passes to the child. Here's a simple list that uses a scoped slot to customize how we render each item: <!-- Parent.vue --> <template> <ScopedSlotList :items="items"> <template v-slot="{ item }"> <!-- Make it bold, just for fun --> <strong></strong> </template> </ScopedSlotList> </template>
<!-- ScopedSlotList.vue --> <template> <ul> <li v-for="item in items" :key="item" > <slot :item="item" /> </li> </ul> </template>
We can rewrite this example to use a function instead of a scoped slot: <!-- Parent.vue --> <template> <ScopedSlotList :items="items" :scoped-slot="(item) => `<strong>${item}</strong>`" > </template>
<!-- ScopedSlotList.vue --> <template> <ul> <li v-for="item in items" :key="item" v-html="scopedSlot(item)" /> </ul> </template>
🔗 Want more Vue and Nuxt links? Michael Hoffman curates a fantastic weekly newsletter with the best Vue and Nuxt links. Sign up for it here. p.s. I also have a bunch of products/courses: |
评论
发表评论