VueJS functional components (SFC): how to encapsulate code? - vue.js

I wrote a simple template-substitution component in VueJS as a single-file component. It doesn't have many features: just one prop, and I also made a computed property to encapsulate some tricky transformations that are done to that prop before it can be used in the template. It looks something like the following:
<template>
...some-html-here...
<a :href="myHref">...</a>
...
</template>
<script>
export default {
name: 'MyComponent',
props: {
href: { type: String, required: true },
},
computed: {
myHref() {
let result = this.href;
// several lines of complicated logic making substitutions and stuff
// ...
return result;
}
}
};
</script>
Now I think this should really be a functional component, as it has no state, no data, no reactivity, and so lunking around a whole instance is wasteful.
I can make this functional just by adding the 'functional' attribute to my <template>. In a functional component, of course, there are no such things as computed properties or methods or whatever. So my question is: where can I put my several lines of complicated logic? I don't want to have to embed this directly into my template, especially as it is used in multiple places. So where can I put code to transform my input props and make them ready to use in my template?

Great question.I was trying to find the same answer and i ended up with the following which i don't know if it is a good way to do though.
The "html" part:
<template functional>
<div>
<button #click="props.methods.firstMethod">Console Something</button>
<button #click="props.methods.secondMethod">Alert Something</button>
</div>
</template>
The "js" part:
<script>
export default {
props: {
methods: {
type: Object,
default() {
return {
firstMethod() {
console.log('You clicked me')
},
secondMethod() {
alert('You clicked me')
}
}
}
}
}
}
</script>
See it in action here
Make sure to read about functional components at docs
NOTE: Be aware using this approach since functional components are stateless (no reactive data) and instanceless (no this context).

Related

Vue prop watcher triggered unexpectedly if the prop is assigned to an object directly in template

Here is the code:
<template>
<div>
<foo :setting="{ value: 'hello' }" />
<button #click="() => (clicked++)">{{ clicked }}</button>
</div>
</template>
<script>
import Vue from 'vue'
// dummy component with dummy prop
Vue.component('foo', {
props: {
setting: {
type: Object,
default: () => ({}),
},
},
watch: {
setting() {
console.log('watch:setting')
},
},
render(createElement) {
return createElement(
'div',
['Nothing']
)
},
})
export default {
data() {
return {
clicked: 0,
}
},
}
</script>
I also made a codepen: https://codepen.io/clinicion-lin/pen/LYNJwJP
The thing is: every time I click the button, the watcher for setting prop in foo component would trigger, I guess the cause is that the content in :settings was re-evaluated during re-render, but I am not sure.
This behavior itself does not cause any problem but it might cause unwanted updates or even bugs if not paid enough attention (actually I just made one, that's why I come to ask :D). The issue can be easily resolved by using :setting="settingValue", but I am wondering if there are alternative solutions, OR best practices for it. I saw some code are assigning object in template directly too, and it feels natural and convenient.
Thanks for anyone who can give an explanation or hint.
First, the docs: "every time the parent component is updated, all props in the child component will be refreshed with the latest value"
By using v-bind:propA="XX" in the template, you are telling Vue "XX is JavaScript expression and I want to use it as value of propA"
So if you use { value: 'hello' } expression, which is literally "create new object" expression, is it really that surprising new object is created (and in your case watcher executed) every time parent is re-rendered ?
To better understand Vue, it really helps to remember that everything in the template is always compiled into plain JavaScript and use the tool as vue-compiler-online to take a look at the output of Vue compiler.
For example template like this:
<foo :settings="{ value: 'hello' }" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": {
value: 'hello'
}
}
})
}
}
Template like this:
<foo :settings="dataMemberOrComputed" />
is compiled into this:
function render() {
with(this) {
return _c('foo', {
attrs: {
"settings": dataMemberOrComputed
}
})
}
}
It's suddenly very clear new object is created every time in the first example, and same reference to an object (same or different ...depends on the logic) is used in second example.
I saw some code are assigning object in template directly too, and it feels natural and convenient.
I'm still a Vue newbie but I browse source code of "big" libraries like Vuetify from time to time to learn something new and I don't think passing "new object" expression into prop is common pattern (how that would be useful?).
What is very common is <foo v-bind="{ propA: propAvalue, propB: propBValue }"> - Passing the Properties of an Object which is a very different thing...

Vue: Make sure the component cannot modify props (even if it is a reference object)?

I know that the vue model is a unidirectional data flow of props.
However, when prop is a reference object, the component can directly modify its properties. This is wrong, but vue will not check it.
I hope there is a mechanism to ensure that the component cannot modify the props (even if it is a reference object), rather than being checked by the developer.
For example, I have a component
<template>
<input v-model="obj.text" />
</template>
<script>
export default {
props: ['obj']
};
</script>
And a page that uses it
<template>
<my-template :obj="myobj"></my-template>
</template>
<script>
export default {
data() {
myobj: {
text: "hello";
}
}
};
</script>
When data changes in 'input', myobj.text will change together. This violates the unidirectional data flow.
Of course, as shown in the answer, I can use the "get" and "set" methods of the "computed".
But I must be careful not to write 'obj.someProperty' to any 'v-model', but this requires my own attention.
I hope there is a mechanism to give a hint when I make a mistake.
Couldn't find an existing duplicate so here's an answer. If anyone can find one, let me know and I'll make this one a Community wiki.
Use a computed property with getter and setter to represent your v-model value.
The getter gets the value from the prop and the setter emits the new value to the parent.
For example
<input v-model="computedProp">
props: ['referenceObject'],
computed: {
computedProp: {
get () {
return this.referenceObject.someProperty
},
set (val) {
this.$emit('updated', val)
}
}
}
and in the parent
<SomeComponent :reference-object="refObject" #updated="updateRefObject">
data: () => ({ refObject: { someProperty: 'initial value' } }),
methods: {
updateRefObject (newVal) {
this.refObject.someProperty = newVal
}
}

Vuejs editing prop object property in child component

I have been using Vue for a while but still do not have a clear understanding of the benefits related to this (probably controversial) question. I know it has been asked in different ways many times, but I can't find a clear answer.
As "Example 1", let's say I have a parent component that contains an address object, and it passes that address to a child AddressForm so it can be edited by the user, like this:
// Parent.vue
<template>
<AddressForm :address="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
Then, the child component looks like this, which directly manipulates the properties of the parent address object.
Here I am only showing an input for the address.name to keep things concise, but you can imagine there is a similar text input for any other properties of the address.
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="address.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
address: {
type: Object,
required: true,
}
},
methods: {
this.$emit('submit');
}
}
</script>
The issues here is that I am directly editing the address prop in the child. Of course I do not get any warnings from Vue about it because I am only editing a property of the address object, not actually mutating it by reassigning.
There are countless places where I can read about why this is not best practice, it's an anti-pattern, and it's a bad idea. You shouldn't alter props in child components.
Ok, but why? Can someone provide any type of real world use-case where the code above can lead to issues down the road?
When I read about the "proper" alternative ways to do things, I am even more confused because I don't see any real difference. Here's what I mean:
Let's call this "Example 2", where I now use v-model to enforce "proper" 2-way binding rather than editing the prop directly like I was in Example 1:
// Parent.vue
<template>
<AddressForm v-model="address" #submit="onSubmit"/>
</template>
<script>
export default {
data () {
return {
address: {}
}
},
methods: {
onSubmit() {
console.log('Submit', this.address);
}
}
}
</script>
// AddressForm.vue
<template>
<form #submit.prevent="onSubmit">
<input v-model="localValue.name">
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
props: {
value: {
type: Object,
required: true,
}
},
computed: {
localValue: {
get: function() {
return this.value;
}
set: function(value) {
this.$emit('input', value);
}
}
},
methods: {
this.$emit('submit');
}
}
</script>
What is the difference between Example 1 and Example 2? Aren't they doing, literally, exactly the same thing?
In Example 2, the computed property says that localValue is always equal to value. Meaning, they are the same exact object, just like they were in Example 1.
Further, as you type in the input, if you watch the events being fired in the Vue debugger, AddressForm never even emits the input events, presumably because localValue isn't actually being set, since it's just an object property that's being changed.
This again shows that Example 1 and Example 2 are doing the exact same thing. The object property is still being directly mutated within the child even though v-model is being used.
So, again, my question is, why is Example 1 considered bad practice? And how is Example 2 any different?
Referring to the docs, manipulating a prop directly causes several issues:
Makes app's data flow harder to understand
Every time a parent component is updated, all props in the child component will be refreshed

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.

How do I create new instances of a Vue component, and subsuqently destroy them, with methods?

I'm trying to add components to the DOM dynamically on user input. I effectively have a situation with ±200 buttons/triggers which, when clicked, need to create/show an instance of childComponent (which is a sort of infowindow/modal).
I would also then need to be able to remove/hide them later when the user 'closes' the component.
I'm imagining something like this?
<template>
<div ref="container">
<button #click="createComponent(1)" />
...
<button #click="createComponent(n)" />
<childComponent ref="cc53" :num="53" v-on:kill="destroyComponent" />
...
<childComponent ref="ccn" :num="n" v-on:kill="destroyComponent"/>
</div>
</template>
<script>
import childComponent from '#/components/ChildComponent'
export default {
components: {childComponent},
methods: {
createComponent (num) {
// How do I create an instance of childComponent with prop 'num' and add it to this.$refs.container?
},
destroyComponent (vRef) {
// How do I destroy an instance of childComponent?
this.vRef.$destroy();
}
}
}
</script>
The number of possible childComponent instances required is finite, immutable and known before render, so I could loop and v-show them, but your typical user will probably only need to look at a few, and certainly only a few simultaneously.
My questions:
Firstly, given there are ±200 of them, is there any performance benefit to only creating instances dynamically as and when required, vs. v-for looping childComponents and let Vue manage the DOM?
Secondly, even if v-for is the way to go for this particular case, how would one handle this if the total number of possible childComponents is not known or dynamic? Is this a job for Render Functions and JSX?
If I understand, you want to display a list of the same component that take :num as a prop.
First, you have to keep in mind that Vue is a "Data driven application", wich means that you need to represent your list as Data in an array or an object, in your case you can use a myList array and v-for loop to display your child components list in the template.
The add and remove operations must be donne on the myList array it self, once done, it will be automatically applied on your template.
To add a new instance just use myList.push(n)
To remove an instance use myLsit.splice(myLsit.indexOf(n), 1);
The result should look like this :
<template>
<input v-model="inputId" />
<button #click="addItem(inputId)">Add Item</button>
<childComponent
v-for="itemId in myList"
:key="itemId"
:ref="'cc' + itemId"
:num="itemId"
#kill="removeItem(itemId)"
/>
</template>
<script>
data(){
return{
inputId : 0,
myList : []
}
},
methods:{
addItem(id){
this.myList.push(id)
},
removeItem(id){
this.myLsit.splice(this.myLsit.indexOf(id), 1)
}
}
</script>
Ps :
Didn't test the code, if there is any error just tell me
#kill method must be emitted by the childComponent, $emit('kill', this.num)
Here is an excellent tutorial to better understand v-for
Performance Penalties
As there is only a limited possibility of ±200 elements, I highly doubt that it can cause any performance issue, and for further fine-tuning, instead of using v-show, you can use v-if it'll reduce the total memory footprint, but increases the render time if you're going to change the items constantly.
Other Approaches
If there weren't limited possibilities of x elements, it'd be still and v-for having items which contain the v-if directive.
But if the user could only see one item (or multiple but limited items) at the same time, instead of v-for, It'd much better to directly bind the properties to the childComponent.
For example, if the child component is a modal that'll be shown by the application when a user clicked on the edit button for a row of a table. Instead of having x number of modals, each having editable contents of a row and showing the modal related to the edit button, we can have one modal and bind form properties to it. This approach usually implemented by having a state management library like vuex.
Finally, This is an implementation based on vuex, that can be used, if the user could only see one childComponent at the same time, it can be easily extended to support multiple childComponent viewed at the same time.
store.js
export Store {
state: {
childComponentVisible: false,
childComponentNumber: 0
},
mutations: {
setChildComponentNumber(state, value) {
if(typeof value !== 'number')
return false;
state.childComponentNumber = value;
},
setChildComponentVisibility(state, value) {
if(typeof value !== 'boolean')
return false;
state.childComponentVisible = value;
}
}
}
child-component.vue
<template>
<p>
{{ componentNumber }}
<span #click="close()">Close</span>
</p>
</template>
<script>
export default {
methods: {
close() {
this.$store.commit('setChildComponentVisibility', false);
}
}
computed: {
componentNumber() {
return this.$store.state.childComponentNumber;
}
}
}
</script>
list-component.vue
<template>
<div class="list-component">
<button v-for="n in [1,2,3,4,5]" #click="triggerChildComponent(n)">
{{ n }}
</button>
<childComponent v-if="childComponentVisible"/>
</div>
</template>
<script>
export default {
methods: {
triggerChildComponent(n) {
this.$store.commit('setChildComponentNumber', n);
this.$store.commit('setChildComponentVisibility', true);
}
},
computed: {
childComponentVisible() {
return this.$store.state.childComponentVisible;
}
}
}
</script>
Note: The code written above is abstract only and isn't tested, you might need to change it a little bit to make it work for your own situation.
For more information on vuex check out its documentation here.