determine if a set of related values have changed in VueJS - vuejs2

I am very new to VueJS.
From what I've seen there is probably an elegant answer to this. I have a table of records. Clicking on one of them opens a modal and loads that row/record. My code looks like this (made easier to read):
Javascript
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
}
});
Html
<tr v-for="record in records">
<td v-on:click="loadRecord(record)">{{ record.id }}</td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
What I'm trying to do is really simple: detect if focusRecord has changed after it has been loaded into the modal from a row/record. Ideally another attribute like app.focusRecord.changed that I can reference. I'm thinking this might be a computed field which I'm learning about, but again with Vue there may be a more elegant way. How would I do this?

What you need is to use VueJS watchers : https://v2.vuejs.org/v2/guide/computed.html#Watchers
...
watch : {
focusRecord(newValue, oldValue) {
// Hey my value just changed
}
}
...
Here is another way to do it, however I didn't know what's refers "focusRecordInitial"
new Vue({
el: '#app',
data() {
return {
records: [],
focusRecordIndex: null
}
},
computed : {
focusRecord() {
if (this.focusRecordIndex == null) return null
if (typeof this.records[this.focusRecordIndex] === 'undefined') return null
return this.records[this.focusRecordIndex]
}
},
watch : {
focusRecord(newValue, oldValue) {
alert('Changed !')
}
},
created() {
this.records = [{id: 1, firstname: 'John', lastname: 'Doe'}, {id: 2, firstname: 'Jane', lastname: 'Doe'}, {id: 3, firstname: 'Frank', lastname: 'Doe'}]
},
methods : {
loadRecord(index) {
this.focusRecordIndex = index
}
}
})
<script src="https://unpkg.com/vue"></script>
<div id="app">
<table>
<tr v-for="(record, i) in records">
<td><button #click="loadRecord(i)">{{ record.id }}</button></td>
<td>{{ record.firstname }} {{ record.lastname }}</td>
</tr>
</table>
{{focusRecord}}
</div>

Vue Watchers
Vue provides a more generic way to react to data changes through the watch option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data.
Vue JS Watchers
You can do something like this:
app = new Vue({
el: '#app',
data: {
records: [], //keys have no significance
focusRecord: { //this object will in the modal to edit, initialize it
id: '',
firstname: '',
lastname: ''
},
focusRecordInitial: {}
},
created: function(){
//load values in app.records via ajax; this is working fine!
app.records = ajax.response.data; //this is pseudo code :)
},
methods: {
loadRecord: function(record){
app.focusRecord = record; // and this works
app.focusRecordInitial = record;
}
},
watch: {
loadRecord: function () {
alert('Record changed');
}
}
});
You can also check out: Vue JS Computed Properties

You can set up a watcher to react to data changes as:
watch: {
'focusRecord': function(newValue, oldValue) {
/* called whenever there is change in the focusRecord
property in the data option */
console.log(newValue); // this is the updated value
console.log(oldValue); // this is the value before changes
}
}
The key in the watch object is the expression you want to watch for the changes.
The expression is nothing but the dot-delimited paths of the property you want to watch.
Example:
watch: {
'focusRecord': function(newValue, oldValue) {
//do something
},
'focusRecord.firstname': function(newValue, oldValue){
//watch 'firstname' property of focusRecord object
}
}

Related

Replace tag dynamically returns the object instead of the contents

I'm building an chat client and I want to scan the messages for a specific tag, in this case [item:42]
I'm passing the messages one by one to the following component:
<script>
import ChatItem from './ChatItem'
export default {
props :[
'chat'
],
name: 'chat-parser',
data() {
return {
testData: []
}
},
methods : {
parseMessage(msg, createElement){
const regex = /(?:\[\[item:([0-9]+)\]\])+/gm;
let m;
while ((m = regex.exec(msg)) !== null) {
msg = msg.replace(m[0],
createElement(ChatItem, {
props : {
"id" : m[1],
},
}))
if (m.index === regex.lastIndex) {
regex.lastIndex++;
}
}
return msg
},
},
render(createElement) {
let user = "";
let msg = this.parseMessage(this.$props.chat.Message, createElement)
return createElement(
'div',
{
},
[
// "hello",// createElement("render function")
createElement('span', '['+ this.$props.chat.Time+'] '),
user,
msg,
]
)
}
};
</script>
I thought passing createElement to the parseMessage method would be a good idea, but it itsn't working properly as it replaces the tag with [object object]
The chatItem looks like this :
<template>
<div>
<span v-model="item">chatITem : {{ id }}</span>
</div>
</template>
<script>
export default {
data: function () {
return {
item : [],
}
},
props :['id'],
created() {
// this.getItem()
},
methods: {
getItem: function(){
obj.item = ["id" : "42", "name": "some name"]
},
},
}
</script>
Example :
if the message looks like this : what about [item:42] OR [item:24] both need to be replaced with the chatItem component
While you can do it using a render function that isn't really necessary if you just parse the text into a format that can be consumed by the template.
In this case I've kept the parser very primitive. It yields an array of values. If a value is a string then the template just dumps it out. If the value is a number it's assumed to be the number pulled out of [item:24] and passed to a <chat-item>. I've used a dummy version of <chat-item> that just outputs the number in a <strong> tag.
new Vue({
el: '#app',
components: {
ChatItem: {
props: ['id'],
template: '<strong>{{ id }}</strong>'
}
},
data () {
return {
text: 'Some text with [item:24] and [item:42]'
}
},
computed: {
richText () {
const text = this.text
// The parentheses ensure that split doesn't throw anything away
const re = /(\[item:\d+\])/g
// The filter gets rid of any empty strings
const parts = text.split(re).filter(item => item)
return parts.map(part => {
if (part.match(re)) {
// This just converts '[item:24]' to the number 24
return +part.slice(6, -1)
}
return part
})
}
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<template v-for="part in richText">
<chat-item v-if="typeof part === 'number'" :id="part"></chat-item>
<template v-else>{{ part }}</template>
</template>
</div>
If I were going to do it with a render function I'd do it pretty much the same way, just replacing the template with a render function.
If the text parsing requirements were a little more complicated then I wouldn't just return strings and numbers. Instead I'd use objects to describe each part. The core ideas remain the same though.

Vue.js Function called many times

Run the code and look in the console in your browser. You will see function "formatName()" is called many times. Why? I dont update data of race.
But if i change function "amIStarted()" to "return start < 5", then it will be executed 2 times, which is correct.
(sorry my english)
https://jsfiddle.net/a496smx2/48/
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function(){
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [
{name: "John", start: 3},
{name: "Mike", start: 7},
{name: "Gabe", start: 333},
],
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
},
formatName: function(name) {
console.log("I was called.")
return 'Mr. '+name;
}
}
});
<div id="stopwatch" ><h4>Time: <span class="gold" >{{time}}</span></h4></div>
<small>Yellow color means the person started</small>
<div id="race" >
<div v-for="oneStart in startList" :class="{gold : amIStarted(oneStart.start)}" >
{{formatName(oneStart.name)}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
You're looking for computed properties instead of methods. They will be optimized so they are run only when a prop it depends on changes. Methods are run every time an update happens, which could be as often as the mouse moves, depending on your application structure and complexity.
function "formatName()" is called many times. Why?
Because you have added a variable which is continuously changing and checked to add a class :class="{gold : amIStarted(oneStart.start)}" and in each chanve vue reload that part in which function comes and it is calling it again.
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function(){
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [
{name: "John", start: 3},
{name: "Mike", start: 7},
{name: "Gabe", start: 333},
],
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
},
formatName: function(name) {
console.log("I was called.")
return 'Mr. '+name;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="stopwatch" ><h4>Time: <span class="gold" >{{time}}</span></h4></div>
<small>Yellow color means the person started</small>
<div id="race" >
<div>
<div v-for="oneStart in startList" >
{{formatName(oneStart.name)}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
</div>
Here is the flow
https://vuejs.org/images/data.png
There are many other ways to achieve what you want to do. Please check for those.
Functions or filters embedded in a template will be called every time the template is rendered, so every time you update the time, that function will be run (once per name) as part of the rerender.
So long as your methods have no unwanted side effects, this is generally fine! But in cases where you may have a lot of these going at once and start running into performance issues, you can switch to a computed function.
Computed functions will be called only when the data they depend on changes. You can't pass parameters to a computed function, so rather than handling each individual name separately you'd need to have it modify the full list of names in one go:
var stopwatch = new Vue({
el: "#stopwatch",
data: {
time: 1
},
created: function() {
setInterval(function() {
stopwatch.time++;
}, 1000);
}
})
var race = new Vue({
el: "#race",
data: {
startList: [{
name: "John", start: 3
}, {
name: "Mike", start: 7
}, {
name: "Gabe", start: 333
}
]
},
computed: {
formattedList() {
console.log("Computed function ran");
let arr = [];
for (let oneStart of this.startList) {
arr.push({
formattedName: 'Mr. ' + oneStart.name,
start: oneStart.start
})
}
return arr
}
},
methods: {
amIStarted: function(start) {
return start < stopwatch.time;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.min.js"></script>
<div id="stopwatch">
<h4>Time: <span class="gold">{{time}}</span></h4>
</div>
<small>Yellow color means the person started</small>
<div id="race">
<div v-for="oneStart in formattedList" :class="{gold : amIStarted(oneStart.start)}">
{{oneStart.formattedName}} will start when time is more then {{oneStart.start}}
</div>
<br><br>
</div>
An alternative approach would be to modify the data in a beforeMount() or created() block, but this would only be appropriate if you're certain the input data will not change during the lifespan of the component.

Getting a Vue.js computed to run without being part of the DOM

I can't figure out how to get Vue.js to always evaluate a computed regardless of if I'm actually using it in the page. A simplified version of what I'm trying to accomplish is to have a couple input fields which I want to influence the value of another field when either has been updated. I also want this field to be manually editable too. Example jsfiddle.
html:
<div id="app">
<p v-if="updateUsername">Just here to get the darn thing to run</p>
<div>
yourName:<input v-model="yourName">
</div>
<div>
dogsName:<input v-model="dogName">
</div>
<div>
username:<input v-model="userName">
</div>
</div>
js:
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
methods: {
},
computed: {
updateUsername: function(){
this.userName = this.yourName + this.dogName;
}
}
});
This works exactly as I want it to but requires I BS the use of "updateUsername" in the html. I'm sure there's a better way.
You could add a watch:
watch: { updateUsername() {} }
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/2/
But it seems what you want are two watchers instead:
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
watch: {
yourName: {
handler() {
this.userName = this.yourName + this.dogName;
},
immediate: true
},
dogName() {
this.userName = this.yourName + this.dogName;
}
}
});
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/6/
Another option (watching two or more properties simultaneously):
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: ''
},
mounted() {
this.$watch(vm => [vm.yourName, vm.dogName].join(), val => {
this.userName = this.yourName + this.dogName;
}, {immediate: true})
}
});
Fiddle: https://jsfiddle.net/acdcjunior/k6rknwqg/11/
For Vue2, computed properties are cached based on their dependencies. A computed property will only re-evaluate when some of its dependencies have changed.
In your example, computed property=updateUserName will be re-evaluate when either dogName or yourName is changed.
And I think it is not a good idea to update other data in computed property, you will remeber you update userName in computed property=updateUserName now, but after a long time, you may meet some problems why my username is updated accidentally, then you don't remember you update it in one of the computed properties.
Finally, based on your example, I think watch should be better.
You can define three watcher for userName, dogName, yourName, then execute your own logic at there.
var main = new Vue({
el: '#app',
data: {
yourName: 'Adam',
dogName: 'Barkster',
userName: 'Adam Barkster'
},
methods: {
},
computed: {
updateUsername: function(){
return this.yourName + this.dogName;
}
},
watch: {
dogName: function(newValue){
this.userName = this.yourName + ' ' + newValue
},
yourName: function(newValue){
this.userName = newValue + ' '+this.dogName
},
userName: function(newValue) {
// below is one sample, it will update dogName and yourName
// when end user type in something in the <input>.
let temp = newValue.split(' ')
this.yourName = temp[0]
this.dogName = temp[1]
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<div id="app">
<p v-if="updateUsername">Just here to get the darn thing to run</p>
<div>
yourName:<input v-model="yourName">
</div>
<div>
dogsName:<input v-model="dogName">
</div>
<div>
username:<input v-model="userName">
</div>
</div>
</div>

vue.js: Tracking currently selected row

I have a simple table where I would like to handle click elements:
<div class="row"
v-bind:class="{selected: isSelected}"
v-for="scanner in scanners"
v-on:click="scannerFilter">
{{scanner.id}} ...
</div>
JS:
new Vue({
el: "#checkInScannersHolder",
data: {
scanners: [],
loading: true
},
methods: {
scannerFilter: function(event) {
// isSelected for current row
this.isSelected = true;
// unselecting all other rows?
}
}
});
My problem is unselecting all other rows when some row is clicked and selected.
Also, I would be interested to know, it it is possible accessing the scanner via some variable of the callback function instead of using this as I might need to access the current context.
The problem is you have only one variable isSelected using which you want to control all the rows. a better approach will be to have variable: selectedScanner, and set it to selected scanner and use this in v-bind:class like this:
<div class="row"
v-bind:class="{selected: selectedScanner === scanner}"
v-for="scanner in scanners"
v-on:click="scannerFilter(scanner)">
{{scanner.id}} ...
</div>
JS
new Vue({
el: "#checkInScannersHolder",
data: {
scanners: [],
selectedScanner: null,
loading: true
},
methods: {
scannerFilter: function(scanner) {
this.selectedScanner = scanner;
}
}
});
I was under the impression you wanted to be able to selected multiple rows. So here's an answer for that.
this.isSelected isn't tied to just a single scanner here. It is tied to your entire Vue instance.
If you were to make each scanner it's own component your code could pretty much work.
Vue.component('scanner', {
template: '<div class="{ selected: isSelected }" #click="toggle">...</div>',
data: function () {
return {
isSelected: false,
}
},
methods: {
toggle () {
this.isSelected = !this.isSelected
},
},
})
// Your Code without the scannerFilter method...
Then, you can do:
<scanner v-for="scanner in scanners"></scanner>
If you wanted to keep it to a single VM you can keep the selected scanners in an array and toggle the class based on if that element is in the array or not you can add something like this to your Vue instance.
<div
:class="['row', { selected: selectedScanners.indexOf(scanner) !== 1 }]"
v-for="scanner in scanners"
#click="toggle(scanner)">
...
</div>
...
data: {
return {
selectedScanners: [],
...
}
},
methods: {
toggle (scanner) {
var scannerIndex = selectedScanners.indexOf(scanner);
if (scannerIndex !== -1) {
selectedScanners.splice(scannerIndex, 1)
} else {
selectedScanners.push(scanner)
}
},
},
...

Passing data into a Vue template

I am fairly new to vue and can't figure out how to add data values within a template. I am trying to build a very basic form builder. If I click on a button it should add another array of data into a components variable. This is working. The I am doing a v-for to add input fields where some of the attributes are apart of the array for that component. I get it so it will add the input but no values are being passed into the input.
I have created a jsfiddle with where I am stuck at. https://jsfiddle.net/a9koj9gv/2/
<div id="app">
<button #click="add_text_input">New Text Input Field</button>
<my-component v-for="comp in components"></my-component>
<pre>{{ $data | json }}</pre>
</div>
new Vue({
el: "#app",
data: function() {
return {
components: [{
name: "first_name",
showname: "First Name",
type: "text",
required: "false",
fee: "0"
}]
}
},
components: {
'my-component': {
template: '<div>{{ showname }}: <input v-bind:name="name" v-bind:type="type"></div>',
props: ['showname', 'type', 'name']
}
},
methods: {
add_text_input: function() {
var array = {
name: "last_name",
showname: "Last Name",
type: "text",
required: "false",
fee: "0"
};
this.components.push(array);
}
}
})
I appreciate any help as I know I am just missing something obvious.
Thanks
Use props to pass data into the component.
Currently you have <my-component v-for="comp in components"></my-component>, which doesn't bind any props to the component.
Instead, do:
<my-component :showname="comp.showname"
:type="comp.type"
:name="comp.name"
v-for="comp in components"
></my-component>
Here is a fork of your fiddle with the change.
while asemahle got it right, here is a boiled down version on how to expose data to the child component. SFC:
async created() {
await this.setTemplate();
},
methods: {
async setTemplate() {
// const templateString = await axios.get..
this.t = {
template: templateString,
props: ['foo'],
}
},
},
data() {
return {
foo: 'bar',
t: null
}
}
};
</script>
<template>
<component :is="t" :foo="foo"></component>
It pulls a template string that is compiled/transpiled into a js-render-function. In this case with Vite with esm to have a client-side compiler available:
resolve: {
alias: {
// https://github.com/vuejs/core/tree/main/packages/vue#with-a-bundler
vue: "vue/dist/vue.esm-bundler.js",
the index.js bundle size increases by few kb, in my case 30kb (which is minimal)
You could now add some what-if-fail and show-while-loading with defineasynccomponent