how to expose property on component in vue.js 2.0 - vue.js

I am thinking how to implement validation on vue.js component.
The initial idea is that component validates and return an error code, like: "require", "min", "max" etc. Another component will display full text message according to this error code.
Because error message might not always display inside component's template. I need two separated components.
pseudo code is like this.
<div>
<mycomponent ref="salary" errcode="ErrorCode"></mycomponent>
</div>
.....
<div>
<errormessage watchcomponent="salary" message="salaryErrorMessages"></errormessage>
</div>
salaryErrorMessages is a hash of codes and messages. for example:
{"require":"please enter salary",
"min": "minimum salary value is 10000"}
Vue has ref attribute on component, . I don't know if I can use ref to reference a component in attribute.
Other solutions I considered:
add an error object in model of the page, and pass into using :sync binding. can monitor same object.
This requires to declare error messages in model.
If I consider the requirement that page also needs to know if there is an error before post back. a global error object might be necessary.
use event bus or Vuex.
This seems official solution, but I don't know .
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.

You should definitely use Vuex. Not only it solves your problem, but it gives you huge scalability in your project.

You're working literally backwards. Vue is about passing data from parent to child, not exposing child properties to parent components.
add an error object in model of the page, and pass into using :sync binding. can monitor same object. This requires to declare error messages in model.
sync is gone from Vue v2, but the idea is fundamentally sort of correct: a parent component holds one object, child components get passed chunks of it as props, parent object automagically gets updated in the parent when it is changed in a child. It doesn't have to be the root component.
use event bus or Vuex. This seems official solution, but I don't know .
Vuex is pretty much always the Correct solution if you have an application that needs to manage a lot of state-related shenanigans.
When a page has multiple instances of , they will trigger same event. all instances will monitor same event.
Vue will very frequently pollute your console with warnings that data must be a function. This is why!
Here is an abridged version really shameful hackjob I inflicted on innocen made for an acquaintance recently:
In my defense, putting things in __proto__ is a very quick way to make them non-enumerable.
Vue.component('error', {
template: '#error',
props: ['condition', 'errorMessage']
})
Vue.component('comp', {
template: '#comp',
props: ['errorMessage', 'model'],
})
Vue.component('app', {
template: '#app',
data() {
return {
models: {
nameForm: {
firstName: '',
lastName: '',
__proto__: {
validator: function(value) {
return _(value).every(x => x.length > 2)
}
}
},
otherForm: {
notVeryInterestingField: 'There are words here',
veryImportantField: 0,
__proto__: {
validator: function(value) {
return value.veryImportantField > 20
}
}
},
__proto__: {
validator: function(value) {
return _(value).every(x => x.validator(x))
}
}
}
}
}
})
const vm = new Vue({
el: '#root'
})
.error {
background: orange
}
.alright {
background: mediumaquamarine
}
section > div, pre {
padding: 6px;
}
section {
flex: 1;
}
#root {
display: flex;
flex-direction: column;
}
<script src="https://unpkg.com/vue#2.1.6/dist/vue"></script>
<script src="https://unpkg.com/lodash"></script>
<template id="comp">
<div :class="[model.validator(model) ? 'alright' : 'error']" style="display: flex; border-bottom: 2px solid rgba(0,0,0,0.4)">
<section>
<div v-for="(field, fieldName) in model">
{{_.startCase(fieldName)}}:
<input v-model="model[fieldName]">
<br>
</div>
<error :condition="!model.validator(model)" :error-message="errorMessage"></error>
</section>
<section>
<pre>props: {{$options.propsData}}</pre>
</section>
</div>
</template>
<template id="error">
<div v-if="condition">
{{ errorMessage || 'Oh no! An error!' }}
</div>
</template>
<template id="app">
<div :class="[models.validator(models) ? 'alright' : 'error']">
<comp :model="model" error-message="Mistakes were made." v-for="model in models"></comp>
<pre>data: {{$data}}</pre>
</div>
</template>
<div id="root">
<app></app>
</div>

Related

Vue/Nuxt Component updates and rerenders without any data changing

I have two components, Carousel.vue and Showcase.vue. I'm testing them both in a page like this:
<template>
<main>
<app-showcase
before-focus-class="app-showcase__element--before-focus"
after-focus-class="app-showcase__element--after-focus"
>
<div class="test-showcase" v-for="n in 10" :key="n">
<img
class="u-image-cover-center"
:src="`https://picsum.photos/1000/1000?random=${n}`"
alt=""
/>
<div>Showcase numero {{ n }}</div>
</div>
</app-showcase>
<div class="u-layout--main u-margin-vertical--4">
<div>
<app-button #click="changeRequestTest(4)">Test Request 4</app-button>
<app-button #click="changeRequestTest(10)">Test Request 10</app-button>
<app-carousel
content-class="u-spread-horizontal--main"
center
:request-element="requestTest"
scroll-by="index"
#index-change="onIndexChange"
>
<template #header>
<h4>Placeholder images</h4>
<div>
Carousel heading h4, showing item number {{ index + 1 }}.
</div>
</template>
<template #default>
<img
:src="`https://picsum.photos/100/80?random=${n}`"
:data-carousel-item-name="n === 10 ? 'giovanni-rana' : ''"
alt=""
v-for="n in 20"
:key="n"
/>
</template>
</app-carousel>
</div>
</div>
</main>
</template>
<script>
export default {
data() {
return {
n1: 20,
n2: 20,
isAnimationOver: false,
index: 0,
requestTest: null,
};
},
methods: {
changeRequestTest(n) {
this.requestTest = n;
},
onIndexChange(e) {
this.requestTest = e;
},
logTest(msg = "hello bro") {
console.log(msg);
},
logElement(e) {
console.log(e);
},
},
created() {
this.requestTest = this.$route.query.hero;
},
};
</script>
Both components use a parameter called index, which basically registers the position (in the children array) of the element that is being focused/shown/highlighted.
...
data() {
return {
index: 0,
showBackButton: true,
showForwardButton: true,
lockScroll: false,
};
},
...
Carousel actually has a prop, requestElement, that allows the carousel to be scrolled to a specific element (via number or element name), and is used in combination with the event "index-change" to update the value in the parent component.
Now that's the problem: every time requestElement updates (there is a watcher in Carousel.vue to follow that), the Showcase component rerenders.
That's a huge problem because the Showcase component applies specific classes on the mounted hook, and on rerender, all that classes are lost. I solved the problem by calling my class-applying-methods in the update hook, but I don't particularly like this performance-wise.
Basically, if I remove any reference to requestElement, everything works as intended, but as soon as that value changes, the showcase rerenders completely. I tried to change props/params names, to use dedicated functions instead of inline scripts, nothing works. No common value is shared, neither via props or vuex, so this makes it even weirder.
Any idea why this happens?
EDIT: I tried the solution from this question, as expected no change was detected in Showcase component, but it still gets updated when requestElement is changed in Carousel.

Are Vue component slots not considered children?

Lets say that I have a parent component, Tabs, which takes in child Tab Components as slots.
So the setup is like this:
Tabs (Parent)
<div>
<slot name="tabChild">
</div>
Tab (Child)
<div>
Tab {{name}}
</div>
MyApp
<Tabs>
<Tab slot="tabChild" name="1" ></Tab>
<Tab slot="tabChild" name="1" ></Tab>
</Tabs>
However, in the Tabs (Parent) component, when I try to programmatically access its children, like this:
Tabs Component
mounted(){
let childTabs = this.$children //this is empty??
childTabs = this.$slots //this is correctly the child Tab Components
}
Moreover, in the Tab (Child) component, when I try to access its parent, which I thought was the Tabs component (since they are slotted within it), it is not:
Tab Component
mounted(){
let parentTab = this.$parent //this is MyApp (grandfather), NOT Tabs
}
Why are the tab child components slotted within the greater Tabs component not its children?
Well, $parent will always refer to the "direct" parent/wrapper a particular component is nested inside, so it's not quite reliable when the need to refer to the "root" parent arises.
Here are some good excerpts from the official docs:
In most cases, reaching into the parent makes your application more difficult to debug and understand, especially if you mutate data in the parent. When looking at that component later, it will be very difficult to figure out where that mutation came from. [1]
Use $parent and $children sparingly as they mostly serve as an escape-hatch. Prefer using props and events for parent-child communication. [2]
Implicit parent-child communication: Props and events should be preferred for parent-child component communication, instead of this.$parent or mutating props.
An ideal Vue application is props down, events up. Sticking to this convention makes your components much easier to understand. [3]
Unfortunately, using the $parent property didn’t scale well to more deeply nested components. That’s where dependency injection can be useful, using two new instance options: provide and inject. [4]
Here's a quick example that sort of demonstrates the above recommendations:
const Parent = Vue.extend({
template: `
<ul :style="{ columnCount: colCount }">
<slot></slot>
</ul>
`,
provide() {
return {
biologicalParent: this
}
},
props: ['value', 'colCount', 'message'],
methods: {
sayIt() {
const
num = this.$children.indexOf(this.value) + 1,
message = [
`I am child no. ${num}`,
`among ${this.$children.length} of my siblings`,
`in ${this.colCount} different lineages.`
]
.join(' ');
this.$emit('update:message', message);
}
},
watch: {
value: 'sayIt'
}
});
const Child = Vue.extend({
template: `<li v-text="name" #click="setChild"></li>`,
props: ['name'],
inject: ['biologicalParent'],
methods: {
setChild() {
this.biologicalParent.$emit('input', this);
}
}
});
new Vue({
el: '#demo',
data: () => ({
lineage: 3,
childCount: 10,
message: 'Click on a child for the associated message.',
lastChild: undefined
}),
components: {
Parent,
Child
}
});
input {
width: 4em;
}
li {
cursor: pointer;
}
.message {
background-color: beige;
border: 1px solid orange;
padding: 3px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<table>
<tr>
<td>Child count:</td>
<td><input type="number" v-model="childCount" /></td>
</tr>
<tr>
<td>Lineage:</td>
<td><input type="number" v-model="lineage" /></td>
</tr>
</table>
<parent
v-model="lastChild"
:col-count="lineage"
:message.sync="message">
<child
v-for="(n, i) of Array.apply(null, {length: childCount})"
:key="i"
:name="`child #${i+1}`">
</child>
</parent>
<p class="message">{{ message }}</p>
</div>
The provide options allows us to specify the data or methods we want to provide to descendant components. In the above example, that's the Parent instance.
Notice how the Child index/number is evaluated at runtime (when clicked) instead of compile time (rendering phase).

VueJS: Why parent's data property not updated when custom event emitted on child fires?

In the following code, codePen demo here
child component emits a custom event changedMsg to parent which changes msg data property on the parent component. Not sure, why changedMsg does not work. It does not modify msg property of parent.
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
VueJS
var child = {
template: '#child',
props: ['parentMsg'],
methods: {
changeParentMsg() {
console.log(this.parentMsg)
this.parentMsg = 'Message was changed by CHILD'
this.$emit('changedMsg', this.parentMsg)
}
}
}
new Vue({
el: '#parent',
data() {
return {
msg: 'Hello World'
}
},
components: {
child
},
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
}
},
})
HTML
<div>
<h4>Parent</h4>
<p>{{ msg }}</p>
<button #click="changeMsg">Change Own Message</button>
<br>
<div class="child">
<h4>Child</h4>
<child :parentMsg="msg" #changedMsg= "msg = $event"></child>
</div>
</div>
</div>
<template id="child">
<div>
<button #click="changeParentMsg">Change Parnets msg</button>
</div>
</template>
CSS
#parent {
background-color: lightblue;
border: 2px solid;
width: 300px;
}
.child {
background-color: lightgray;
border: 2px solid;
margin-top: 20px
}
Thanks
Note: In a single file setup this works, but not in the setup below which is using template tag. Not sure, why?
This is explained in the docs:
Event Names
Unlike components and props, event names will never be used as variable or property names in JavaScript, so there’s no reason to use camelCase or PascalCase. Additionally, v-on event listeners inside DOM templates will be automatically transformed to lowercase (due to HTML’s case-insensitivity), so v-on:myEvent would become v-on:myevent – making myEvent impossible to listen to.
For these reasons, we recommend you always use kebab-case for event names.
So, it's not a good practice to make the logic on the listem event directive, i made this way and worked:
<child :parentMsg="msg" #changed-msg="msgChanged"></child>
</div>
</div>
on the child tag i changed the #changedMsg to kebab-case so now it's #changed-msg and made it call a function instead of do the logic on the child tag, function called msgChanged, so you need to create it on your methods section:
methods: {
changeMsg() {
this.msg = 'Changed Own Msg'
},
msgChanged(param) {
console.log(param)
}
}
hope that helps

Why is Vue changing the parent value from a child without emitting an event

I am fairly new to Vue but doesn't this behavior completely contradict the design of props down, events up?
I have managed to stop it by using Object.assign({}, this.test_object ); when initializing the value in child-component but shouldn't that be the default behaviour?
Here is some background.
I am trying to have a dirty state in a much larger application (Eg a value has changed so a user must save the data back to the database before continuing on their way)
I had an event being emitted, and caught by the parent but the code I had to test the value and init the dirty state was not running as the value had already been changed in the parent component.
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
}
});
Vue.component( 'child-component', {
template: '#child-component',
props: {
test_object: Object
},
data: function() {
return {
child_object: this.test_object
}
}
});
Vue.config.productionTip = false;
new Vue({
el: '#app',
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input style="border:1px solid #CCC; display:block;" type="text" name="html_input" v-model="child_object.val" />
</div>
</script>
<div id="app">
<parent-component></parent-component>
</div>
Use of v-model is a very deceptive thing. If you are not careful, you might end-up mutating data that doesn't belong to your component. In your case, you are accidentally passing read-only props directly to the v-model. It doesn't know if it is a prop or a local component state.
What you are doing is the right solution but considering one-way/unidirectional data flow in mind, we can rewrite this example in more explicit and elegant fashion:
Your component definition would be:
Vue.component( 'parent-component', {
template: '#parent-component',
data: function() {
return {
testObject: {
val: 'Test Value'
}
}
},
methods: {
// Added this method to listen for input event changes
onChange(newValue) {
this.testObject.val = newValue;
// Or if you favor immutability
// this.testObject = {
// ...this.testObject,
// val: newValue
// };
}
}
});
Your templates should be:
<script type="text/x-template" id="parent-component">
<div>
<child-component :test_object="testObject"
#inputChange="onChange"></child-component>
<p>This is in the parent component</p>
<p><code>testObject.val = {{testObject.val}}</code></p>
</div>
</script>
<!-- Instead of v-model, you can use :value and #input binding. -->
<script type="text/x-template" id="child-component">
<div>
<label for="html_input">HTML Input</label>
<input type="text" name="html_input"
:value="test_object.val"
#input="$emit('inputChange', $event.target.value)" />
</div>
</script>
Key things to note:
When using v-model, ensure that you are strictly working on a local value/data of the component. By no means, it should be referenced copy of external prop.
A custom form-like component can be readily converted into the one that can work with v-model provided you accept current value as :value prop and event as #input. v-model will just work out of the box.
Any modification to the value should happen in the same component.

Is there any way to speed up rendering time for one small change in a long list of elements?

My example linked below shows a very large list of a grid of divs (1,000 x 20)
and when clicking on one div, it will highlight only that one element. However,
it seems there is significant overhead by VueJS in the rerendering which introduces a lag when clicking.
<div class="row" v-for="row in rows">
<div v-for="column in row.columns" :class="{red: isHighlighted(row,column)}" #click.prevent="setHighlighted({row: row.id, column: column.id})">
<div>Value: {{column['value']}}</div>
</div>
</div>
Code Pen Example
Sure, you can speed it up by not doing something that requires an evaluation on every update. In your case, the class setting has to call a function for every box every time row or column changes.
I added the style as a member of the column, so that on highlighting, it can be found directly, rather than requiring each cell to notice the change to highlighted. However, this still didn't remove the lag.
After working on this for a while, I surmised that the :class setting was being re-evaluated on every update, even if it was not a function call. My earlier solution handled class-setting explicitly, and that avoided the :class issue. This solution uses a component for each cell, which avoids re-calculation because components only re-evaluate when their props change.
const store = new Vuex.Store({
state: {
rows: [],
rowCount: 2000,
highlighted: {
row: null,
column: null
}
},
getters: {
rows(state) {
return state.rows;
},
rowCount(state) {
return state.rowCount;
},
highlighted(state) {
return state.highlighted;
}
},
mutations: {
setRows(state, rows) {
state.rows = rows;
},
setHighlighted(state, highlighted) {
state.highlighted = highlighted;
}
}
});
new Vue({
el: "#app",
store,
data() {
return {
highlightedEntry: null,
highlightedEl: null
};
},
created() {
this.setRows(
Array.from(Array(this.rowCount).keys()).map(i => {
return {
id: i,
columns: Array.from(Array(parseInt(20)).keys()).map(j => {
return {
id: j,
value: Math.random().toPrecision(4),
isHighlighted: false
};
})
};
})
);
},
computed: {
...Vuex.mapGetters(["rows", "rowCount", "highlighted"])
},
components: {
aCell: {
props: ['rowId', 'column'],
template: '<div :class="{red: column.isHighlighted}" #click="highlight">Value: {{column.value}}</div>',
computed: {
style() {
return this.column.style;
}
},
methods: {
highlight() {
this.$emit('highlight', this.rowId, this.column);
}
}
}
},
methods: {
...Vuex.mapMutations(["setRows", "setHighlighted"]),
highlight(rowId, column) {
if (this.highlightedEntry) {
this.highlightedEntry.isHighlighted = false;
}
this.highlightedEntry = column;
column.isHighlighted = true;
this.setHighlighted({
row: rowId,
column: column.id
});
}
}
});
.row {
display: flex;
justify-content: space-between;
}
.row>* {
border: 1px solid black;
}
.red {
background-color: red;
}
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/2.5.13/vue.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/vuex/3.0.1/vuex.js"></script>
<div id="app">
<div>
Cells: {{rowCount * 20}}
</div>
<div class="row" v-for="row in rows" :key="row.id">
<div v-for="column in row.columns">
<a-cell :row-id="row.id" :column="column" #highlight="highlight"></a-cell>
</div>
</div>
</div>
If each of your items has a unique property like an id, pass that to :key="item.id":
<div v-for="column in row.columns" :key="column.id" ...
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values (using shorthand here):
<div v-for="item in items" :key="item.id">
<!-- content -->
</div>
It is recommended to provide a key with v-for whenever possible, unless the iterated DOM content is simple, or you are intentionally relying on the default behavior for performance gains.
Since it’s a generic mechanism for Vue to identify nodes, the key also has other uses that are not specifically tied to v-for, as we will see later in the guide.
https://v2.vuejs.org/v2/guide/list.html#key