Appearance
🔥 (#66) Multiple v-models (and 4 bonus tips!)
June 2022
Hey there!
You probably know by now that this week is the launch of the Vue Tips Collection book!

So I won't say too much more about it.
It's 30% off until the end of the week, so don't wait too long to decide if you want it or not.
To celebrate the launch, I have put four extra tips into this newsletter. It'll give you a small taste of what the book is like.
Enjoy, and have a wonderful week!
—Michael
🔥 Multiple v-models
In Vue 3 we're not limited to a single v-model:
<AddressForm
v-model:street-name="streetName"
v-model:street-number="streetNumber"
v-model:postal-code="postalCode"
v-model:province="province"
v-model:country="country"
/>
This makes dealing with complex forms a lot easier!
First, we need to create the props and events for v-model to hook into (I've omitted a couple v-models for simplicity):
<!-- AddressForm.vue -->
<script setup>
// Set up all the props
defineProps({
streetName: {
type: String,
required: true,
},
streetNumber: {
type: Number,
required: true,
},
// ...
country: {
type: String,
required: true,
},
});
// Set up our events
defineEmits([
"update:streetName",
"update:streetNumber",
// ...
"update:country",
]);
</script>
Then, inside the component we use the prop to read the value, and emit update:<propname> to update it:
<template>
<form>
<input
type="text"
:value="streetName"
@input="$emit('update:streetName', $event.target.value)"
/>
<input
type="text"
:value="streetNumber"
@input="$emit('update:streetNumber', $event.target.value)"
/>
<!-- ... -->
<input
type="text"
:value="country"
@input="$emit('update:country', $event.target.value)"
/>
</form>
</template>
You can read more about using multiple v-models here.
🔥 How I deal with dynamic classes
A pattern I use constantly is triggering classes with boolean flags:
<template>
<div :class="disabled && 'disabled-component'">
Sometimes this component is disabled. Other times it isn't.
</div>
</template>
/* Some styles */
.disabled-component {
background-color: gray;
color: darkgray;
cursor: not-allowed;
}
Either the trigger is a prop I use directly, or a computed prop that tests for a specific condition:
disabled() {
return this.isDisabled || this.isLoading;
}
If I just need one class on an element, I use the logical AND to trigger it:
<div :class="disabled && 'disabled-component'"></div>
Sometimes it's a decision between two classes, so I'll use a ternary:
<div :class="disabled ? 'disabled-component' : 'not-yet-disabled'" />
I don't often use more than two classes like this, but that's where an Object or Array comes in handy:
<div
:class="{
primary: isPrimary,
secondary: isSecondary,
tertiary: isTertiary,
}"
/>
<div
:class="[
isPrimary && 'primary',
isSecondary && 'secondary',
isTertiary && 'tertiary',
]"
/>
Of course, when it gets complex enough it's better to just have a computed prop that returns a string of class names (or returns an Object or Array):
<div :class="computedClasses" />
🔥 Dynamic Slot Names
We can dynamically generate slots at runtime, giving us even more flexibility in how we write our components:
<!-- Child.vue -->
<template>
<div v-for="step in steps" :key="step.id">
<slot :name="step.name" />
</div>
</template>
Each of these slots works like any other named slot. This is how we would provide content to them:
<!-- Parent.vue -->
<template>
<Child :steps="steps">
<!-- Use a v-for on the template to provide content
to every single slot -->
<template v-for="step in steps" v-slot:[step.name]>
<!-- Slot content in here -->
</template>
</Child>
</template>
We pass all of our steps to the Child component so it can generate the slots. Then we use a dynamic directive argument v-slot:[step.name] inside a v-for to provide all of the slot content.
When might you need something like this?
I can imagine one use case for a complex form generated dynamically. Or a wizard with multiple steps, where each step is a unique component.
I'm sure there are more!
🔥 How to make a variable created outside of Vue reactive
If you get a variable from outside of Vue, it's nice to be able to make it reactive.
That way, you can use it in computed props, watchers, and everywhere else, and it works just like any other state in Vue.
If you're using the options API, all you need is to put it in the data section of your component:
const externalVariable = getValue();
export default {
data() {
return {
reactiveVariable: externalVariable,
};
},
};
If you're using the composition API with Vue 3, you can use ref or reactive directly:
import { ref } from "vue";
// Can be done entirely outside of a Vue component
const externalVariable = getValue();
const reactiveVariable = ref(externalVariable);
// Access using .value
console.log(reactiveVariable.value);
Using reactive instead:
import { reactive } from "vue";
// Can be done entirely outside of a Vue component
const externalVariable = getValue();
// Reactive only works with objects and arrays
const anotherReactiveVariable = reactive(externalVariable);
// Access directly
console.log(anotherReactiveVariable);
If you're still on Vue 2 (as many of us are), you can use observable instead of reactive to achieve precisely the same result.
🔥 Default Content with Slots
You can provide fallback content for a slot, in case no content is provided:
<!-- Child.vue -->
<template>
<div>
<slot> Hey! You forgot to put something in the slot! </slot>
</div>
</template>
This content can be anything, even a whole complex component that provides default behaviour:
<!-- Child.vue -->
<template>
<div>
<slot name="search">
<!-- Can be overridden with more advanced functionality -->
<BasicSearchFunctionality />
</slot>
</div>
</template>
🔥 Vue 中特殊的 CSS 伪类选择器
如你想给 slot 内容添加额定的样式,你可以使用 :slotted 这个伪类选择器:
<style scoped>
/* 给传入的 slot <p> 标签添加 margin */
:slotted(p) {
margin: 15px 5px;
}
</style>
也可以使用 :global 来应用全局 scope 样式,在 <style scoped> 中也可以使用:
<style scoped>
:global(body) {
margin: 0;
padding: 0;
font-family: sans-serif;
}
</style>
如果有很多全局样式想添加,更简单的方式是添加第二个 <style> 标签:
<style scoped>
/* Add margin to <p> tags within the slot */
:slotted(p) {
margin: 15px 5px;
}
</style>
<style>
body {
margin: 0;
padding: 0;
font-family: sans-serif;
}
</style>
阅读 文档 了解更多。
📜 Rewriting Nuxt HN with Fastify, Vite and Vue 3
Nuxt is a fantastic framework, but even the best frameworks aren't perfect.
It this article, Jonas uses his experience as a consultant to explore the dark side of Nuxt, and how to refactor the Nuxt Hacker News clone to use Fastify and Vite to eliminate some of those dark spots.
Read it here: Rewriting Nuxt HN with Fastify, Vite and Vue 3
💬 Eraser vs. sledgehammer
"You can use an eraser on the drafting table or a sledgehammer on the construction site." —Frank Lloyd Wright
🧠 Spaced-repetition: Smooth dragging (and other mouse movements)
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.
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'sthrottlemethod with nowaitparameter 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;
});
Here's the full example. You can check out a working demo here:
<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>
import { ref } from "vue";
export default {
setup() {
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;
});
return {
x,
y,
dragging,
};
},
};
</script>
p.s. I also have two courses: Reusable Components and Clean Components
来源
原文 https://michaelnthiessen.com/weekly-066-june-22/
本作品采用知识共享署名-相同方式共享 4.0 国际许可协议进行许可。