vue.js computed beginner - javascript

I am trying to learn vue.js despite not having any background with javascript. I ran into some code when following a video that was teaching about 'computed', and I tried experimenting on it and had a bit of trouble along the way.
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<button #click="showToggle1">Switch!</button>
</div>
new Vue({
el:'#app',
data:{
show = true;
},
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
methods:{
showToggle2:function(){
this.show = !this.show;
}
});
Basically it's making "Do you also see me?" disappear and appear depending on the value of "show". I know that if you write #click:'showToggle2()' instead of #click:'showToggle1' at the button, the value changes and it works. I'm just having some trouble understanding how computed works and why showToggle1 doesn't change the value of show when I click the button

Some problems.
First, you have syntactical problems. data is an object, so instead of:
data:{
show = true;
}
Should be:
data:{
show: true
}
Next, computed properties are to be used like... properties. For example, like declared in data. So, typicall, you read from them. You don't execute computed properties in #click events. So this code:
<button #click="showToggle1">Switch!</button>
Is not correct. It will error because showToggle1 is not a method, it is, as said, a computed property. What you should have in click is a method, like:
<button #click="showToggle2">Switch!</button>
This will work because showToggle2 is a method. And you should use methods to perform changes.
Not, before going into the last and most tricky part, here's a working demo:
new Vue({
el: '#app',
data: {
show: true
},
computed: {
/*showToggle1: function() {
return this.show = !this.show;
}*/
},
methods: {
showToggle2: function() {
this.show = !this.show;
}
}
});
<script src="https://unpkg.com/vue"></script>
<div id='app'>
<p>Do you see me?</p>
<p v-if="show">Do you also see me?</p>
<hr>
Value of show: {{ show }}<br>
<button #click="showToggle2">Switch2!</button>
</div>
The tricky part is your computed property (which I commented out in the code above):
computed:{
showToggle1:function(){
return this.show = !this.show
}
},
Basically what it is doing is it is automatically negating the value of show whenever it changes.
This happens because the computed property is calculated whenever show updates. And what is happening is:
You initialize data with true (because of data: {show: true}).
The showToggle1 computed auto-recalculates, because it has this.show inside of it (it depends on it).
When recalculating, showToggle1 sets the value of show to false (because of return this.show = !this.show).
That's why show becomes false.
And that's also why whenever you change (even from the method, which is the correct place) the value of show to true it will automatically go back to false. Because any change in show triggers the showToggle1 computed recalculation, which sets show back to false.
In summary:
Use methods to perform changes.
Don't change properties inside computed properties.

Related

How to make method calls and v-shows work from outside Vue

We are using Vue.js to implement the popup function.
The "Click" button in the Vue App will launch the popup correctly, but the following example
vueApp.methods.openModal() /// call from outside
The popup does not appear when I call from outside, although the function call itself is possible. Why is this? Also, how can I make v-show work properly when calling a method from outside Vue like this?
xxx.html
<div id="app">
<button v-on:click="openModal">Click</button>
<div id="dbg_overlay" v-show="showContent" v-on:click="closeModal">
<div id="content"></div>
<button v-on:click="closeModal">Close</button>
</div>
</div>
xxx.js
const vueApp = {
data() {
return {
showContent: false,
}
},
methods: {
openModal: function() {
this.showContent = true
},
closeModal: function() {
this.showContent = false
}
},
};
Vue.createApp(lessonScheduleEdit).mount("#app");
vueApp.methods.openModal() /// call from outside
vue I am using :
<script src="https://unpkg.com/vue#next"></script>
You need the following:
assign your Vue app to a variable, available anywhere in global scope.
once you make it available, to call a method defined as methodName in its methods, you have to call it directly (e.g: myVar.methodName(), not as myVar.methods.methodName() !!!)
How to make the variable globally available:
The most common method to declare globally available variables would be to use the window object:
window.myVueApp = Vue.createApp(vueApp).mount("#app");
// from anywhere else in the window:
window.myVueApp.openModal();
A more modern way of making a variable globally available is to use window's alias: globalThis:
globalThis.myVueApp = Vue.createApp(vueApp).mount('#app');
// both of these will now work
// because, inside a browser, globalThis === window
window.myVueApp.openModal();
globalThis.myVueApp.openModal();
Note: can I use globalThis?
But, even though window now has a fancy new alias, using it is just as bad (read why below).
Note: Polluting the global scope with variables is generally regarded as bad practice, as it runs the risk of name collisions (when both your code and someone else's, used on same page, accidentally use the same global scope variable names).
Ultimately, this means that, at least in theory, your code is unreliable (it might fail under certain circumstances). Ideally you want to write unbreakable code, which does not interfere with (break) anything else running on same page, at any point.
To minimize this naming collision risk, you could actually place your app inside the Vue namespace, which is already available in global scope, as we already know the Vue namespace does not have a myVueApp property (and the likelihood that anyone else would also use Vue.myVueApp on your page is negligible):
Vue.myVueApp = // ...your app here...
See it working:
const vueApp = {
data() {
return {
showContent: false,
}
},
methods: {
openModal: function() {
this.showContent = true
},
closeModal: function() {
this.showContent = false
}
},
};
Vue.myModeratelyAmazingVueApp = Vue.createApp(vueApp).mount("#app");
<script src="https://unpkg.com/vue#next/dist/vue.global.prod.js"></script>
<div id="app">
<button #click="openModal">Click</button>
<div id="dbg_overlay" v-show="showContent" #click="closeModal">
<div id="content"></div>
<button #click="closeModal">Close</button>
</div>
</div>
<button onclick="Vue.myModeratelyAmazingVueApp.openModal()">external open</button>
The easiest way is to use v-if and check if your defined value is equal to true. Like this:
<div id="app">
<button id="show-modal" #click="showModal = true">Show Modal</button>
<modal v-if="showModal" #close="showModal = false">
<!-- Your content in here -->
</modal>
</div>
you can also do showModal = true inside of your methods, just like this:
methods: {
clickModal() { //just renamed in template it's #click="clickModal()"
showModal = true;
}
}
Hopefully this helps you out!

Vue Refresh page with realtime data

I'm building some app using Laravel & Vue, and so far so good, but I'm no expert with Vue.
So I have one very "begginers" problem, using live data.
So I want to make button that will check if live data is on or off, and if they turn it on, it must refresh data and set liveData state to true.
For example:
This is my "button" and it's not working as expected, It will change state but data is still no live
<div v-if="liveData">
<div #click="liveData = false">
Turn OFF Live data
</div>
</div>
<div v-else="liveData">
<div #click="liveData = true">
Turn On Live data
</div>
</div>
I have defined state like so:
data() {
return {
liveData: false
}
},
And this is my created() function:
created() {
if(this.liveData){
window.Echo.channel("addOrder").listen(".order-created", (order) => {
this.$store.commit("ADD_ORDER", order);
});
}
this.$store.dispatch("GET_ORDERS");
},
So in this case only button is not working, but if I set state to true it's working perfectly.
What do I need to do here? Do I need to make new function to work or?
Created will only be executed once in your component lifecycle. At this point the value of liveData is always false.
If you click on your "button" the value should change but your code inside of created will not be executed once more.
Instead of created you can use an immediate watcher:
watch: {
liveData: {
immediate: true,
handler(val) {
// your code from created here
}
}
Correct the mistake
<div v-else!="liveData">
<div v-if="liveData">
<div #click="liveData = false">
Turn OFF Live data
</div>
</div>
<div v-else!="liveData">
<div #click="liveData = true">
Turn On Live data
</div>
</div>
or
<div v-else>

How to generate Vue components from Javascript

I'm new to Vue.js (with a background in Computer Science and programming, including interactive Javascript webpages) and as I'm a teacher, I have a quiz site I use to give homework to my students.
My codebase is messy, so I decided to migrate the whole thing to Vue, with the idea that I could use a component for each individual type of question -- separation of concerns, and all that.
However, I can't seem to find a way to generate appropriate components on the fly and include them in my page.
Here's a simplified version of my framework, with two question types. If I include the components directly in the HTML, they work fine.
Vue.component("Freetext",{
props: ["prompt","solution"],
data : function() {return {
response:""
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1> <p><input type="text" v-model="response"></input></p> <p><button class="LG_checkbutton" #click="check()">Check</button></p></span>'
})
Vue.component("multi",{
props : { prompt: String,
options : Array,
key_index : Number // index of correct answer
},
data : function() {return {
response:""
}},
methods : {
check : function (k) {
if (k == this.key_index) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'<span><h1>{{prompt}}</h1><button v-for="(v,k) in options" #click="check(k)">{{v}}</button></span>'
})
</script>
<div id="app">
<Freetext prompt="Type 'correct'." solution="correct"></freetext>
<multi prompt="Click the right answer." :options='["right","wrong","very wrong"]' :key_index=0></multi>
</div>
<script>
var app = new Vue({
el: "#app",
data : {
questions:[ {type:"Multi",
prompt: "Click the right answer.",
options:["right","wrong","very wrong"],
key:0},
{type:"Freetext",
prompt:"Type 'correct'.",
solution:"correct"}
],
question_number:0
},
methods : {
nextQuestion : function () {
this.question_number ++;
}
}
})
</script>
But what I want to do is generate the contents of the div app on the fly, based on using the data member app.question_number as an index to app.questions, and the .type member of the question indicated (i.e. app.questions[app.question_number].type)
If I try to make the app of the form:
{{question}}
</div>
<script>
//...
computed : {
question : function () {
var typ = this.questions[this.question_number].type;
return "<"+typ+"></"+typ+">";
}
...I just get as plain text, and it isn't parsed as HTML.
If I try document.getElementById("app").innerHTML = "<multi prompt='sdf'></multi>"; from the console, the tag shows up in the DOM inspector, and isn't processed by Vue, even if I call app.$forceUpdate().
Is there any way round this?
While Keith's answer works for most of what I need to do, there's another way to handle this that I've just found out about, which I thought I'd share in case anyone else is looking for it: giving a block level HTML element a v-html property.
For me, this is handy as a short term fix as I'm migrating a codebase that generates dynamic HTML as strings, and I can quickly integrate some of my existing code without reworking it completely.
For example, I have a function makeTimetable that takes a custom datastructure representing a week's actively and turns it into a table with days across the top and times down the left-hand side, setting appropriate rowspans for all the activities. (It's a bit of a convoluted function, but it does what I need and isn't really worth refactoring at this point.)
So I can use this as follows:
<script type="text/x-template" id="freetext-template">
<span>
<div v-html="tabulated_timetable"></div>
<p>{{prompt}}</p>
<p><input type="text" v-model="response"></input></p>
<p><button class="LG_checkbutton" #click="check()">Check</button></p>
</span>
</script>
<script>
var freetext = Vue.component("Freetext",{
props: {"prompt":String,
"timetable":Object,
"solution":String,
data : function() {return {
response:""
}},
computed : {
tabulated_timetable : function () {
return makeTimetable (this.timetable);
}},
methods : {
check : function () {
if (this.solution == this.response) {
alert ("Correct!");
app.nextQuestion();
} else {
alert ("Try again!");
}
}
},
template:'#freetext-template'
})
</script>
(I suppose I could put `tabulated_timetable` in `methods` rather than `computed`, as it's set once and never changed, but I don't know if there would be any performance benefit to doing it that way.)
I think maybe a slightly different approach, Vue supports the concept of "dynamic components"
see https://v2.vuejs.org/v2/guide/components-dynamic-async.html
this will let you define what component to use on each question which would look something like
<component v-bind:is="question.component" :question="question"></component>

setTimeout on a high volume data

I have tried googling and searching entirety of stack overflow for this question but I think it boils down to the keywords I'm using to search.
Basically my problem boils down to the following: when the cursor leaves an element, wait 500 milliseconds before closing the element. Before close the element, check if the cursor is back in the element, and if its not, do not hide it.
I'm using vuejs to do this but I boiled down the problem to being in setTimeout function. The part where I have the code is fairly complex to post it here, therefore I created a simple POC to demonstrate the problem:
<template>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
components: {},
methods: {
handleMouseEnter(index) {
setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
return {
showBox: []
};
},
created() {
for (let i = 0; i <= 2000; i++) {
this.showBox[i] = false;
}
}
};
</script>
You can checkout the sandbox here: https://codesandbox.io/s/cold-river-ruz7b
If you hover over from top to bottom in a moderate speed you will realize that even after leaving the li element the red box stays.
I guess the problem lays in the fact that handleMouseEnter is being called with a setTimeout and the handleMouseLeave is not. Therefore, making handleMouseEnter be executed after handleMouseLeave therefore showing the box.
Any light would be highly appreciated here and if a short explanation could be given on why the problem is happening it would be great
Your example seems to operate the opposite way round to the original problem description (the timer is on showing not hiding) but I think I get what you mean.
As you suggest, the problem is that the timer callback is being called after the mouseleave event fires. So the red box does get hidden but shortly thereafter the timer fires and brings it back.
In the example below I have simply cancelled the timer using clearTimeout. In general it might be necessary to store an array of such timers, one for each element, but in this specific example I believe it only makes sense to have one timer active at once so I can get away without an array.
I also moved the initial population of showBox into data. There seemed no reason to use a created hook here.
There's no need to copy the whole array each time, you can just use $set to set the value in the existing array. I haven't changed that in my example.
I would also note that for this particular example you don't need an array to hold all the showBox values. Only one red box can be visible at once so you only need a single property to hold the index of the currently visible box. I haven't changed this in my example as I suspect your real use case is not as straight forward as this.
new Vue({
el: '#app',
methods: {
handleMouseEnter(index) {
this.currentTimer = setTimeout(() => {
let showBox = [...this.showBox];
showBox[index] = true;
this.showBox = showBox;
}, 500);
},
handleMouseLeave(index) {
clearTimeout(this.currentTimer)
let showBox = [...this.showBox];
showBox[index] = false;
this.showBox = showBox;
}
},
data() {
const showBox = [];
for (let i = 0; i <= 2000; i++) {
showBox[i] = false;
}
return {
showBox
};
}
})
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<ul v-for="x in 2000" :key="x">
<li #mouseenter="handleMouseEnter(x)" #mouseleave="handleMouseLeave(x)" style="height: 50px;">
Hello
<span style="background-color: red" v-show="showBox[x]">BOX</span>
</li>
</ul>
</div>

How to make focus() work before the DOM element shown

UPDATE: it turns out there is no way to directly apply focus to textbox before DOM rendering finished
I wonder why the focus() function does not work on hidden element.
For example( I am using Vue.js ):
var vm = new Vue({
el: "#app",
data:{
showtext: false
},
methods: {
showTxt(ev){
this.showtext = true
var vm = this;
// if I uncomment setTimeout, then the textbox can set focus
//setTimeout(function(){
vm.$refs.textbox.focus()
//}, 0)
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<button #click="showTxt">
Show Textbox and SetFocus on it
</button>
<div v-show="showtext">
<input ref="textbox" type="text" />
</div>
</div>
What I am trying to do is to click that button and show textbox and put focus in textbox, but currently, the textbox can not get focus if I directly call .focus(). It only works when I wrap a setTimeout around it(which I guess run on the next event loop). I wonder if there is any way to make the focus working without setTimeout?
Thanks
Preferable to use vm.$nextTick or Vue.nextTick (We don't need to care about nextTick even actually uses setTimeout, Vue will guarantee nextTick will do its job, even in future nextTick may use other approaches implement same goal).
As Vue API: nextTick says,
Defer the callback to be executed after the next DOM update cycle. Use
it immediately after you’ve changed some data to wait for the DOM
update.
Also you can check Vue Guide: Async Update Queue
For your case, when click the button to show the input, it will execute this.showtext=true, then execute element.focus. But actually the Dom element is still invisible (VNode is changed, but Vue hasn't re-render&patch).
So you have to use vm.$nextTick or Vue.nextTick to execute .focus after Vue re-render that input out.
Check the demo below:
Vue.config.productionTip = false
var vm = new Vue({
el: "#app",
data:{
showtext: false
},
methods: {
showTxt(ev){
this.showtext = true
var vm = this;
console.log('Current:', this.$el.innerHTML)
this.$nextTick(()=>{
vm.$refs.textbox.focus()
console.log('Nexttick:', this.$el.innerHTML)
})
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<button #click="showTxt">
Show Textbox and SetFocus on it
</button>
<div v-show="showtext">
<input ref="textbox" type="text" />
</div>
</div>
Why setTimeout works:
because setTimeout(,0) adds one task to the task-queue. But it will be executed at next event loop, but render will be exeucted after execute micro-tasks in current event loop.
Check HTML SPEC: event loop processing model (please look into Step 7), after current task (including this.showtext to true, data reactivity triggers re-render) already executed (it will be removed from task queue), the system will render first before pop one task (probably is setTimeout(,0) if setTimeout(,0) task is the oldest task) from the queue.
But Promise is micro-task, it will not work because it will be executed before render (please look into above event loop processing model: step 6).
Vue.config.productionTip = false
var vm = new Vue({
el: "#app",
data:{
showtext: false
},
methods: {
showTxt(ev){
this.showtext = true
var vm = this;
new Promise((resolve, reject)=>{
vm.$refs.textbox.focus()
resolve()
}).then(()=>{
vm.$refs.textbox.focus()
})
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<button #click="showTxt">
Show Textbox and SetFocus on it
</button>
<div v-show="showtext">
<input ref="textbox" type="text" />
</div>
</div>
Or you can look into this Youtube Video, it will decribe better than mine.
Hidden elements cannot get focus by the nature of focus entity concept. Only visible and enabled ones. In any UI system, HTML/CSS DOM included.
You could get rid of setTimeout only if you use other means of setting focus, for example autofocus attribute. Without it you will have to separate setting showtext flag and call for focus into separate event "ticks".
var vm = new Vue({
el: "#app",
data:{
showtext: false
},
methods: {
showTxt(ev){
this.showtext = true
this.$refs.textbox.focus()
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<button #click="showTxt">
Show Textbox and SetFocus on it
</button>
<div v-show="showtext">
<input ref="textbox" autofocus type="text" />
</div>
</div>

Categories

Resources