How to apply classes to Vue.js Functional Component from parent component? - javascript

Suppose I have a functional component:
<template functional>
<div>Some functional component</div>
</template>
Now I render this component in some parent with classes:
<parent>
<some-child class="new-class"></some-child>
</parent>
Resultant DOM doesn't have new-class applied to the Functional child component. Now as I understand, Vue-loader compiles Functional component against render function context as explained here. That means classes won't be directly applied and merge intelligently.
Question is - how can I make Functional component play nicely with the externally applied class when using a template?
Note: I know it is easily possible to do so via render function:
Vue.component("functional-comp", {
functional: true,
render(h, context) {
return h("div", context.data, "Some functional component");
}
});

TL;DR;
Use data.staticClass to get the class, and bind the other attributes using data.attrs
<template functional>
<div class="my-class" :class="data.staticClass || ''" v-bind="data.attrs">
//...
</div>
</template>
Explanation:
v-bind binds all the other stuff, and you may not need it, but it will bind attributes like id or style. The problem is that you can't use it for class because that's a reserved js object so vue uses staticClass, so binding has to be done manually using :class="data.staticClass".
This will fail if the staticClass property is not defined, by the parent, so you should use :class="data.staticClass || ''"
Example:
I can't show this as a fiddle, since only "Functional components defined as a Single-File Component in a *.vue file also receives proper template compilation"
I've got a working codesandbox though: https://codesandbox.io/s/z64lm33vol

Render function inside a functional component example, to supplement #Daniel's answer:
render(createElement, { data } {
return createElement(
'div',
{
class: {
...(data.staticClass && {
[data.staticClass]: true,
})
},
attrs: {
...data.attrs,
}
}
)
}
We can use ES6 computed property name in order to set a static class.

You have to use props to pass attributes to components
https://v2.vuejs.org/v2/guide/components.html#Passing-Data-to-Child-Components-with-Props

Related

vue 3 access props passed from parent component that are not defined in child component

hope everything is okay.
i am used to react.js but when i try vue things were a bit different
in react it's very simple accessing props passed from parent in the child component
but in vue i must define each prop to use it.
so i was just wondering if it's possible to pass any prop and use it in the child component without defining it
in react i can do that
// parent component
const Parent = () => {
return (
<Child anyprop="propvalue" />
)
}
const Child = (props) => {
return (
<p>{JSON.stringify(props)}</p>
)
}
and that would work
in vue i can only use props if i define it like this
//parent
<template>
<Child anyprop="value" anotherprop="value"></Child>
</template>
<script setup>
const props = defineProps({
anyprop: String
})
//child
<template>
<p>{{props}}</p>
</template>
<script setup>
const props = defineProps({
anyprop: String
})
</script>
if i do that in vue i can only see "anyprop" not the "anotherprop" i must define it in the define props block to use it
any ideas what can i do to achieve something like what react.js offers
All data that isn't defined in props goes to attributes. Despite the name, they aren't HTML attributes and can have non-string values.
In a template they are available as $attrs:
<Child :anyprop="$attrs.anyprop" anotherprop="value"/>
A more common case is to pass all attributes instead of enumerating them, this is a counterpart to React {...rest}:
<Child v-bind="$attrs" anotherprop="value"/>
This may be not needed for root element like Child because attribute fallthrough is done by default, otherwise inheritAttrs: false should be used.
This requires Child to have anyprop prop declared.
In a script, attributes are available as context.attrs in setup function and useAttrs in script setup, they can be used to compute props for nested elements.

Vue functional component throws _c is not defined error

I created the following functional component using render method:
import Vue from "vue"
const { render, staticRenderFns } = Vue.compile(`<div>Hello World</div>`)
Vue.component("HelloWorld", {
functional: true,
render,
staticRenderFns
})
And then in App.vue:
<template>
<div id="app">
<HelloWorld />
</div>
</template>
<script>
export default {
data() {
return {
compiled: false
}
}
}
</script>
<style>
</style>
And I get the error:
_c is not defined.
Am I doing something wrong here?
As far as I'm aware, the render functions generated by Vue.compile cannot be used to render a functional component.
I think the closest thing to creating a functional component from a template string would involve parsing the string to get the element type and attributes and render as:
Vue.component("hello-world", {
functional: true,
render: function(createElement, context) {
return createElement(
"div",
'example text'
);
}
});
As Decade Moon already mentioned, render functions returned by Vue.compile cannot be used as render function in functional component. Reason for that becomes clear when you inspect the signature of the function returned by Vue.compile:
const render: (createElement: any) => VNode
As you can see, function is missing second argument, which is required for render functions of functional components:
render: (createElement: CreateElement, context: RenderContext<Record<never, any>>) => VNode
Functional components are instanceless. That means no this context - that's why additional context parameter is needed to carry props\listeners etc.
Also if you look at this post on Vue forum:
The render function created by compile() relies on private properties of the component. To have access to those properties, the method has to be assigned to a property of the component(so it has access to those properties via this)
However that doesn't mean you can't create functional component with template. You can, you are just not able to use Vue.compile to pass template text dynamically. If you are fine with static template, you can do it like this:
// component.vue
<template functional>
<div>Hello World</div>
</template>
<script>
export default {
name: "hello"
};
</script>
...and use the component like any other SFC (single file component = VUE file)
If you need dynamic template text, just use non-functional component instead...

vue - $emit vs. reference for updating parent data

We need to use $emit to update the parent data in a vue component. This is what has been said everywhere, even vue documentation.
v-model and .sync both use $emit to update, so we count them $emit here
what I'm involved with is updating the parent data using reference type passing
If we send an object or array as prop to the child component and change it in the child component, changes will be made to the parent data directly.
There are components that we always use in a specific component and we are not going to use them anywhere else. In fact, these components are mostly used to make the app codes more readable and to lighten the components of the app.
passing reference type values as prop to children for directly change them from children is much easier than passing values then handle emitted event. especially when there are more nested components
code readability is even easier when we use reference type to update parent.
For example, suppose we have grand-parent, parent and child components. in parent component we have a field that change first property of grand-parent data and in child component we have another field that change second property of grand-parent data
If we want to implement this using $emit we have something like this : (we are not using .sync or v-model)
// grand-parent
<template>
<div>
<parent :fields="fields" #updateFields="fields = $event" >
</div>
</template>
<script>
import parent from "./parent"
export default {
components : {parent},
data(){
return {
fields : {
first : 'first-value',
second : 'second-value',
}
}
}
}
</script>
// parent
<template>
<div>
<input :value="fields.first" #input="updateFirstField" />
<child :fields="fields" #updateSecondField="updateSecondField" >
</div>
</template>
<script>
import child from "./child"
export default {
components : {child},
props : {
fields : Object,
},
methods : {
updateFirstField(event){
this.$emit('updateFields' , {...this.fields , first : event.target.value})
},
updateSecondField(value){
this.$emit('updateFields' , {...this.fields , second : value})
}
}
}
</script>
// child
<template>
<div>
<input :value="fields.first" #input="updateSecondField" />
</div>
</template>
<script>
export default {
props : {
fields : Object,
},
methods : {
updateFirstField(event){
this.$emit('updateSecondField' , event.target.value)
},
}
}
</script>
Yes, we can use .sync to make it easier or pass just field that we need to child. but this is basic example and if we have more fields and also we use all fields in all component this is the way we do this.
same thing using reference type will be like this :
// grand-parent
<template>
<div>
<parent :fields="fields" >
</div>
</template>
<script>
import parent from "./parent"
export default {
components : {parent},
data(){
return {
fields : {
first : 'first-value',
second : 'second-value',
}
}
}
}
</script>
// parent
<template>
<div>
<input v-model="fields.first" />
<child :fields="fields" >
</div>
</template>
<script>
import child from "./child"
export default {
components : {child},
props : {
fields : Object,
}
}
</script>
// child
<template>
<div>
<input v-model="fields.second" />
</div>
</template>
<script>
export default {
props : {
fields : Object,
}
}
</script>
as you see using reference type is much easier. even if there was more fields.
now my question :
should we use reference type for updating parent data or this is bad approach ?
even if we use a component always in the same parent again we should not use this method ?
what is the reason that we should not use reference type to update parent?
if we should not use reference type why vue pass same object to children and not clone them before passing ? (maybe for better performance ?)
The "always use $emit" rule isn't set in stone. There are pros and cons of either approach; you should do whatever makes your code easy to maintain and reason about.
For the situation you described, I think you have justified mutating the data directly.
When you have a single object with lots of properties and each property can be modified by a child component, then having the child component mutate each property itself is fine.
What would the alternative be? Emitting an event for each property update? Or emitting a single input event containing a copy of the object with a single property changed? That approach would result in lots of memory allocations (think of typing in a text field emitting a cloned object for each keypress). Having said that, though, some libraries are designed for this exact purpose and work pretty well (like Immutable.js).
For simple components that manage only small data like a textbox with a single string value, you should definitely use $emit. For more complex components with lots of data then sometimes it makes sense for the child component to share or own the data it is given. It becomes a part of the child component's contract that it will mutate the data in certain circumstances and in some particular way.
what is the reason that we should not use reference type to update parent?
The parent "owns" the data and it knows that nobody but itself will mutate it. No surprises.
The parent gets to decide whether or not to accept the mutation, and can even modify it on-the-fly.
You don't need a watcher to know when the data is changed.
The parent knows how the data is changed and what caused the change. Imagine there are multiple ways that the data can be mutated. The parent can easily know which mutation originated from a child component. If external code (i.e. inside a child component) can mutate the data at any time and for any reason, then it becomes much more difficult for the parent to know what caused the data to change (who changed it and why?).
if we should not use reference type why vue pass same object to children and not clone them before passing ? (maybe for better performance ?)
Well yes, for performance, but also many other reasons such as:
Cloning is non-trivial (Shallow? Deep? Should the prototype be copied too? Does it even make sense to clone the object? Is it a singleton?).
Cloning is expensive memory- and CPU-wise.
If it were cloned then doing what you describe here would be impossible. It would be silly to impose such a restrictive rule.
#Vue Detailed usage of $refs, $emit, $on:
$refs - parent component calls the methods of the child component. You can pass data.
$emit - child components call methods of the parent component and pass data.
$on - sibling components pass data to each other.

How to foward $refs in Vue

I have a component which should pass everything on to the child. I'm successfully passing $attrs and $listeners already:
<template>
<el-form v-on="$listeners" v-bind="$attrs" :label-position="labelPosition">
<slot />
</el-form>
</template>
But I'm unsure how to also forward $refs like we can do in React, so that when using my component like this:
<el-form-responsive
class="form"
:model="formValues"
status-icon
:rules="rules"
ref="form"
label-width="auto"
#submit.native.prevent="submitForm"
>
Then this.$refs.form is actually a reference to the child <el-form>.
I would rather do this transparently, as in you pass exactly the same props to el-form-responsive as you would to a el-form without needing to know that refs has to be passed in a special way.
I don't think it is possible to directly mimic React's ref. A ref attribute in Vue is just a string which is used to register a child component reference to the parent's $refs object during the render function.
Here are the links to documentations doc & doc
So basically it's a kind of inverted logic.. instead of passing a ref to a child in Vue we get it from the child into the parent. So it's not really possible at this point to create a grandchild reference, which is what you need.
There are some workarounds though.
1. Quick dirty and not transparent but technically it would work:
In the parent component, which uses your el-form-responsive, on mounted hook we could replace the original child reference with the grandchild ref.
Your el-form-responsive component. Template:
<el-form ref="elform">
A parent which uses your el-form-responsive. Template:
<el-form-responsive ref="form">
Script:
...
mounted () {
this.$refs.form = this.$refs.form.$refs.elform
}
And after this this.$refs.form is actually a reference to the granchild <el-form>
2. This one would be more elaborate, but probably mach better then the first method:
In order to make the el-form-responsive component really transparent you could expose some of the methods and properties from the child el-form component to any potential parent. Something like this:
el-form-responsive. Template:
<el-form ref="elform">
Script:
export default {
data: () => ({
whatever: null
}),
mounted () {
this.whatever = this.$refs.elform.whatever
},
methods: {
submit () {
this.$refs.elform.submit()
}
}
}
So then inside some parent el-form-responsive could be used like this:
<el-form-responsive ref="form">
...
mounted () {
const formWhatever = this.$refs.form.whatever // actually `whatever` from `el-form`
this.$refs.form.submit() // eventually calls submit on `el-form`
},
If you are working with a custom component with the script setup in Vue 3, it's worth nothing that template refs work like this.
Essentially, you will have to use defineExpose to "expose" your child component data to the parent component
Try this to replace parent's ref with child's , In el-form-responsive
<template>
<el-form v-on="$listeners" v-bind="$attrs" :label-position="labelPosition" ref='ref'>
<slot />
</el-form>
</template>
mounted () {
Object.entries(this.$parent.$refs).forEach(([key, value]) => {
if (value === this) {
this.$parent.$refs[key] = this.$refs.ref
}
})
...
It is work for me:
mounted() {
Object.assign(Object.getPrototypeOf(this), this.$refs.formRef)
}
warning: insert to __proto__,maybe is bad way

Changing object from external file

I am learning Vue and before that I have some experience working with React.
As, I move on comprehending the basics of vue, I sort of compare things with JS
<template>
<div id="app">
<HelloWorld msg="Below This we will inclue our To-do task list"/>
<p>{{msg}}</p>
<Todos :toDoItem="todos"/>
</div>
</template>
import Todos from "./components/todo.vue";
let todo =
{
name: "Rohit",
title: "Full Stack Developer",
completed: true
}
export default {
name: "app",
components: {
HelloWorld,
Todos
},
data() {
return {
msg: "hello",
todos: todo
};
}
};
We are passing the props here to child component and changing it using method
<template>
<div>
<div class="main-class">
<p>{{toDoItem.title}}</p>
<input type="checkbox" v-on:change="markComplete">
</div>
</div>
</template>
<script>
export default {
name: "toDoItem",
props: {
toDoItem: {
type: Object
}
},
method: {
markComplete() {
this.toDoItem.complete = !this.toDoItem.complete
}
};
</script>
This is where I am unable to comprehend many things.
Question 1: Based on my understanding of this, shouldn't it point to a global space and hence this should be undefined?
markComplete() {
this.toDoItem.complete = !this.toDoItem.complete
}
Question 2: In react we can't change props passed to a child object probably? but here we are changing the props? Also, In general, if we declare a object in another file (app.js), can we change it to in another file (someApp.js)?
All methods in a Vue instance are bound to the instance so that this works properly.
You should not be changing props. Here, you are changing a member of a prop -- toDoItem is a prop, and complete is a member of it -- which is not controlled, but you still should not do it. You should instead emit an event that the parent handles.
Your markComplete should be in the parent, since it owns the data being manipulated. It would look like:
markComplete() {
this.todos.complete = !this.todos.complete;
}
In the child, the method would be something like
toggleComplete() {
this.$emit('toggle');
}
And the parent would use v-on to handle the toggle event:
<Todos :toDoItem="todos" v-on:toggle="markComplete"/>
This way all the code that manipulates data owned by a component happens within that component. Nobody calls anybody else's methods or modifies their data.
I think you intend for todos to be an array of items, and probably want to v-for over them, having one child component for each item, but that's another issue.
I cannot answer your last question about objects declared in different files, because I don't understand it. It is not unusual to import objects and modify them, generally. As I mentioned, though, Vue components should be the only ones to modify their own data.

Categories

Resources