Vue - How to get document/window properties bound to a computed property? - javascript

I would like to listen to all focus events on inputs accross my Vue application.
To get the currently focused input, I thought about binding the document.activeElement property to a computed property in my app component, but this is not reactive, why ?
Declaring the activeElement in the data is not reactive either.
Same thing for watchers !
The only way to get it working is by simply returning the value after a focus/blur event on the input itself, but that doesn't suit my needs here.
new Vue({
el: "#app",
data: {
activeElem: document.activeElement.tagName,
realActiveElem: document.activeElement.tagName,
},
methods: {
getActiveElem() {
this.realActiveElem = document.activeElement.tagName;
}
},
computed: {
focused() {
return document.activeElement.tagName;
}
},
watch: {
activeElem(val, oldVal) {
console.log(val !== oldVal);
},
focused(val, oldVal) {
console.log(val !== oldVal);
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h2 #focus="getActiveElem()">
Data: {{activeElem}}
</h2>
<h2>
Computed: {{focused}}
</h2>
<h2>
From function to data: {{realActiveElem}}
</h2>
<input placeholder="Focus/Blur me" id="test" #focus="getActiveElem()" #blur="getActiveElem()" />
</div>
Is there any way to bind document or window properties as reactive ?

Vue can only react to changes made to data, not to the DOM. The change in document.activeElement is a DOM change.
You could use use an event to fire a method to update data. For example:
new Vue({
el: "#app",
data: {
element: ""
},
created() {
document.addEventListener("focusin", this.focusChanged);
},
beforeDestroy() {
document.removeEventListener("focusin", this.focusChanged);
},
methods: {
focusChanged(event) {
this.element = event.target.tagName;
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<input id="mine">
<p>{{ element }}</p>
</div>

Related

Vue state not updated with default injected value

If you click the button, you can see the value of state updated in the console, but it isn't updated in the page output. How can I make it work with a default injected value?
const Component = {
inject: {
state: {
default: () => ({
example: 1
})
}
},
template: `<div>
<div>{{ state }}</div>
<button #click="click">click</button>
</div>`,
methods: {
click() {
this.state.example += 1
console.log(this.state)
}
}
}
new Vue({
el: "#app",
components: {
Component
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component></component>
</div>
Is it related to as Vue docs say "Note: the provide and inject bindings are NOT reactive. This is intentional. However, if you pass down an observed object, properties on that object do remain reactive."? I'm confused about the difference between the BINDINGS not being reactive but the OBSERVED OBJECT being reactive. Could you show an example to demo the difference?
Sorry, but it is not clear what you want - where's the provider for the "injection"? Why do you use inject in the same component as you use the value itself?
Here's your code, without inject:
1. Use the data attribute
const Component = {
data() {
return {
state: {
example: 1
}
}
},
template: `<div>
<div>{{ state }}</div>
<button #click="click">click</button>
</div>`,
methods: {
click() {
this.state.example += 1
console.log(this.state)
}
}
}
new Vue({
el: "#app",
components: {
Component
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component></component>
</div>
Just use the data attribute - you can have a default value for example.
2. Use injection
inject is something completely different - it's a way to pass values from a provider to a consumer:
const Component = {
inject: ['state1'],
data() {
return {
state: {
example: 1
}
}
},
template: `<div>
<div>injected: {{ state1 }}</div>
<div>{{ state }}</div>
<button #click="click">click</button>
</div>`,
methods: {
click() {
this.state.example += 1
console.log(this.state)
}
}
}
new Vue({
el: "#app",
provide: {
state1: {
example1: 1
}
},
components: {
Component
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component></component>
</div>
You can "skip" levels of components and use the provided value in components where you inject it - you don't have to pass it all the way down as props.
3. Create reactive inection
If you want to have reactive injection then you need to pass down something more complex:
const Component1 = {
inject: ['state2'],
data() {
return {
state: {
example: 1
}
}
},
template: `<div>
<div>injected: {{ state2.state2P }}</div>
<div>{{ state }}</div>
<button #click="click">click</button>
</div>`,
methods: {
click() {
this.state.example += 1
console.log(this.state)
}
}
}
new Vue({
el: "#app",
data() {
return {
state2: {
example2: 1
}
}
},
provide() {
// create an object (state2)
const state2 = {}
// define a property on the object (state2P), that
// has a get() function that always gets the provider's
// value you want to inject
Object.defineProperty(state2, 'state2P', {
enumerable: true,
get: () => this.state2,
})
// return the created object (with a property that always
// gets the value in the parent)
return {
state2
}
},
components: {
Component1
},
methods: {
parentClick() {
this.state2.example2 += 1
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<component1></component1>
<button #click="parentClick">PARENT CLICK</button>
</div>
I added a button to the template, so you can see that a method defined in the provider component's scope changes the value displayed in the consumer component's scope. (Also had to change the component's name, as Vue started to "whine" about using a restricted word.)

Is Vue's 'destroyed' method called on page refresh?

I am wondering if refreshing a page that runs a Vue app will trigger the Vue's .destroyed callback.
From what I observed in a Vue app that contains these simple lifecycle callbacks:
created() {
console.log(' created');
},
destroyed() {
console.log('destroyed');
}
only 'created' is logged (not 'destroyed'). How can I check if the .destroyed callback has been executed?
I found the similar question and answer to it on stackoverflow
Do something before reload or close in vue.js
He/she basically explains that nothing is destroyed on page reload, you need to define
window.onbeforeunload = function(){
return "Are you sure you want to close the window?";
}
If you want to do something before a page refresh
As your question was
Is Vue's 'destroyed' method called on page refresh?
No, destroyed method called if your component's controller lost or you manually destroy, above example is for manually destroy.
I have found very good example in vuejs forum which uses externally this.$destroy() method.
new Vue({
el: '#app',
data() {
return {
value: 'will work until destroy'
};
},
methods: {
destroy() {
this.$destroy();
}
},
beforeDestroy() {
console.log('Main Vue destroyed')
}
})
var tmp = Vue.extend({
template: `
<div>
<span>{{ value }}</span>
<input v-model="value" />
</div>
`,
data() {
return {
value: 'always bind and work'
};
},
beforeDestroy() {
console.log('Mounted destroyed')
}
});
new tmp().$mount('#mount-point');
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
{{ value }}
<input v-model="value" />
<div id="mount-point"></div>
<button #click="destroy()">Destroy</button>
</div>
Reference
Another example. If component's control lost or removed then destroy method will be called of that component's
Vue.component('comp1', {
template: '<div>A custom component1!</div>',
destroyed(){
console.log('comp1 destroyed');
}
})
Vue.component('comp2', {
template: '<div>A custom component2!</div>',
destroyed(){
console.log('comp2 destroyed');
}
})
new Vue({
el: '#app',
data() {
return {
value: 1
};
},
methods: {
},
beforeDestroy() {
console.log('Main Vue destroyed')
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.10/vue.js"></script>
<div id="app">
<select v-model="value">
<option value="1">comp1</option>
<option value="2">comp2</option>
</select>
<comp1 v-if="value==1"></comp1>
<comp2 v-if="value==2"></comp2>
<button #click="destroy()">Destroy</button>
</div>

Dynamically changing props

On my app, I have multiple "upload" buttons and I want to display a spinner/loader for that specific button when a user clicks on it. After the upload is complete, I want to remove that spinner/loader.
I have the buttons nested within a component so on the file for the button, I'm receiving a prop from the parent and then storing that locally so the loader doesn't show up for all upload buttons. But when the value changes in the parent, the child is not getting the correct value of the prop.
App.vue:
<template>
<upload-button
:uploadComplete="uploadCompleteBoolean"
#startUpload="upload">
</upload-button>
</template>
<script>
data(){
return {
uploadCompleteBoolean: true
}
},
methods: {
upload(){
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
}
</script>
Button.vue:
<template>
<button
#click="onClick">
<button>
</template>
<script>
props: {
uploadComplete: {
type: Boolean
}
data(){
return {
uploadingComplete: this.uploadComplete
}
},
methods: {
onClick(){
this.uploadingComplete = false
this.$emit('startUpload')
}
</script>
Fixed event name and prop name then it should work.
As Vue Guide: Custom EventName says, Vue recommend always use kebab-case for event names.
so you should use this.$emit('start-upload'), then in the template, uses <upload-button #start-upload="upload"> </upload-button>
As Vue Guide: Props says,
HTML attribute names are case-insensitive, so browsers will interpret
any uppercase characters as lowercase. That means when you’re using
in-DOM templates, camelCased prop names need to use their kebab-cased
(hyphen-delimited) equivalents
so change :uploadComplete="uploadCompleteBoolean" to :upload-complete="uploadCompleteBoolean"
Edit: Just noticed you mentioned data property=uploadingComplete.
It is easy fix, add one watch for props=uploadComplete.
Below is one simple demo:
Vue.config.productionTip = false
Vue.component('upload-button', {
template: `<div> <button #click="onClick">Upload for Data: {{uploadingComplete}} Props: {{uploadComplete}}</button>
</div>`,
props: {
uploadComplete: {
type: Boolean
}
},
data() {
return {
uploadingComplete: this.uploadComplete
}
},
watch: { // watch prop=uploadComplete, if change, sync to data property=uploadingComplete
uploadComplete: function (newVal) {
this.uploadingComplete = newVal
}
},
methods: {
onClick() {
this.uploadingComplete = false
this.$emit('start-upload')
}
}
})
new Vue({
el: '#app',
data() {
return {
uploadCompleteBoolean: true
}
},
methods: {
upload() {
this.uploadCompleteBoolean = false
// do stuff to upload, then when finished,
this.uploadCompleteBoolean = true
},
changeStatus() {
this.uploadCompleteBoolean = !this.uploadCompleteBoolean
}
}
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<button #click="changeStatus()">Toggle Status {{uploadCompleteBoolean}}</button>
<p>Status: {{uploadCompleteBoolean}}</p>
<upload-button :upload-complete="uploadCompleteBoolean" #start-upload="upload">
</upload-button>
</div>
The UploadButton component shouldn't have uploadingComplete as local state (data); this just complicates the component since you're trying to mix the uploadComplete prop and uploadingComplete data.
The visibility of the spinner should be driven by the parent component through the prop, the button itself should not be responsible for controlling the visibility of the spinner through local state in response to clicks of the button.
Just do something like this:
Vue.component('upload-button', {
template: '#upload-button',
props: ['uploading'],
});
new Vue({
el: '#app',
data: {
uploading1: false,
uploading2: false,
},
methods: {
upload1() {
this.uploading1 = true;
setTimeout(() => this.uploading1 = false, Math.random() * 1000);
},
upload2() {
this.uploading2 = true;
setTimeout(() => this.uploading2 = false, Math.random() * 1000);
},
},
});
<script src="https://rawgit.com/vuejs/vue/dev/dist/vue.js"></script>
<div id="app">
<upload-button :uploading="uploading1" #click="upload1">Upload 1</upload-button>
<upload-button :uploading="uploading2" #click="upload2">Upload 2</upload-button>
</div>
<template id="upload-button">
<button #click="$emit('click')">
<template v-if="uploading">Uploading...</template>
<slot v-else></slot>
</button>
</template>
Your question seems little bit ambiguë, You can use watch in that props object inside the child component like this:
watch:{
uploadComplete:{
handler(val){
//val gives you the updated value
}, deep:true
},
}
by adding deep to true it will watch for nested properties in that object, if one of properties changed you ll receive the new prop from val variable
for more information : https://v2.vuejs.org/v2/api/#vm-watch
if not what you wanted, i made a real quick example,
check it out hope this helps : https://jsfiddle.net/K_Younes/64d8mbs1/

Passing the properties of an object as props Vuejs

hi everyone,
I have some confusion. I have two component ( child and parent component) and I pass the properties of an object as props
<child :child-data="abc" ></child>
Vue.component('childComponent', {
props: ['childData'],
data: function(){
return {
count:this.childData,// recommend use Vue.util.extend
}
},
});
Vue will recursively convert "data" properties of childComponent into getter/setters to make it “reactive”.
So why doesn't it automatic bind data to the template? I read some recommend use Vue.util.extend. Why Vue.util.extend?
UPDATE
my example:
https://jsfiddle.net/hoanghung1995/xncs5qpd/56/
when i set default value of parentData ,childDataA will display it. But when i use v-model to override parentData then childDataA not “reactive”. I must use "watch" to override "data" ,similar to childDataB
Vue.util.extend example: https://jsfiddle.net/sm4kx7p9/3/
Why do Vue.util.extend work fine but not use "watch"?,
To explain what is actually happening in the background, Linus Borg has as excellent answer for your question. To summarize his answer, the reason why your approach doesn't work is being data is a computed property while props are being passed in as primitive types. In other words, data makes a copy of your props (instead of passing by reference).
Another way to bypass this is to declare your childData as computed properties instead of data, i.e.:
computed: {
childDataA() {
return this.childPropsA;
},
childDataB() {
return this.childPropsB;
}
}
The reason why using computed works is because the computed properties now watches changes to their dependencies.
A proof-of-concept example based on your original fiddle:
Vue.component('child', {
props: ['childPropsA', 'childPropsB'],
template: "#sub",
computed: {
childDataA() {
return this.childPropsA;
},
childDataB() {
return this.childPropsB;
}
}
});
new Vue({
el: '#app',
data: {
parentData: '123'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
parentData:{{parentData}}<br>
<input type="text" v-model="parentData">
<child :child-props-a="parentData" :child-props-b="parentData"></child>
</div>
<template id="sub">
<div>
<p> 1- {{ childDataA }}</p>
<p> 2- {{ childDataB }}</p>
</div>
</template>
The approach above is functionally identical to the data + watch approach, but I find it rather cumbersome and adds unnecessary verbosity to your code:
data: function() {
return {
childDataA: this.childPropsA,
childDataB: this.childPropsB
};
},
watch: {
childPropsA() {
this.childDataA = this.childPropsA;
},
childPropsB() {
this.childDataB = this.childPropsB;
}
}
Vue.component('child', {
props: ['childPropsA', 'childPropsB'],
template: "#sub",
data: function() {
return {
childDataA: this.childPropsA,
childDataB: this.childPropsB
};
},
watch: {
childPropsA() {
this.childDataA = this.childPropsA;
},
childPropsB() {
this.childDataB = this.childPropsB;
}
}
});
new Vue({
el: '#app',
data: {
parentData: '123'
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
parentData:{{parentData}}<br>
<input type="text" v-model="parentData">
<child :child-props-a="parentData" :child-props-b="parentData"></child>
</div>
<template id="sub">
<div>
<p> 1- {{ childDataA }}</p>
<p> 2- {{ childDataB }}</p>
</div>
</template>
Watch does only watch properties which are indeed reactive. Passing objects doesn`t makes their properties reactive. Just pass the properties that you want as props.
With Vue.util.extend you force the property to be reactive in this instance.

proper use of Vue $refs

Im attempting to recreate this exact inline editing functionality in on of my vue components. However, and I may be wrong, I see some of the syntax is outdated Vue, in particular the v-el directive being used. I've attempted to update the syntax like so:
new Vue({
el: '#app',
data: {
numbers: [{
val: 'one',
edit: false
},
{
val: 'two',
edit: false
},
{
val: 'three',
edit: false
}
]
},
methods: {
toggleEdit: function(ev, number) {
number.edit = !number.edit
// Focus input field
if (number.edit) {
Vue.nextTick(function() {
ev.$refs.input.focus(); // error occurs here
})
}
},
saveEdit: function(ev, number) {
//save your changes
this.toggleEdit(ev, number);
}
}
})
<div id="app">
<template v-for="number in numbers">
<span v-show="!number.edit"
v-on:click="toggleEdit(this, number)">{{number.val}}</span>
<input type="text"
ref="input"
v-model="number.val"
v-show="number.edit"
v-on:blur="saveEdit(ev, number)"> <br>
</template>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
However I get a range of errors... any suggestions on how to properly execute this?
Here is the Error:
[Vue warn]: Error in nextTick: "TypeError: undefined is not an object
(evaluating 'ev.$refs.input')"
Many things changed from Vue.js 1.x to 2.x. I will walk you through the changes necessary in that snippet of yours:
v-repeat should be v-for
Replace v-el="input" with ref="input"
Since you are using ref="input" inside a v-for, then this.$refs.input will be an array of elements, not a single element.
To access each single element, you will need an index (for the array), that's why you should include the index variable in the v-for: v-for="(number, index) in numbers"
Pass the index instead of the ev to the functions, so you can get the<input>s later using vm.$refs.input[index].focus();
And that's pretty much it. After changes you'll get:
new Vue({
el: '#app',
data: {
numbers: [
{
val: 'one',
edit: false
},
{ val: 'two',
edit: false
},
{
val: 'three',
edit: false
}
]
},
methods: {
toggleEdit: function(index, number){
number.edit = !number.edit;
// Focus input field
var vm = this;
if (number.edit){
Vue.nextTick(function() {
vm.$refs.input[index].focus();
})
}
},
saveEdit: function(index, number){
//save your changes
this.toggleEdit(index, number);
}
}
})
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.13/dist/vue.js"></script>
<div id="app">
<template v-for="(number, index) in numbers">
<span v-show="!number.edit"
v-on:click="toggleEdit(index, number)">{{number.val}}</span>
<input type="text"
ref="input"
v-model="number.val"
v-show="number.edit"
v-on:blur="saveEdit(index, number)"> <br>
</template>
</div>
If you want the functionality and not the code design, I'd recommend you redesign it. I think you want to edit data, and the data shouldn't have to know whether it's being edited. That is the role of a component.
So let's make a component that lets you v-model data. The component itself has a span and an input. If you're editing, it shows the input, otherwise, the span. Click starts editing, blur stops editing. When editing starts, set focus on the input.
It takes a value prop. Its input element emits an input event to signal changes (per component v-model spec.
new Vue({
el: '#app',
data: {
stuff: ['one', 'two', 'three']
},
components: {
inlineEditor: {
template: '#inline-editor-template',
props: ['value'],
data() {
return {
editing: false
}
},
methods: {
startEditing() {
this.editing = true;
this.$nextTick(() => this.$refs.input.focus());
},
stopEditing() {
this.editing = false;
}
}
}
}
});
<script src="//unpkg.com/vue#latest/dist/vue.js"></script>
<div id="app">
<inline-editor v-for="item, index in stuff" v-model="stuff[index]"></inline-editor>
</div>
<template id="inline-editor-template">
<div>
<span #click="startEditing" v-show="!editing">{{value}}</span>
<input ref="input" :value="value" #input="e => $emit('input', e.target.value)" #blur="stopEditing" v-show="editing">
</div>
</template>

Categories

Resources