In a functional component, how can I perform a pure transformation of props before rendering them? - vue.js

I'm quite new to Vue, and a problem I'm facing right now is defining a functional Vue component that represents an individual grid tile.
This component renders as a single div element. It takes two props representing its x/y position in the grid and needs to use these two props to figure out the CSS classes to apply to the div. Essentially, I just need a way to run these props through a pure function that produces a classObject which I can then v-bind to the class attribute of the div.
Below is the template and component logic I've defined in GridSquare.vue:
<template functional>
<div :class="classObject(props)"></div>
</template>
<script lang="ts">
import Vue from "vue";
import Component from "vue-class-component";
<script lang="ts">
export default {
props: {
xIndex: Number,
yIndex: Number
},
methods: {
classObject(props) {
const { xIndex, yIndex } = props;
return {
normal: true,
"thick-left": xIndex % 10 === 0,
"thick-top": yIndex % 10 === 0,
"thick-right": (xIndex + 1) % 10 === 0,
"thick-bottom": (yIndex + 1) % 10 === 0
};
}
}
};
</script>
However, this doesn't work: all I see is blank space where the div elements should be, and errors in my console like this:
vue.runtime.esm.js?ff9b:1737 TypeError: _vm.classObject is not a function
To summarise, I'm just looking for a way to process props into a different object and then making use of that in my template.

This is not possible right now. You can track this related issue.
You'll have to write the render function manually.

Related

Vue3 (Vite) directly access parent data from child component

I have a simple landing page using Vue3 Vite (SSG) without Vuex.
I need to pass a screenWidth value being watched in App.vue to a bunch of child components so that they change images depending on the user's screenWidth.
I could use props to pass this value, but it seems a bit cumbersome to write them for 8 child components, and to use composition data export or provide / inject is definitely overkill.
is there not a way to simply access a parent's data via something like instance.parent (didn't work), $parent.message (Vue2 way), etc from a child component?
// Parent:
data() {
return {
screenWidth: 123
}
}
// Child
<div v-if="$parent.screenWidth > 1200">
img...
</div>
EDIT: Solving this with props for now as no other (working) solution seems to be available in Vite for what used to be easy as pie in Vue2.
EDIT 2: It occurs to me now that using VueUse's built in useWindowSize might have been a good solution here.
Use v-model binding.
Parent component (assuming setup script):
<script setup lang='ts'>
import {ref} from 'vue';
const screenWidth = ref(720);
// use screenWidth as a regulat reactive variable here
</script>
<template>
<Child v-model="screenWidth" />
</template>
Child component:
<script setup lang="ts">
import {ref, watchEffect} from 'vue';
const props = defineProps<{
modelValue: number;
}>();
const emit = defineEmits<{
(e: 'update:modelValue', value: number): void
}>();
const value = ref(props.modelValue);
watchEffect(() => value.value = props.modelValue);
function setValue(newValue: number) {
value.value = key;
emit('update:modelValue', value.value);
}
</script>
<template>
// Use `value` and `setValue` here
</template>

Vue does not get template ref of vNode-rendered elements array with v-for

I'm creating a library and I do renderless components that just renders whatever is in slots. For the purpose of demonstration, I added dummy VNodes array instead.
<template>
<component
v-for="vnode of vnodes"
:key="vnode.toString()"
:is="vnode"
ref="elements"
/>
</template>
<script setup>
import { ref, h, onMounted } from "vue";
const elements = ref([]);
const vnodes = [
h(
"div",
{
class: "foo"
},
"Hello One!"
),
h(
"div",
{
class: "bar"
},
"Hello Two!"
)
];
onMounted(() => {
console.log(elements.value[0]);
// expected: First element of array (Element)
// reality: undefined
});
</script>
CodePen repro
In my use case, I fetch VNodes from slots and then rendering it via <component> in template. It renders properly, however I can't get the references to rendered elements at onMounted(). Does anyone know what's wrong with this approach, or is it a Vue bug?
I have tried it in codepen and I also think this is how you should do it in vue 3. (best practice as of Vue.js version 3)
Functional Template Refs
:ref="(el) => elements.push(el)"
instead of
ref="elements"
If you face issues with duplicates in the array you might have to reset the array in a vue lifecycle hooks

Vue3 Composition API Reusable reactive values unique to calling component

Running Vue 3.2.6 and Vite 2.5.1
I've been experimenting a bit with the new Composition API and trying to figure out some common usecases where it makes sense to use it in favor of the OptionsAPI. One good usecase I immediately identified would be in Modals, the little popups that occur with a warning message or dialogue or whatever else.
In my old Apps, I'd have to create the modal opening logic in every single component where the modal is being called, which lead to a lot of repetition. With the CompAPI, I tried extracting the logic into a simple modal.ts file that exports 2 things, a reactive openModal boolean, and a toggleModal function. It works great! Until I have more than one modal in my app, that is, in which case it'll open every single Modal at once, on top of one another.
As an example setup
modal.ts
import { ref } from "vue";
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
export { openModal, toggleModal };
App.vue
<template>
<Example1 />
<Example2 />
<Example3 />
</template>
Modal.vue
<template>
<div class="modal" #click.self.stop="sendClose">
<slot />
</div>
</template>
<script setup>
const emit = defineEmits(["closeModal"]);
const sendClose = () => {
emit("closeModal");
};
</script>
Example#.vue
Note that each of these are separate components that have the same layout, the only difference being the number
<template>
<h1>Example 1 <span #click="toggleModal">Toggle</span></h1>
<teleport to="body">
<Modal v-if="openModal" #closeModal="toggleModal">
<h1>Modal 1</h1>
</Modal>
</teleport>
</template>
<script setup>
import { openModal, toggleModal } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
</script>
What happens when clicking the toggle span is obvious (in hindsight). It toggles the value of openModal, which will open all 3 modals at once, one on top of the other. The issue is even worse if you try to implement nested Modals, aka logic in one modal that will open up another modal on top of that one.
Am I misunderstanding how to use ref here? Is it even possible for each component to have and keep track of its own version of openModal? Cause the way I've set it up here, it's acting more like a global store, which isn't great for this particular usecase.
The way I imagined this working is that each component would import the reactive openModal value, and keep track of it independently. That way, when one component calls toggleModal, it would only toggle the value inside of the component calling the function.
Is there a way of doing what I originally intended via the Composition API? I feel like the answer is simple but I can't really figure it out.
That is because you are not exporting your composition correctly, resulting in a shared state, since you are exporting the same function and ref to all components. To fix your issue, you should wrap whatever you're exporting in modal.ts in a function, say:
// Wrap in an exported function (you can also do a default export if you want)
export function modalComposition() {
const openModal = ref(false);
const toggleModal = () => {
openModal.value = !openModal.value;
};
return { openModal, toggleModal };
}
And in each component that you plan to use the composition, simply import it, e.g.:
import { modalComposition } from "#/shared/modal";
import Modal from "#/components/Modal.vue";
// By invoking `modalComposition()`, you are no longer passing by reference
// And therefore there is no "shared state"
const { openModal, toggleModal } = modalComposition();
Why does this work?
When you export a function and then invoke it in the setup of every single component, you are ensuring that each component is setup by executing the function, which returns a new ref for every single instance.

Vue render function: include slots to child component without wrapper

I'm trying to make a functional component that renders a component or another depending on a prop.
One of the output has to be a <v-select> component, and I want to pass it down all its slots / props, like if we called it directly.
<custom-component :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</custom-component>
<!-- Should renders like this (sometimes) -->
<v-select :loading="loading">
<template #loading>
<span>Loading...</span>
</template>
</v-select>
But I can't find a way to include the slots given to my functional component to the I'm rendering without adding a wrapper around them:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// But it forces me to add a div wrapper...
h('div', { slot: 'loading' }, context.slots()['loading'])
],
)
}
I can't use the scopedSlots option since this slot (for example) has no slot props, so the function is never called.
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
scopedSlots: {
loading(props) {
// Never called because no props it passed to that slot
return context.slots()['loading']
}
}
},
Is there any way to pass down the slots to the component i'm rendering without adding them a wrapper element?
I found out it's totally valid to use the createElement function to render a <template> tag, the same used to determine which slot we are on.
So using it like this fixes my problem:
render (h: CreateElement, context: RenderContext) {
// Removed some logic here for clarity
return h(
'v-select',
{
props: context.props,
attrs: context.data.attrs,
on: context.listeners,
},
[
// I use the `slot` option to tell in which slot I want to render this.
// <template> vue pseudo element that won't be actually rendered in the end.
h('template', { slot: 'loading' }, context.slots()['loading'])
],
)
}
In Vue 3 it's a way easier.
Check the docs Renderless Components (or playground)
An example from the docs:
App.vue
<script setup>
import MouseTracker from './MouseTracker.vue'
</script>
<template>
<MouseTracker v-slot="{ x, y }">
Mouse is at: {{ x }}, {{ y }}
</MouseTracker>
</template>
MouseTracker.vue
<script setup>
import { ref, onMounted, onUnmounted } from 'vue'
const x = ref(0)
const y = ref(0)
const update = e => {
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
</script>
<template>
<slot :x="x" :y="y"/>
</template>
Just to mention, you can easily override CSS of the component in the slot as well.

Vue- best practice for loops and event handlers

I am curious if it is better to include methods within loops instead of using v-if. Assume the following codes work (they are incomplete and do not)
EX: Method
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<span v-on:click="insertPrompt($event)">
{{ d }}
</span>
</div>
</div>
</template>
<script>
export default {
data() {
data:[
.....
]
},
methods:{
insertPrompt(e){
body.insertBefore(PROMPT)
}
}
}
</script>
The DOM would be updated via the insertPrompt() function which is just for display
EX: V-IF
//Parent
<template >
<div>
<div v-for="(d, i) in data" v-bind:key="i">
<child v-bind:data="d"/>
</div>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data() {
data:[
.....
]
},
}
</script>
//Child
<template>
<div>
<span v-on:click="display != display">
{{ d }}
</span>
<PROMPT v-if="display"/>
</div>
</template>
<script>
import child from './child'
export default {
components:{
child
},
data(){
return {
display:false
}
},
props: {
data:{
.....
}
},
}
</script>
The PROMPT is a basic template that is rendered with the data from the loop data click.
Both methods can accomplish the same end result. My initial thought is having additional conditions within a loop would negatively impact performance?!?!
Any guidance is greatly appreciated
Unless you are rendering really huge amounts of items in your loops (and most of the times you don't), you don't need to worry about performance at all. Any differences will be so small nobody will ever notice / benefit from having it a tiny touch faster.
The second point I want to make is that doing your own DOM manipulations is often not the best idea: Why do modern JavaScript Frameworks discourage direct interaction with the DOM
So I would in any case stick with the v-if for conditionally rendering things. If you want to care about performance / speed here, you might consider what exactly is the way your app will be used and decide between v-if and v-show. Citing the official documentation:
v-if is “real” conditional rendering because it ensures that event
listeners and child components inside the conditional block are
properly destroyed and re-created during toggles.
v-if is also lazy: if the condition is false on initial render, it
will not do anything - the conditional block won’t be rendered until
the condition becomes true for the first time.
In comparison, v-show is much simpler - the element is always rendered
regardless of initial condition, with CSS-based toggling.
Generally speaking, v-if has higher toggle costs while v-show has
higher initial render costs. So prefer v-show if you need to toggle
something very often, and prefer v-if if the condition is unlikely to
change at runtime.
https://v2.vuejs.org/v2/guide/conditional.html#v-if-vs-v-show
There are numerous solutions to solving this issue, but let's stick to 3. Options 2 and 3 are better practices, but option 1 works and Vue was designed for this approach even if hardcore developers might frown, but stick yoru comfort level.
Option 1: DOM Manipulation
Your data from a click, async, prop sets a condition for v-if or v-show and your component is shown. Note v-if removes the DOM element where v-show hides the visibility but the element is still in the flow. If you remove the element and add its a complete new init, which sometimes works in your favor when it come to reactivity, but in practice try not to manipulate the DOM as that will always be more expensive then loops, filters, maps, etc.
<template >
<div>
<div v-for="(d, i) in getData"
:key="i">
<div v-if="d.active">
<child-one></child-one>
</div>
<div v-else-if="d.active">
<child-two></child-two>
</div>
</div>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
data() {
return {
data: [],
}
},
computed: {
getData() {
return this.data;
},
},
mounted() {
// assume thsi woudl come from async but for now ..
this.data = [
{
id: 1,
comp: 'ChildOne',
active: false
},
{
id: 2,
comp: 'ChildTwo',
active: true
},
];
}
}
</script>
Option 2: Vue's <component> component
Always best to use Vue built in component Vue’s element with the is special attribute: <component v-bind:is="currentTabComponent"></component>
In this example we pass a slug or some data attribute to activate the component. Note we have to load the components ahead of time with the components: {}, property for this to work i.e. it has to be ChildOne or ChildTwo as slug string. This is often used with tabs and views to manage and maintain states.
The advantage of this approach is if you have 3 form tabs and you enter data on one and jump to the next and then back the state / data is maintained, unlike v-if where everything will be rerendered / lost.
Vue
<template >
<div>
<component :is="comp"/>
</div>
</template>
<script>
import ChildOne from "./ChildOne";
import ChildTwo from "./ChildTwo";
export default {
components: {
ChildOne,
ChildTwo
},
props: ['slug'],
data() {
return {
comp: 'ChildOne',
}
},
methods: {
setComponent () {
// assume prop slug passed from component or router is one of the components e.g. 'ChildOne'
this.comp = this.slug;
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
Option 3: Vue & Webpack Async and Dynamic components.
When it comes to larger applications or if you use Vuex and Vue Route where you have dynamic and large number of components then there are a number of approaches, but I'll stick to one. Similar to option 2, we are using the component element, but we are using WebPack to find all Vue files recursively with the keyword 'module'. We then load these dynamically / asynchronous --- meaning they will only be loaded when needed and you can see this in action in network console of browser. This means I can build components dynamically (factory pattern) and render them as needed. Example, of this might be if a user adds projects and you have to build and config views dynamically for projects created e.g. using vue router you passed it a ID for a new project, then you would need to dynamically load an existing component or build and load a factory built one.
Note: I'll use v-if on a component element if I have many components and I'm unsure the user will need them. I don't want to maintain state on large collections of components because I will end up memory and with loads of observers / watches / animations will most likely end up with CPU issues
<template >
<div>
<component :is="module" v-if="module"/>
</div>
</template>
<script>
const requireContext = require.context('./', true, /\.module\.vue$/);
const modules = requireContext.keys()
.map(file =>
[file.replace(/(.*\/(.+?)\/)|(\.module.vue$)/g, ''), requireContext(file)]
)
.reduce((components, [name, component]) => {
// console.error("components", components)
components[name] = component.default || component
return components
}, {});
export default {
data() {
return {
module: [],
}
},
props: {
slug: {
type: String,
required: true
}
},
computed: {
getData() {
return this.data;
},
},
methods: {
setModule () {
let module = this.slug;
if (!module || !modules[module]) {
module = this.defaultLayout
}
this.module = modules[module]
}
},
mounted() {
this.nextTick(this.setModule())
}
}
</script>
My initial thought is having additional conditions within a loop would negatively impact performance?
I think you might be confused by this rule in the style guide that says:
Never use v-if on the same element as v-for.
It's only a style issue if you use v-if and v-for on the same element. For example:
<div v-for="user in users" v-if="user.isActive">
But it's not a problem if you use v-if in a "child" element of a v-for. For example:
<div v-for="user in users">
<div v-if="user.isActive">
Using v-if wouldn't have a more negative performance impact than a method. And I'm assuming you would have to do some conditional checks inside your method as well. Remember that even calling a method has some (very small) performance impact.
Once you use Vue, I think it's a good idea not to mix it up with JavaScript DOM methods (like insertBefore). Vue maintains a virtual DOM which helps it to figure out how best to update the DOM when your component data changes. By using JavaScript DOM methods, you won't be taking advantage of Vue's virtual DOM anymore.
By sticking to Vue syntax you also make your code more understandable and probably more maintainable other developers who might read or contribute to your code later on.