Understanding 2-way data binding and scope using Leaflet with Vue - javascript

I'm used to approaching this problem the React way, with 1-way data binding and state, so I'm having trouble thinking about it with Vue.
I have a map that renders points based on the lat/lng of news stories. When a user changes the value of a select, the points on the map update. When the user clicks on a point, a popup opens with a link to the story. However, I cannot get these two functionalities to work together.
Here's my Vue template:
<div class="row">
<div id="map" class="map"></div>
</div>
<select v-model="selectedMonth">
<option disabled value="">Please select one</option>
<option>January 2018</option>
<option>December 2017</option>
<option>November 2017</option>
<option>October 2017</option>
</select>
<button v-on:click="reset">Reset all</button>
<div class="row">
<div class="col col-xs-12 col-sm-6 col-md-3 col-lg-3"
v-for="(story, index) in displayedStories">
<img v-bind:src="story.img_src" />
<br />
<a v-bind:href="story.url" target="_blank">{{ story.card_title }}</a>
<p>{{ story.published }}</p>
</div>
</div>
and the JS:
export default {
name: 'app',
data() {
return {
leafleftMap: null,
tileLayer: null,
markers: [],
allStories: [],
selectedMonth: null,
}
},
mounted() {
this.getNodes()
this.initMap()
},
computed: {
displayedStories() {
const displayedStories = this.selectedMonth
? dateFilter(this.selectedMonth, this.allStories)
: this.allStories
if (this.leafleftMap) {
/* remove old markers layer */
this.leafleftMap.removeLayer(this.markers)
/* create a new batch of markers */
const markers = displayedStories.map(story => L.marker(story.coords)
.bindPopup(mapLink(story))
)
const storyMarkers = L.layerGroup(markers)
/* add current markers to app state and add to map */
this.markers = storyMarkers
this.leafleftMap.addLayer(storyMarkers)
this.changedMonth = this.selectedMonth
}
return displayedStories
},
},
methods: {
getNodes() { /* make api request */ }
initMap () { /* initialize map with */ }
},
}
The problem is with the line this.leafleftMap.removeLayer(this.markers). When it's there, the markers render and change with the select button, but the popup doesn't work. When I remove that line, the popup works, but the map loses its ability to update when the select changes.
I tried adding a custom directive to the select:
<select v-model="selectedMonth" v-updateMonth>
in hopes to focus when the JavaScript is enacted:
directives: {
updateMonth: {
update: function(el, binding, vnode) {
console.log('select updated')
vnode.context.leafleftMap.removeLayer(vnode.context.markers)
}
}
},
but the directive is called whenever anything on the page changed, not just when I update the select.
I'm trying to call a function (to remove the markers) only when the select is changed, but can't seem to get that to work in Vue. It wants to call every function with every update.

I was able to solve this issue with an #change and I think I have a better understanding of when to use computed in Vue.
First, I moved a lot of the update logic out of computed:
computed: {
displayedStories() {
return this.selectedMonth
? dateFilter(this.selectedMonth, this.allStories)
: this.allStories
},
},
so that it's just returning an array. Then I added a listener to the select:
<select v-model="selectedMonth" #change="updateStories()">
and then created a new method for handling that change:
methods {
updateStories() {
const markers = displayedStories.map(story => L.marker(story.coords)
.bindPopup(mapLink(story)))
const storyMarkers = L.layerGroup(markers)
this.markers = storyMarkers
this.leafleftMap.addLayer(storyMarkers)
this.changedMonth = this.selectedMonth
},
},

Related

v-bind:style for dynamical value in Vue.js

When I use v-bind:style for using dynamical value, I came across the problem that v-bind:style doesn't work but I'm sure v-bind:style gets correct value(:style='{ color : red(any other value) }') in console and css in style section reflects successfully. why not v-bind ?? Any idea ?? Thank you so much.
<div class="l-modal" v-if="modalVisible1">
<div class="p-modal" #click="hide_modal" :style='{ color : titleColor1 }' ref="test">
<p>{{titleTxt1}}</p>
<p>{{contentTxt1}}</p>
<p>{{endTxt1}}</p>
<button class="p-modal__btn">{{buttonTxt1}}</button>
</div>
</div>
<div class="l-modal__bg" v-if="modalBgVisible1" #click="hide_modal"></div>
data () {
return {
selected_title_color1:'',
titleColor1:'',
colors:['紅色','藍色','黃色','粉色','土地色','水藍色','灰色','淺綠色','橙色'],
colors_dic:{紅色:'red',藍色:'blue',黃色:'yellow',粉色:'pink',土地色:'blawn',水藍色:'aqua',灰色:'gray',淺綠色:'palegreen',橙色:'orange'},
}
},
watch:{
selected_title_color1: function () {
this.titleColor1 = this.colors_dic[this.selected_title_color1];
}
},
As described you should use an computed property for the style.
This one will also auto reflect any changes of props.
If you have some conditions you can also modify the values depending on that in the computed callback function. I've added an example with darkmode to make this approach clear.
export default {
data(){ return {
selected_title_color1:'',
titleColor1:'',
colors:['紅色','藍色','黃色','粉色','土地色','水藍色','灰色','淺綠色','橙色'],
colors_dic:{紅色:'red',藍色:'blue',黃色:'yellow',粉色:'pink',土地色:'blawn',水藍色:'aqua',灰色:'gray',淺綠色:'palegreen',橙色:'orange'},
modalVisible1: true,
darkmode: false, // example value
}},
watch:{
selected_title_color1: function () {
this.titleColor1 = this.colors_dic[this.selected_title_color1];
}
},
computed: {
// Here comes your style magic.
// Return an Object that you will bind to the style prop.
// Changes to any reactive value will be reflected directly.
style(){
// create the base style here.
let newStyle = {}
// apply your titleColor1 which results in style="{color:titleColor1}"
if(this.titleColor1){
this.color = this.titleColor1
}
// if you would have more conditions you can add them here, too
// just an _example_ if you would have a darkmode value in data.
if(this.darkmode){
this.backgroundColor = '#222222'
} ​
​
​return newStyle
​}
​},
​methods: {
​// rest of methods if
​}
}
and then attach it to your div with :style="style".
<div class="l-modal" v-if="modalVisible1">
​<div class="p-modal" #click="hide_modal" :style="style" ref="test">
​<p>{{titleTxt1}}</p>
​<p>{{contentTxt1}}</p>
​<p>{{endTxt1}}</p>
​<button class="p-modal__btn">{{buttonTxt1}}</button>
​</div>
</div>
<div class="l-modal__bg" v-if="modalBgVisible1" #click="hide_modal"></div>
Tip from my side. I would outsource the code for setting the color and bind the method to an event that changes the color instead of using an watcher. This can make your app a bit more flexible and cleans it up. But the way you've done it works, too.
Check following snippet please, it look like everything works fine:
new Vue({
el: "#demo",
data () {
return {
selected_title_color1:'',
titleColor1:'',
colors:['紅色','藍色','黃色','粉色','土地色','水藍色','灰色','淺綠色','橙色'],
colors_dic:{紅色:'red',藍色:'blue',黃色:'yellow',粉色:'pink',土地色:'blawn',水藍色:'aqua',灰色:'gray',淺綠色:'palegreen',橙色:'orange'},
modalVisible1: true,
}
},
watch:{
selected_title_color1: function () {
this.titleColor1 = this.colors_dic[this.selected_title_color1];
}
},
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="demo">
<div class="l-modal" v-if="modalVisible1">
<div class="p-modal" :style='{ color : titleColor1 }' ref="test">
<p>{{ titleColor1 }} - {{ selected_title_color1 }}</p>
</div>
</div>
<select v-model="selected_title_color1">
<option value="" disabled>Select color</option>
<option v-for="option in colors" v-bind:value="option">
{{ option }}
</option>
</select>
</div>

Hide popup box when clicked anywhere on the document

I am trying to make a component with a list of items and when I click on each of the items, it shows me an edit popup. When I click on it again, it hides the edit popup. But I would like to also be able to click anywhere on the document and hide all edit popups (by setting edit_item_visible = false).
I tried v-on-clickaway but since I have a list of items then it would trigger multiple times. And the #click event would trigger first and then the clickaway event would trigger multiple times and hide it right after showing it. I also tried to change the component's data from outside but with no luck.
Vue.component('item-list', {
template: `
<div>
<div v-for="(item, index) in items" #click="showEdit(index)">
<div>{{ item.id }}</div>
<div>{{ item.description }}</div>
<div v-if="edit_item_visible" class="edit-item">
Edit this item here...
</div>
</div>
</div>
`,
data()
{
return {
items: [],
edit_item_visible: false,
selected: null,
};
},
methods:
{
showEdit(index)
{
this.selected = index;
this.edit_item_visible = !this.edit_item_visible;
}
},
});
const App = new Vue ({
el: '#app',
})
If you want to be able to edit multiple items at the same time, you should store the list of edited items, not global edit_item_visible flag.
showEdit(item)
{
this.selected = item;
this.editing_items.push(item);
}
// v-on-clickaway="cancelEdit(item)"
cancelEdit(item)
{
let idx = this.editing_items.indexOf(item);
this.editing_items.splice(idx, 1);
}

Vue JS - Problem with computed property not updating

I am quite new with VueJS and I have been having trouble lately with some computed properties which do not update as I would like. I've done quite some research on Stack Overflow, Vue documentation and other ressources but i haven't found any solution yet.
The "app" is basic. I've got a parent component (Laundry) which has 3 child components (LaundryMachine). The idea is to have for each machine a button which displays its availability and updates the latter when clicked on.
In order to store the availability of all machines, I have a data in the parent component (availabilities) which is an array of booleans. Each element corresponds to a machine's availability.
When I click on the button, I know the array availibities updates correctly thanks to the console.log. However, for each machine, the computed property "available" does not update is I would want it to and I have no clue why.
Here is the code
Parent component:
<div id="machines">
<laundry-machine
name="AA"
v-bind:machineNum="0"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="BB"
v-bind:machineNum="1"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
<laundry-machine
name="CC"
v-bind:machineNum="2"
v-bind:availableArray="this.availabilities"
v-on:change-avlb="editAvailabilities"
></laundry-machine>
</div>
</div>
</template>
<script>
import LaundryMachine from './LaundryMachine.vue';
export default {
name: 'Laundry',
components: {
'laundry-machine': LaundryMachine
},
data: function() {
return {
availabilities: [true, true, true]
};
},
methods: {
editAvailabilities(index) {
this.availabilities[index] = !this.availabilities[index];
console.log(this.availabilities);
}
}
};
</script>
Child component:
<template>
<div class="about">
<h2>{{ name }}</h2>
<img src="../assets/washing_machine.png" /><br />
<v-btn color="primary" v-on:click="changeAvailability">
{{ this.availability }}</v-btn>
</div>
</template>
<script>
export default {
name: 'LaundryMachine',
props: {
name: String,
machineNum: Number,
availableArray: Array
},
methods: {
changeAvailability: function(event) {
this.$emit('change-avlb', this.machineNum);
console.log(this.availableArray);
console.log('available' + this.available);
}
},
computed: {
available: function() {
return this.availableArray[this.machineNum];
},
availability: function() {
if (this.available) {
return 'disponible';
} else {
return 'indisponible';
}
}
}
};
</script>
Anyway, thanks in advance !
Your problem comes not from the computed properties in the children, rather from the editAvailabilities method in the parent.
The problem is this line in particular:
this.availabilities[index] = !this.availabilities[index];
As you can read here, Vue has problems tracking changes when you modify an array by index.
Instead, you should do:
this.$set(this.availabilities, index, !this.availabilities[index]);
To switch the value at that index and let Vue track that change.

Vue bind multiple v-models to elements in v-for loop

I have a v-for loop that creates sections, all of which have a switch component that is set to ON. It is set to on because it is part of this group. The idea is that when one gets switched to OFF it will make an API call to set the new state and be removed from the group.
The trouble that I'm having is that right now I'm binding the switch with v-model and a computed property and when the sections get built they are connected to the SAME property. So if one gets switched to OFF, they all do. I am not sure how to separate these so that when one is clicked it only affects that one. I will also need data associated with the switch that is clicked to make the API call. PS, a click method on the switch element DOES NOT WORK.
HTML
<div class="col-md-6 col-sm-12" v-for="person in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle"></switcher>
</div>
VUE
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return true;
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}
You can change your toggle to an array:
computed: {
people() { return this.$store.getters.peopleMonitoring },
toggle: {
get() {
return Array(this.people.length).fill(true);
},
set() {
let dto = {
reportToken: this.report.reportToken,
version: this.report.version
}
this.$store.dispatch('TOGGLE_MONITORING', dto).then(response => {
}, error => {
console.log("Failed.")
});
}
}
}
}
And your HTML:
<div class="col-md-6 col-sm-12" v-for="(person, index) in people">
<switcher size="lg" color="green" open-name="ON" close-name="OFF" v-model="toggle[index]"></switcher>
</div>

VueJS - Updating variable in another vue

I have a vue, which uses an accordion table that displays the data when a particular row is selected. There is a button "Edit" which hides the data and shows a form.
The form is in another vue (to separate them out out..) The form is showing on clicking the button, however, inside the form I have another button "Save" which calls an ajax request, then hides the form and shows the data.
The problem I'm having is that I cannot seem to figure out how I can update the variable inside the first vue from the second vue. I could use the store but this is not an option as it would update for everyone, whereas it should only update for the particular user.
Enquiries vue: (HTML)
<tr v-if="row.id in expanded">
<td :colspan="9" style="background-color: #F0FFFF;">
<div class="accordian-body">
<div v-if="editing != false">
<enquiries-view-edit></enquiries-view-edit>
</div>
<div v-else>
<div class="container">
<div class="pull-right">
<button type="button" class="btn btn-primary btn-md" #click="editing = !editing">Edit</button>
</div>
</div>
</div>
</div>
</td>
</tr>
Javascript:
export default {
components: {
Multiselect
},
data() {
return {
msg: 'This is just an estimation!',
tooltip: {
actual_price: 'Click on the price to edit it.'
},
expanded: {},
replacedCounter: 0,
theModel: [],
enquiry: [],
center: {lat: 53.068165, lng: -4.076803},
markers: [],
needsAlerting: false,
editing: false
}
},
}
Inside EnquiriesVue I have:
export default {
props: ['editing'],
computed: {
editing: function {
console.log("Computed the value");
}
}
}
I have tried to compute the value, but this is not doing anything inside the console.
EDIT:
Basically, inside enquiries-view-edit I want a button where you click on it, and it updates the variable editing inside the Enquiries vue so that the form hides and the data vue is then shown.
A child component can communicate with its parent by emitting events. Like this:
export default {
props: ['editing'],
methods: {
onClick: function {
console.log("Clicked on child!");
this.$emit('stop-editing');
}
}
}
This assumes you have something like this in your child component's template:
<button #click="onClick">Stop editing</button>
You could then "listen" to that event from the parent component as you would any other native event, by registering a handler when including the child component.
<enquiries-view-edit #stop-editing="editing = !editing"></enquiries-view-edit>
Obviously this is a very simple example. You can also pass data along with your emitted event for more complex scenarios.

Categories

Resources