Computed not running when updated from emitted event vue js - javascript

I'm kinda a noob at vue js, however I can't seem to understand why the allValid event isn't being emitted from the following code: (snippet here, full code in the js fiddle)
http://jsfiddle.net/sTX7y/674/
Vue.component('password-validator', {
template: `
<ul>
<regex-validation regex='.{6,}' v-on:valid='v => { isLongEnough = v }' :input='input'>
Is Long Enough
</regex-validation>
<regex-validation regex='[A-Z]' v-on:valid='v => { hasUppercase = v }' :input='input'>
Has Upper
</regex-validation>
</ul>
`,
props: [ 'input' ],
data: function(){
return {
isLongEnough: false,
hasUppercase: false,
}
},
computed:{
isValid: function(){
var valid = this.isLongEnough && this.hasUppercase;
this.$emit('allValid', valid);
return valid;
}
}
});
When viewing this using the vue chrome extension I can clearly see that isLongEnough and hasUppercase both flip from true to false, (and the validation is reflected on the output). It's just that the last isValid computed function just never seems to run...
Thanks for the help and if you see any other noob mistakes feel free to chime in on how I could do this better.

The computed function is defined correctly in the password-validator component. The only problem is you have referenced it ouside of the component scope. i.e. {{ isValid }} is in the html outside of the template. To correct this you can change the password-validator template thus:
template: `
<ul>
<regex-validation regex='.{6,}' v-on:valid='v => { isLongEnough = v }' :input='input'>
Is Long Enough
</regex-validation>
<regex-validation regex='[A-Z]' v-on:valid='v => { hasUppercase = v }' :input='input'>
Has Upper
</regex-validation>
Is Valid: {{ isValid }}
</ul>
now that the reference to the computed property isValid is inside the template, it should update accordingly.
Updated the fiddle here: jsfiddle

Related

Checking if a value or multiple values inside an array of objects have changed and then rerun a method in VueJS

EDIT:
I've updated my watcher and recall the function to update the percentage live, but it's still not verbose enough to check and update all changed objs.
watch: {
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(`changed obj`, changedObjIndex)
this.handleDisplayStatus(changedObjIndex)
console.log(
`obj that changed`,
this.tableData[changedObjIndex].completion_progress
)
},
},
Problem
I have a table that shows the details and progress of a video(s) upload. The progress bar is where I am having trouble updating it live (more on this below).
I have a blank tableData array in my data that is filled when the component is rendered with a handleDisplayStatus function. This method only runs once obviously, so I was planning on putting a watcher on the tableData array and then rerunning the handleDisplayStatus function if there is a change. I initially went with:
this.tableData.every((item) =>
console.log(
`completion progress from watcher`,
item.completion_progress
)
)
However .every only returns a boolean and while it would rerun the handleDisplayStatus function, that function takes an index, _row so that doesn't seem to be the fix.
Also putting this.handleDisplayStatus(index, _row) in the watcher will give this error in the console Error in callback for watcher "tableData": "ReferenceError: index is not defined".
Which makes sense because that function needs the indexes (_row param is not being used I think).
From there I went with .findIndex, but again that just gives me the first element that satisfies the condition. Since the table can have multiple videos loading at the same time, this is not the correct fix either.
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(changedObjIndex)
}
As I mentioned, everything works fine, but I need a page refresh to show the updated number. We are purposely not using socket.io for this; even though I think it's probably a good idea.
Any ideas of how I can get this function to call properly and get the percentages to update in real time? Even the old :key to force a rerender doesn't want to work here.
Any help or guidance would be greatly appreciated!
NOTE:
my progress bar just takes in a percentage prop as a number to render and is pretty straightforward. If you need to see that code as well I can edit this post!
tableData Array of Objs with key/values
(NOTE: the task_status should be: "IN PROGRESS", but I couldn't edit that on the mySQL workbench)
Method that needs to be recalled
handleDisplayStatus(index, _row) {
if (this.tableData[index].task_status == "IN PROGRESS") {
let trainingProcessId = this.tableData[index].training_process_id
this.tableData[index].intervalId = setInterval(() => {
if (
this.tableData[index].completion_progress != 100 &&
this.tableData[index].task_status == "IN PROGRESS"
) {
getTrainingProcessInfo({
trainingProcessId: trainingProcessId,
}).then((res) => {
let jsonObj = JSON.parse(JSON.stringify(res.data))
this.tableData[index].completion_progress =
jsonObj.completion_progress
})
} else {
clearInterval(this.tableData[index].intervalId)
}
}, 6000)
}
return this.tableData[index].task_status
}
template (fwiw)
<template>
<div>
<el-card class="box-card">
<div style="margin-left: 5px" class="_table-wrapper">
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="tableData"
id="el-table"
>
<el-table-column
:label="$t('training_monitor_content.table_column_5')"
align="center"
>
<template slot-scope="scope">
<ProgressBar
:key="scope.row.user"
style="width: 100%; height: 20px; flex-shrink: 0"
:percentage="scope.row.completion_progress"
/>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
If I understood you correctly try with deep watch:
watch: {
tableData: {
handler(newValue) {
newValue.forEach((obj, idx) => {
if (obj.status === 'In Progress') this.handleDisplayStatus(idx)
});
},
deep: true
}
}

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>

How to detect whether an element inside a component is overflown in Vue?

I have a component ResultPill with a tooltip (implemented via vuikit) for the main container. The tooltip text is calculated by a getter function tooltip (I use vue-property-decorator) so the relevant bits are:
<template>
<div class="pill"
v-vk-tooltip="{ title: tooltip, duration: 0, cls: 'some-custom-class uk-active' }"
ref="container"
>
..some content goes here..
</div>
</template>
<script lang="ts">
#Component({ props: ... })
export default class ResultPill extends Vue {
...
get tooltip (): string { ..calcing tooltip here.. }
isContainerSqueezed (): boolean {
const container = this.$refs.container as HTMLElement | undefined;
if(!container) return false;
return container.scrollWidth != container.clientWidth;
}
...
</script>
<style lang="stylus" scoped>
.pill
white-space pre
overflow hidden
text-overflow ellipsis
...
</style>
Now I'm trying to add some content to the tooltip when the component is squeezed by the container's width and hence the overflow styles are applied. Using console, I can roughly check this using $0.scrollWidth == $0.clientWidth (where $0 is the selected element), but when I start tooltip implementation with
get tooltip (): string {
if(this.isContainerSqueezed())
return 'aha!'
I find that for many instances of my component this.$refs.container is undefined so isContainerSqueezed doesn't help really. Do I have to somehow set unique ref per component instance? Are there other problems with this approach? How can I check whether the element is overflown?
PS to check if the non-uniqueness of refs may affect the case, I've tried to add to the class a random id property:
containerId = 'ref' + Math.random();
and use it like this:
:ref="containerId"
>
....
const container = this.$refs[this.containerId] as HTMLElement | undefined;
but it didn't help: still tooltip isn't altered.
And even better, there's the $el property which I can use instead of refs, but that still doesn't help. Looks like the cause is this:
An important note about the ref registration timing: because the refs themselves are created as a result of the render function, you cannot access them on the initial render - they don’t exist yet! $refs is also non-reactive, therefore you should not attempt to use it in templates for data-binding.
(presumably the same is applicable to $el) So I have to somehow recalc tooltip on mount. This question looks like what I need, but the answer is not applicable for my case.
So, like I've mentioned in one of the edits, docs warn that $refs shouldn't be used for initial rendering since they are not defined at that time. So, I've made tooltip a property instead of a getter and calcuate it in mounted:
export default class ResultPill extends Vue {
...
tooltip = '';
calcTooltip () {
// specific logic here is not important, the important bit is this.isContainerSqueezed()
// works correctly at this point
this.tooltip = !this.isContainerSqueezed() ? this.mainTooltip :
this.label + (this.mainTooltip ? '\n\n' + this.mainTooltip : '');
}
get mainTooltip (): string { ..previously used calculation.. }
...
mounted () {
this.calcTooltip()
}
}

vue.js put focus on input

HTML
<span :style="{ display : displayTitle }" #dblclick="showInput()">
{{ node.title }}
</span>
<input :style="{ display : displayTitleInput }" type="text"
#blur="hideInput1" #keydown="hideInput2"
#input="changeTitle(node.id , $event.target.value)"
:value="node.title">
JS
data() {
return {
displayTitle: "inline-block",
displayTitleInput: "none"
};
},
showInput() {
this.displayTitle = "none"
this.displayTitleInput = "inline-block"
},
hideInput1() {
this.displayTitle = "inline-block"
this.displayTitleInput = "none"
},
hideInput2(event) {
if (event.keyCode === 13) {
this.hideInput1()
}
},
I am a beginner Japanese web developer. I am not good at English, sorry.
HTML is in "v-for" (v-for="node in list").
When double click text, it turns to <input>.
I want to make it possible to focus on input when it appears.
I tried this but it didn't work.
HTML
<span :style="{ display : displayTitle }" #dblclick="showInput(node.id)">
{{ node.title }}
</span>
<input :ref='"input_" + node.id' :style="{display:displayTitleInput}" type="text"
#blur="hideInput1" #keydown="hideInput2"
#input="changeTitle(node.id , $event.target.value)"
:value="node.title">
JS
showInput(id) {
this.displayTitle = "none"
this.displayTitleInput = "inline-block"
this.$nextTick(this.$refs["input_" + id][0].focus())
},
There was no error on console, but didn't work.
Your primary problem is that $nextTick takes a callback function but you are executing
this.$refs["input_" + id][0].focus()
immediately. You could get your code working correctly with
this.$nextTick(() => {
this.$refs["input_" + id][0].focus()
})
However, I think you'll run in to further problems and your code can be made much simpler.
One problem you'll find is that all your node inputs will become visible when double-clicking on any of them due to your style rules.
You could instead store an "editing" flag somewhere either on the node or in a separate object.
Below is an example that simplifies your code by...
Using the array-like nature of ref when used within a v-for loop, and
Using the enter modifier on your #keydown event binding
new Vue({
el: '#app',
data: {
list: [
{id: 1, title: 'Node #1'},
{id: 2, title: 'Node #2'}
],
editing: {}
},
methods: {
showInput(id, index) {
this.$set(this.editing, id, true)
this.$nextTick(() => {
this.$refs.input[index].focus()
})
},
hideInput(id) {
this.editing[id] = false
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.min.js"></script>
<ul id="app">
<li v-for="(node, index) in list">
<span v-show="!editing[node.id]" #dblclick="showInput(node.id, index)">
{{ node.title }}
</span>
<input v-show="editing[node.id]" type="text"
ref="input" :value="node.title"
#blur="hideInput(node.id)" #keydown.enter="hideInput(node.id)">
</li>
</ul>
The way you use this.$nextTick(); is incorrect. You should pass it a callback function.
this.$nextTick(function () {
this.$refs["input_" + id].focus()
})
https://jsfiddle.net/un65e9oc/7/
I'm not however sure how that array access is working for you, because as I notice, $refs is an object with the keys referring to the ref name.
[Edit: Thanks to #Phil's comment, above is clear.]
The above is the correct solution for your problem. Since you have already got that answer, I'll add something other than that.
The reason why you see this behavior is that because the reference you hold in $refs doesn't get updated when you change the visibility of the text box in your showInput() method. So when you call this.$refs["input_" + id].focus();, it's actually trying to set focus on a hidden element (because the current reference is not updated).
That's why you need to call the $nextTick() to update it. But if you wanted a quick fix to your problem, without calling $nextTick(), you could update it manually like this:
this.displayTitleInput = "inline-block"
this.$refs["input_" + id].style.display = this.displayTitleInput
this.$refs["input_" + id].focus();
This would also work :) Hope it helps!!
if you want to set focus after click on something and show input text box with set focus with vue js
directives: {
focus: {
// directive definition
inserted: function (el) {
el.focus()
}
}
}
and use custom directive for it. In case you need it should work on click then set with click
directives: {
click: {
// directive definition
inserted: function (el) {
el.focus()
}
}
}
and use it
<input v-focus> or <input v-click>
enter code here
The autofocus attribute is your friend:
<input type="text" autofocus />
It's work for me when we validate the form and want to set dynamically focus on each field
this.$validator.validateAll("FORM_NAME").then(valid => {
var errors = this.$validator.errors;
if (valid) {
console.log('All Fields are valid')
} else {
const errorFieldName = this.$validator.errors.items[0].field;
console.log(errorFieldName);
this.$refs[errorFieldName].focus();
}
});

VueJs manipulate inline template and reinitialize it

this question is similar to VueJS re-compile HTML in an inline-template component and also to How to make Vue js directive working in an appended html element
Unfortunately the solution in that question can't be used anymore for the current VueJS implementation as $compile was removed.
My use case is the following:
I have to use third party code which manipulates the page and fires an event afterwards. Now after that event was fired I would like to let VueJS know that it should reinitialize the current DOM.
(The third party which is written in pure javascript allows an user to add new widgets to a page)
https://jsfiddle.net/5y8c0u2k/
HTML
<div id="app">
<my-input inline-template>
<div class="wrapper">
My inline template<br>
<input v-model="value">
<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>
</div>
</my-input>
</div>
Javascript - VueJS 2.2
Vue.component('my-input', {
data() {
return {
value: 1000
};
}
});
Vue.component('my-element', {
props: {
value: String
},
methods: {
click() {
console.log('Clicked the button');
}
}
});
new Vue({
el: '#app',
});
// Pseudo code
setInterval(() => {
// Third party library adds html:
var newContent = document.createElement('div');
newContent.innerHTML = `<my-element inline-template :value="value">
<button v-text="value" #click="click"></button>
</my-element>`; document.querySelector('.wrapper').appendChild(newContent)
//
// How would I now reinialize the app or
// the wrapping component to use the click handler and value?
//
}, 5000)
After further investigation I reached out to the VueJs team and got the feedback that the following approach could be a valid solution:
/**
* Content change handler
*/
function handleContentChange() {
const inlineTemplates = document.querySelector('[inline-template]');
for (var inlineTemplate of inlineTemplates) {
processNewElement(inlineTemplate);
}
}
/**
* Tell vue to initialize a new element
*/
function processNewElement(element) {
const vue = getClosestVueInstance(element);
new Vue({
el: element,
data: vue.$data
});
}
/**
* Returns the __vue__ instance of the next element up the dom tree
*/
function getClosestVueInstance(element) {
if (element) {
return element.__vue__ || getClosestVueInstance(element.parentElement);
}
}
You can try it in the following fiddle
Generally when I hear questions like this, they seem to always be resolved by using some of Vue's more intimate and obscured inner beauty :)
I have used quite a few third party libs that 'insist on owning the data', which they use to modify the DOM - but if you can use these events, you can proxy the changes to a Vue owned object - or, if you can't have a vue-owned object, you can observe an independent data structure through computed properties.
window.someObjectINeedtoObserve = {...}
yourLib.on('someEvent', (data) => {
// affect someObjectINeedtoObserve...
})
new Vue ({
// ...
computed: {
myObject () {
// object now observed and bound and the dom will react to changes
return window.someObjectINeedtoObserve
}
}
})
If you could clarify the use case and libraries, we might be able to help more.

Categories

Resources