How to address the data of a component from within that component? - vue.js

In a standalone Vue.js script I can mix functions and Vue data:
var vm = new Vue ({
(...)
data: {
number: 0
}
(...)
})
function return100 () {
return 100
}
vm.number = return100()
I therefore have a Vue instance (vm) which data is directly addressable via vm.<a data variable>)
How does such an addressing works in a component, since no instance of Vue is explicitly instantiated?
// the component file
<template>
(...)
</template>
<script>
function return100 () {
return 100
}
export default {
data: function () {
return {
number: 0
}
}
}
// here I would like to set number in data to what return100()
// will return
??? = return100()
</script>

You can achieve the target by using code like this.
<template>
<div>{{ name }}</div>
</template>
<script>
const vm = {
data() {
return {
name: 'hello'
};
}
};
// here you can modify the vm object
(function() {
vm.data = function() {
return {
name: 'world'
};
}
})();
export { vm as default };
</script>
But I really don't suggest you to modify data in this way and I think it could be considered as an anti-pattern in Vuejs.
In almost all the use cases I met, things could be done by using Vue's lifecycle.
For example, I prefer to write code with the style showed below.
<template>
<div>{{ name }}</div>
</template>
<script>
export default {
data() {
return {
name: 'hello'
};
},
mounted() {
// name will be changed when this instance mounted into HTML element
const vm = this;
(function() {
vm.name = 'world';
})();
}
};
</script>

Related

How watch global variable in component vuejs?

I need global variables for errors. But I don't want set input variable for every component.
How I can watch $errors in component ABC without input variable?
(without <abc :errors="$errors"></abc>)
index.js:
Vue.prototype.$errors = {};
new Vue({
el: '#app',
render: h => h(App),
}
App.vue:
...
name: 'App',
components: {
ABC
}
...
methods:{
getContent() {
this.$errors = ...from axis...
}
Component ABC:
<template>
<div>{{ error }}</div>
</template>
...
watch: {
???
}
Here's an example of how it could be done:
const errors = Vue.observable({ errors: {} })
Object.defineProperty(Vue.prototype, '$errors', {
get () {
return errors.errors
},
set (value) {
errors.errors = value
}
})
new Vue({
el: '#app',
methods: {
newErrors () {
// Generate some random errors
const errors = {}
for (const property of ['name', 'type', 'id']) {
if (Math.random() < 0.5) {
errors[property] = 'Invalid value'
}
}
this.$errors = errors
}
}
})
new Vue({
el: '#app2',
watch: {
$errors () {
console.log('$errors has changed')
}
}
});
<script src="https://unpkg.com/vue#2.6.10/dist/vue.js"></script>
<div id="app">
<pre>{{ $errors }}</pre>
<button #click="newErrors">New errors</button>
</div>
<div id="app2">
<pre>{{ $errors }}</pre>
</div>
I've created two Vue instances to illustrate that the value really is shared. Clicking the button in the first instance will update the value of $errors and the watch is triggered in the second instance.
There are a few tricks in play here.
Firstly, reactivity can only track the reading and writing of properties of an observable object. So the first thing we do is create a suitable object:
const errors = Vue.observable({ errors: {} })
We then need to wire this up to Vue.prototype.$errors. By defining a get and set for that property we can proxy through to the underlying property within our observable object.
All of this is pretty close to how data properties work behind the scenes. For the data properties the observable object is called $data. Vue then uses defineProperty with get and set to proxy though from the Vue instance to the $data object, just like in my example.
as Estradiaz said:
You can use Vuex and access the value outside of Vue like in this answer: https://stackoverflow.com/a/47575742/10219239
This is an addition to Skirtles answer:
You can access such variables via Vue.prototype.variable.
You can set them directly, or use Vue.set, it works either way.
My code (basically the same as Skirtless):
main.js
const mobile = Vue.observable({ mobile: {} });
Object.defineProperty(Vue.prototype, '$mobile', {
get() { return mobile.mobile; },
set(value) { mobile.mobile = value; }
});
function widthChanged() {
if (window.innerWidth <= 768) {
if (!Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', true);
} else if (Vue.prototype.$mobile) Vue.set(Vue.prototype, '$mobile', false);
}
window.addEventListener("resize", widthChanged);
widthChanged();
Home.vue:
watch: {
'$mobile'(newValue) {
// react to Change in width
}
}

Vue model not updating

When I try to update my custom text-area component's model data this.message='<span id="foo">bar</span> the text and html does not display in the htmltextarea tag like it should, but I can see the update applied in the Vue dev tool's console. I've also tried switching to an object instead of a string and using Vue.set, but this does not work either.
Any suggestions on how to fix this?
The goal with the htmlTextArea component is to get the users text from the htmlTextArea tag (this works), manipulate this text and bind it back to the textarea, but with HTML in it.
Custom text-area component:
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: 'htmlTextArea',
props:['value'],
mounted: function () {
this.$el.innerHTML = this.value;
},
methods: {
updateHTML: function(e) {
this.$emit('input', e.target.innerHTML);
}
}
}
</script>
Other component:
<template>
...
<htmlTextArea id="textarea" v-model="message"></htmlTextArea>
...
</template>
<script>
data: {
return {
message: 'something'//this works
}
}
...
methods: {
changeText() {
this.message='<span id="foo">bar</span>'//this does not
}
},
components: {
htmlTextArea
}
</script>
You need to set the value explicitly after the value props change. you can watch for value change.
<template>
<div contenteditable="true" #input="updateHTML" class="textareaRoot"></div>
</template>
<script>
export default {
// Custom textarea
name: "htmlTextArea",
props: ["value"],
mounted: function() {
this.$el.innerHTML = this.value;
},
watch: {
value(v) {
this.$el.innerHTML = v;
}
},
methods: {
updateHTML: function(e) {
this.$emit("input", e.target.innerHTML);
}
}
};
</script>
Change the data property into a function, as you have it defined it is not reactive.
data () {
return {
message: 'something'//this works
}
}
Now when you update the message property in your method, the component will update accordingly.
Reactivity in depth

Is that the right way to set an id in Vue.js component?

I am trying to integrate Phaser 3 with Vue.js 2.
My goal is to create a Vue.js component associated with a game canvas.
My initial solution was:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
mounted () {
this.id = 'game' + this._uid
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
This code attach the game canvas to my template. But to my surprise it only worked 'sometimes'.
I figured out, after hours of debugging, that my div element in the DOM wasn't updated with the id when I was instantiating my new Game.
So I came up with the solution of instantiating the id in the beforeMount () method as follow:
<template>
<div :id="id">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
id: null,
game: null
}
},
beforeMount () {
this.id = 'game' + this._uid
},
mounted () {
var config = {
parent: this.id,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
It is working, but I would like to know if there is a more simple and elegant solution ?
One better solution for integrating Phaser.Game into the application is directly passing the config the HTML element, a configuration supported by Phaser.Game.
To get a reference to a HTML element in vue, you can use refs, these are basically id's, but local to the component itself, so there is not risk in creating conflicts.
<template>
<div ref="myDiv">
</div>
</template>
<script>
import Phaser from 'phaser'
export default {
data () {
return {
game: null
}
},
mounted () {
var config = {
parent: this.$refs.myDiv,
type: Phaser.CANVAS
}
this.game = new Phaser.Game(config)
....
}
}
</script>
Vue3 sample:
<script setup>
import { ref,onMounted } from 'vue';
import Phaser from 'phaser'
const myDiv = ref(null)
let canvasWidth = 750;
let canvasHeight = 1450;
onMounted(() => {
const config = {
type: Phaser.AUTO,
parent: popWrap.value,
width: canvasWidth,
height: canvasHeight,
scene: {
preload: preload,
create: create,
update: update
}
};
const game = new Phaser.Game(config);
})
</script>
<template>
<div ref="myDiv">
</div>
</template>

How can I test a custom input Vue component

In the Vue.js documentation, there is an example of a custom input component. I'm trying to figure out how I can write a unit test for a component like that. Usage of the component would look like this
<currency-input v-model="price"></currency-input>
The full implementation can be found at https://v2.vuejs.org/v2/guide/components.html#Form-Input-Components-using-Custom-Events
The documentation says
So for a component to work with v-model, it should (these can be configured in 2.2.0+):
accept a value prop
emit an input event with the new value
How do I write a unit test that ensures that I've written this component such that it will work with v-model? Ideally, I don't want to specifically test for those two conditions, I want to test the behavior that when the value changes within the component, it also changes in the model.
You can do it:
Using Vue Test Utils, and
Mounting a parent element that uses <currency-input>
Fake an input event to the inner text field of <currency-input> with a value that it transforms (13.467 is transformed by <currency-input> to 13.46)
Verify if, in the parent, the price property (bound to v-model) has changed.
Example code (using Mocha):
import { mount } from '#vue/test-utils'
import CurrencyInput from '#/components/CurrencyInput.vue'
describe('CurrencyInput.vue', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data: { price: null },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
})
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
In-browser runnable demo using Jasmine:
var CurrencyInput = Vue.component('currency-input', {
template: '\
<span>\
$\
<input\
ref="input"\
v-bind:value="value"\
v-on:input="updateValue($event.target.value)">\
</span>\
',
props: ['value'],
methods: {
// Instead of updating the value directly, this
// method is used to format and place constraints
// on the input's value
updateValue: function(value) {
var formattedValue = value
// Remove whitespace on either side
.trim()
// Shorten to 2 decimal places
.slice(0, value.indexOf('.') === -1 ? value.length : value.indexOf('.') + 3)
// If the value was not already normalized,
// manually override it to conform
if (formattedValue !== value) {
this.$refs.input.value = formattedValue
}
// Emit the number value through the input event
this.$emit('input', Number(formattedValue))
}
}
});
// specs code ///////////////////////////////////////////////////////////
var mount = vueTestUtils.mount;
describe('CurrencyInput', () => {
it("changing the element's value, updates the v-model", () => {
var parent = mount({
data() { return { price: null } },
template: '<div> <currency-input v-model="price"></currency-input> </div>',
components: { 'currency-input': CurrencyInput }
});
var currencyInputInnerTextField = parent.find('input');
currencyInputInnerTextField.element.value = 13.467;
currencyInputInnerTextField.trigger('input');
expect(parent.vm.price).toBe(13.46);
});
});
// load jasmine htmlReporter
(function() {
var env = jasmine.getEnv()
env.addReporter(new jasmine.HtmlReporter())
env.execute()
}())
<link rel="stylesheet" type="text/css" href="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.css">
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine.js"></script>
<script src="https://cdn.jsdelivr.net/jasmine/1.3.1/jasmine-html.js"></script>
<script src="https://npmcdn.com/vue#2.5.15/dist/vue.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue-template-compiler#2.5.15/browser.js"></script>
<script src="https://rawgit.com/vuejs/vue-test-utils/2b078c68293a41d68a0a98393f497d0b0031f41a/dist/vue-test-utils.iife.js"></script>
Note: The code above works fine (as you can see), but there can be improvements to tests involving v-model soon. Follow this issue for up-to-date info.
I would also mount a parent element that uses the component. Below a newer example with Jest and Vue Test Utils. Check the Vue documentation for more information.
import { mount } from "#vue/test-utils";
import Input from "Input.vue";
describe('Input.vue', () => {
test('changing the input element value updates the v-model', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Brendan Eich';
await wrapper.find('input').setValue(name);
expect(wrapper.vm.$data.name).toBe(name);
});
test('changing the v-model updates the input element value', async () => {
const wrapper = mount({
data() {
return { name: '' };
},
template: '<Input v-model="name" />',
components: { Input },
});
const name = 'Bjarne Stroustrup';
await wrapper.setData({ name });
const inputElement = wrapper.find('input').element;
expect(inputElement.value).toBe(name);
});
});
Input.vue component:
<template>
<input :value="$attrs.value" #input="$emit('input', $event.target.value)" />
</template>

Call method from another component in Vue.js

How do I call the method from another component in this scenario? I would like to load additional pice of data from the API once the button is clicked in the component 1 to the component 2.
Thanks
Here are my two components in the seperate files:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
methods: {
buttonClicked: function () {
//call changeName here
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
You can name the component and then $ref to the method from another componenent.
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: "compbutton",
methods: {
buttonClicked: function() {
//call changeName here
this.$root.$refs.compname_component.changeName();
}
}
};
</script>
compname.vue
<template>
<div>{{name}}</div>
</template>
<script>
export default {
name: "compname",
data: function() {
return {
name: "John"
};
},
methods: {
changeName: function() {
this.name = "Ben";
}
},
created() {
// set componenent name
this.$root.$refs.compname_component = this;
}
};
</script>
Alternative answer: you can pass the function you want the child to invoke as a prop from the parent component. Using your example:
compbutton.vue
<template>
<div>
<a href v-on:click="buttonClicked">Change Name</a>
</div>
</template>
<script>
export default {
name: 'compbutton',
props: {
clickHandler: {
type: Function,
default() {
return function () {};
}
}
},
methods: {
buttonClicked: function () {
this.clickHandler(); // invoke func passed via prop
}
}
}
</script>
compname.vue
<template>
<div>{{name}}</div>
<compbutton :click-handler="changeName"></compbutton>
</template>
<script>
export default {
name: 'compname',
data: function () {
return {
name: 'John'
}
},
methods: {
changeName: function () {
this.name = 'Ben'
}
}
}
</script>
Note, in your example, it doesn't appear where you want the 'compbutton' component to be rendered, so in the template for compname.vue, thats been added as well.
You can use a service as a go-between. Usually, services are used to share data but in javascript functions can be treated like data also.
The service code is trivial, just add a stub for the function changeName
changeName.service.js
export default {
changeName: function () {}
}
To have services injected into the components, you need to include vue-injector in the project.
npm install --save vue-inject
or
yarn add vue-inject
and have a register of services,
injector-register.js
import injector from 'vue-inject';
import ChangeNameService from '#/services/changeName.service'
injector.service('changeNameService', function () {
return ChangeNameService
});
then in main.js (or main file may be called index.js), a section to initialize the injector.
import injector from 'vue-inject';
require('#/services/injector-register');
Vue.use(injector);
Finally, add the service to the component dependencies array, and use the service
compname.vue
<script>
export default {
dependencies : ['changeNameService'],
created() {
// Set the service stub function to point to this one
this.changeNameService.changeName = this.changeName;
},
...
compbutton.vue
<script>
export default {
dependencies : ['changeNameService'],
name: 'compbutton',
methods: {
buttonClicked: function () {
this.changeNameService.changeName();
}
}
...
Add a # to the button href to stop page reloads
Change Name
See the whole thing in CodeSandbox