I'm new to vue and have a really hard time getting it working. I made a single file component for my google maps div:
map.vue:
<template>
<div id="map" class="col-100">{{searchArea.radius}}</div>
</template>
<script>
function updateSearchLocation(latlng) {
this.searchArea.lat = latlng.lat();
this.searchArea.long = latlng.lng();
}
function initMap() {
map = new google.maps.Map(document.getElementById('map'), {
zoom: 15,
streetViewControl: false,
mapTypeControl: false,
zoomControl: false
});
google.maps.event.addListener(map, 'dragend', function() {
updateSearchLocation(map.getCenter());
});
addMyLocationButton();
getMyLocation();
}
window.initMap = initMap;
module.exports = {
props: {
searchArea: Object
}
}
</script>
The other functions called here (addMyLocationButton, getMyLocation) are also defined in this file and working. What's not working is accessing the searchArea object within the updateSearchLocation function.
I have a searchArea object in the root of my app:
searchArea: {
lat: null,
long: null,
radius: 2500
}
It's succesfully passed to this component (I can see it on this component in the vue dev tools). I want to use this map.vue component to update this object because in another component I'm making API calls based on this object.
The map is showing, so initMap is being called. The getMyLocation method gets the current location of the user, and then calls the same updateSearchLocation function.
Loading my map like this:
<script async defer src="https://maps.googleapis.com/maps/api/js?key=xxx&callback=initMap"></script>
put functions into exported object within 'methods' object:
module.exports = {
props: {
searchArea: Object
},
data: function() {
return {
latlng: whatever
}
},
methods: {
updateSearchLocation: function(this.latlng) {..... },
initMap: function() {.. call updateSearchLocation() as this.updateSearchLocation(this.latlng) ... },
}
}
Related
I'm starting with the Higcharts wrapper for vue. Currently I'm migrating the code of a stockchart that I was using outside vue without problems into the wrapper. Everything is going well except that I can't populate the chart from component data or computed variables. Only from hard-written array or from component props.
This is the code:
<template>
<highcharts
class="stock"
v-bind:constructor-type="'stockChart'"
v-bind:options="config"
v-bind:deepCopyOnUpdate="false"
></highcharts>
</template>
<script>
export default {
name: "stockChart",
props: {
options: {
type: Object,
required: true
}
},
data: function() {
return {
config: {
series: [{
name: this.options.title,
//data: [[1,3],[2,7],[3,9],[4,2],[5,0],[10,13]] //THIS WORKS!
//data: this.options.plotData //THIS ALSO WORKS!!
data: this.plotData //THIS DOESN'T...
}],
(...)
},
plotData: [[1,3],[2,7],[3,9],[4,2],[5,0],[10,13]]
}
},
computed: {
// THIS ALSO ISN'T WORKING... THAT IS HOW I WANT IT TO WORK
/*plotData: function(){
return this.options.xData.map((e,i) => [e, this.options.yData[i]]);
}*/
}
}
</script>
<style scoped>
.stock {
width: 70%;
margin: 0 auto
}
</style>
I don't understand anything. The three methods should be equivalent. Why I can load data from props but not from data or computed? I can't find any good documentation about the vue wrapper to understand why is this happening.
Thanks for your help,
H25E
The answer is very simple. The reason is that the Vue defines all component data only after returning a whole data object, so you should not use this keyword to refer other component data within data definition. In order to make it work correctly, you should keep the plotData within component's data, but move the config into the computed properties. Take a look on the code below:
props: {
options: {
type: Object,
required: true
}
},
data: function() {
return {
plotData: [[1,3],[2,7],[3,9],[4,2],[5,0],[10,13]]
}
},
computed: {
config: function() {
return {
series: [{
name: this.options.title,
data: this.plotData
}]
}
},
}
I would like to access refs in a vue.js component, where the ref itself is created dynamically like so:
<style>
</style>
<template>
<div>
<lmap class="map" v-for="m in [1, 2, 3]" :ref="'map' + m"></lmap>
</div>
</template>
<script>
module.exports = {
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
console.log('all refs', this.$refs);
// prints an object with 3 keys: map1, map2, map3
console.log('all ref keys', Object.keys(this.$refs));
// would expect ["map1", "map2", "map3"], prints an empty array instead
Vue.nextTick().then(() => {
console.log('map1', this.$refs["map1"]);
// would expect a DOM element, instead prints undefined
})
},
destroyed: function(){
},
methods: {
},
}
</script>
However this seems not to work (see above in the comments), and I can't figure why.
I think the problem is that you are importing the component asynchronously, with httpVueLoader, which then downloads and imports the component only when the component is rendered from the dom, therefore, the component has not yet been imported into the nextTick callback.
I suggest you put a loaded event in the map.vue component, maybe in mounted lifecycle , which will be listened to in the father, example #loaded = "showRefs"
surely when the showRefs(){ } method is invoked, you will have your refs populated ;)
Try using a template string e.g
`map${m}`
You have to wait until components have been rendered / updated. This works:
module.exports = {
data: function () {
return {
};
},
components: {
lmap: httpVueLoader('components/base/map.vue'),
},
mounted: function(){
},
destroyed: function(){
},
updated: function(){
Vue.nextTick().then(() => {
console.log('all ref keys', Object.keys(this.$refs));
console.log('map1', this.$refs['map1'][0].$el);
})
},
methods: {
},
}
I am trying to create custom event handlers for child components/elements of the functional component. The problem is that when using a render() function to create the child components, I cannot access their this context.
Suppose we have the following functional component:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
this.$emit('input', data)
}
for (const prop of Object.keys(context.props.value)){
const child = createElement('input', {
props: {
value: context.props[prop]
},
on: {
input: function(event){
// 'this' is not binded here - it is undefined,
// hence the inputHandler() function is
// rising an error
inputHandler.apply(this, [event.target.value, prop])
}
}
})
template.push(child)
}
return template
}
}
Is it possible to access this context for a vnode, when creating event handler this way?
P.S. Use case info: I want to implement a component that automatically generates <input> elements for a resource and uses two-way binding through v-model directive. I also want to use it in <table> where wrapping in <td> will be required, thus I made the component functional.
Functional components don't a have a "this", because there is no Vue instance for them. This makes them lightweight.
This also means emiting events from them is kind of harder, since you need to implement Vue's logic yourself.
Lacking an instance doesn't mean you cannot events, instead, you need to manually parse context.listeners and call the event handler manually. In the case of v-model, you need to call the input listener:
const Aggregate = {
functional: true,
props: {
value: Object // to work with v-model
},
render: function(createElement, context){
const template = []
const inputHandler = function(value, prop, handler){
const data = Object.assign({}, context.props.value, { [prop]: value })
console.log(context.props.value)
console.log(data)
// Call handler directly instead of using this.$emit
handler(data)
}
for (const prop of Object.keys(context.props.value)){
console.log(context.props.value, prop)
const child = createElement('input', {
// Small bug fixes in the following section:
domProps: {
value: context.props.value[prop]
},
// End bug fixes
on: {
input: function(event){
// pass `context.listeners.input` instead of binding here
inputHandler(event.target.value, prop, context.listeners.input)
}
}
})
template.push(child)
}
return template
}
}
new Vue({
el: "#app",
components: {
Aggregate
},
data: {
test: {
key1: "val1",
key2: "val2",
}
},
})
<!-- development version, includes helpful console warnings -->
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<div id="app">
<aggregate v-model="test"></aggregate>
<pre>{{ test }}</pre>
<button #click="test = {...test, ping: 'pong'}">Add key</button>
</div>
I have created a component which has a function which makes external API calls and then fills an array. I used created() life hook to run the function for the 1st time. I am passing a variable from the parent component into this component and then based upon this variable change I want the function to run again.
How do I achieve this.
Attaching my code below
<template>
<div>
<p>{{ data_to_show_on_mainpage }}</p>
</div>
</template>
<script>
import axios from 'axios'
export default {
name: 'GetCategoryItemsAndDisplayOne',
props: ['categoriesfordisplay','ismainpage', 'catalogselected'],
data(){
return {
IsMainPage_1 : "",
data_to_show_on_mainpage : [],
}
},
watch: {
catalogselected: function(){
this.GetItemsToShowonMainPage()
}
},
methods:{
changevalue(){
console.log("i am reducing it to emplty after change of catalog")
this.IsMainPage_1 = this.catalogselected
this.data_to_show_on_mainpage = []
},
CatlogService(catlog_name,category,gender,mainpage){
let url = "http://localhost:5000/xyz/" + (this.catalogselected).replace(/'/g,"%27") +"/api/"+ (gender) + "/catalogvis/" + (category) +"/items"
console.log(encodeURI(url))
axios.get(encodeURI(url)).then((resp)=>{
this.data_to_show_on_mainpage.push(resp.data.response.Results.results[0])
})
.catch(err => {
console.log("we got an error the url is " + url)
console.log(err);
})
},
GetItemsToShowonMainPage(){
this.changevalue()
if(this.categoriesfordisplay.men_for_display.length>0){
for(let i =0;i<this.categoriesfordisplay.men_for_display.length;i++){
let category = this.categoriesfordisplay.men_for_display[i].replace(/"/g,"%27");
this.CatlogService(this.catalogselected,category,'men',this.ismainpage)
}
}
if(this.categoriesfordisplay.women_for_display.length>0){
for(let i = 0;i<this.categoriesfordisplay.women_for_display.length;i++){
let category = this.categoriesfordisplay.women_for_display[i].replace(/"/g,"");
this.CatlogService(this.catalogselected,category,'women',this.ismainpage)
}
}
},
},
created(){
this.GetItemsToShowonMainPage()
}
}
</script>
<style>
</style>
How Do i trigger the GetItemsToShowonMainPage() function whenever the catalogselected varaible is changed.
It looks fine.
As #a-lau says, make sure the parent is updating the catalogselected prop
Btw, you can write your watcher this way and remove completely the created hook:
watch: {
catalogselected: {
handler: "GetItemsToShowonMainPage",
immediate: true
}
}
If you still have issues you might want to write a minimal reproduction on https://codesandbox.io/s/vue
I'm using <component v-for="..."> tags in Vue.js 2.3 to dynamically render a list of components.
The template looks like this:
<some-component v-for="{name, props}, index in modules" :key="index">
<component :is="name" v-bind="props"></component>
</some-component>
The modules array is in my component data() here:
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
},
{
name: 'some-thing',
props: {
color: '#f3f',
text: 'some other text',
},
},
],
I'm using the v-bind={...} object syntax to dynamically bind props and this works perfectly. I also want to bind event listeners with v-on (and use .sync'd props) with this approach, but I don't know if it's possible without creating custom directives.
I tried adding to my props objects like this, but it didn't work:
props: {
color: '#f3f',
text: 'some other text',
'v-on:loaded': 'handleLoaded', // no luck
'volume.sync': 'someValue', // no luck
},
My goal is to let users re-order widgets in a sidebar with vuedraggable, and persist their layout preference to a database, but some of the widgets have #events and .synced props. Is this possible? I welcome any suggestions!
I don't know of a way you could accomplish this using a dynamic component. You could, however, do it with a render function.
Consider this data structure, which is a modification of yours.
modules: [
{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync:{
"volume": "volume"
},
on:{
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on:{
clicked: "onClicked"
}
},
],
Here I am defining two other properties: sync and on. The sync property is an object that contains a list of all the properties you would want to sync. For example, above the sync property for one of the components contains volume: "volume". That represents a property you would want to typically add as :volume.sync="volume". There's no way (that I know of) that you can add that to your dynamic component dynamically, but in a render function, you could break it down into it's de-sugared parts and add a property and a handler for updated:volume.
Similarly with the on property, in a render function we can add a handler for an event identified by the key that calls a method identified in the value. Here is a possible implementation for that render function.
render(h){
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {on:{}, props:{}}
// add props
if (template.props){
def.props = template.props
}
// add sync props
if (template.sync){
for (let sync of Object.keys(template.sync)){
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on){
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)){
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
}
Basically, the render method looks through all the properties in your template in modules to decide how to render the component. In the case of properties, it just passes them along. For sync properties it breaks it down into the property and event handler, and for on handlers it adds the appropriate event handler.
Here is an example of this working.
console.clear()
Vue.component("some-thing", {
props: ["volume","text","color"],
template: `
<div>
<span :style="{color}">{{text}}</span>
<input :value="volume" #input="$emit('update:volume', $event.target.value)" />
<button #click="$emit('loaded')">Click me</button>
</div>
`
})
Vue.component("other-thing", {
template: `
<div>
<button #click="$emit('clicked')">Click me</button>
</div>
`
})
new Vue({
el: "#app",
data: {
modules: [{
name: 'some-thing',
props: {
color: '#0f0',
text: 'some text',
},
sync: {
"volume": "volume"
},
on: {
loaded: "handleLoaded"
}
},
{
name: 'other-thing',
on: {
clicked: "onClicked"
}
},
],
volume: "stuff"
},
methods: {
handleLoaded() {
alert('loaded')
},
onClicked() {
alert("clicked")
}
},
render(h) {
let components = []
let modules = Object.assign({}, this.modules)
for (let template of this.modules) {
let def = {
on: {},
props: {}
}
// add props
if (template.props) {
def.props = template.props
}
// add sync props
if (template.sync) {
for (let sync of Object.keys(template.sync)) {
// sync properties are just sugar for a prop and a handler
// for `updated:prop`. So here we add the prop and the handler.
def.on[`update:${sync}`] = val => this[sync] = val
def.props[sync] = this[template.sync[sync]]
}
}
// add handers
if (template.on) {
// for current purposes, the handler is a string containing the
// name of the method to call
for (let handler of Object.keys(template.on)) {
def.on[handler] = this[template.on[handler]]
}
}
components.push(h(template.name, def))
}
return h('div', components)
},
})
<script src="https://unpkg.com/vue#2.2.6/dist/vue.js"></script>
<div id="app"></div>