Accessing DOM element in VueJS - javascript

I created the following setup
HTML
<div class="col-md-6">
<div id="GISMap" v-el:map></div>
</div>
main.js VUE
var Vue = require('vue');
import VueResource from 'vue-resource';
import HomeView from './components/HomeView.vue';
Vue.use(VueResource);
Vue.config.debug = true;
window.app = new Vue({
el: '.content',
components: {
HomeView
},
methods: {
// Broadcast info that API has been loaded. Listen to this in GoogleMaps Module
init: function() {
this.$broadcast('MapsApiLoaded');
}
}
})
MODULE 1: HomeView.vue
<script>
export default {
events: {
MapsApiLoaded: function() {
// Initialize GIS Map
initGISMap(this.$els.map);
}
}
}
</script>
GoogleMaps.js
function initGISMap(selector) {
map = new google.maps.Map(selector, {
zoom: 10,
mapTypeId: google.maps.MapTypeId.ROADMAP,
});
// Set initial Location and center map to this location
initialLocation = new google.maps.LatLng(48.184845, 11.252553);
map.setCenter(initialLocation);
// Create a searchmarker
searchMarker = createMarker();
// Init Autocomplete for GIS
initAutoComplete();
}
I want to create the map in the div container with the tag v-el:map. When I call initGISMap(this.$els.map) within the module, and print out the value in the console, it is empty. So it seems that I don't have access to this element from within a module? How do I need to change this?
Overall approach:
main.js init() method is broadcasting an info when the map is loaded. This is caught within the HomeView module and the initGISMap is supposed to be called, residing in the GoogleMaps.js. This is all working fine, but the element to be handed over is missing.

The element is probably not in the scope of your HomeView module.
The good thing is: vm.$broadcast() can take more than one argument, so you can pass the element directly to the function with
this.$broadcast('MapsApiLoaded', this.$els.map);
and in HomeView.vue, you can pass it on with
export default {
events: {
MapsApiLoaded: initGISMap;
}
}
(You don't have to wrap initGISMap in a closure here.)

Related

Can't call Vue component method outside of it's element

I have a simple vue component with one method that I am trying to call outside of it's wrapper element #app but it is not triggering. Is there a way to reigster the view component so that I could call it with Component.function();
var viewModel = new Vue({
el: "#app",
data: {},
methods: {
test: function() {
alert("test fuction called");
}
}
});
HTML:
<div id="app">
</div>
<a #click="viewModel.test()">Click me!</a>
Fiddle:
https://jsfiddle.net/queeeeenz/Lja7pake/198/
I tested for a while.
It might be not able to use # in elements outside of Vue element
The var viewModel seems not attached to window object
I can run with this though
JS
window.viewModel = new Vue({
el: "#app",
data: {},
methods: {
test: function() {
alert("test fuction called");
}
}
});
HTML
<div id="app">
</div>
<a onClick="viewModel.test()">Click me!</a>
First of all, it seems like you are attaching a click handler the "Vue" way, without actually it being a Vue component. That is not going to work.
To strictly achieve what you want, you have to expose your function to a different scope, e.g. via assigning it to a window attribute.
var viewModel = new Vue({
el: "#app",
data: {},
created () {
// Now it is exposed
window.test = this.test;
},
methods: {
test: function() {
alert("test fuction called");
}
}
});
// And later
window.test();
A better way of doing this is probably by using a global event bus. Instead of exposing random functions in the global scope, you can instead create a bus that you expose instead. The nice part about that is that if everything happens within the Vue application, you could use this.$bus.$emit('...') from anywhere in the Vue application and listen to it everywhere else in the Vue application. The nice part if it is used outside the Vue application is that you use a set interface between the inside of your Vue application and the outside of your Vue application, preventing you from having to expose more and more functions in the global scope, and allowing you to figure out what can and cannot be done from outside the Vue application.
import Vue from 'vue';
export const bus = new Vue();
// Elsewhere
import { bus } from './bus';
Vue.prototype.$bus = bus;
// In outside code
import { bus } from '../../my-vue-application/bus';
bus.$emit('test');
// In your component
var viewModel = new Vue({
el: "#app",
data: {},
created () {
this.$bus.$on('test', this.test);
},
beforeDestroy () {
this.$bus.$off('test', this.test);
},
methods: {
test: function() {
alert("test fuction called");
}
}
});

Mapbox style changes/breaks on zoom when a layer is added

I have a mapbox map, initialized with the outdoors-v9 style (tried other styles, same behavior). When I add a layer to the map - a marker or a geojson source and zoom the map, the style changes or breaks, I'm not sure which.
This is the map before the zoom
and after the zoom
here are the functions that init the map and add markers
mapboxgl.accessToken = "pk.*******";
buildMap: function() {
const _self = this;
_self.map = new mapboxgl.Map({
container: "map",
style: "mapbox://styles/mapbox/outdoors-v9",
center: [-95.712891, 37.09024],
zoom: 3
});
_self.map.on('load', function() {
_self.map.addSource('route', {
'type': 'geojson',
'data': {
"type": "FeatureCollection",
"features": []
}
});
_self.map.addLayer({
'id': 'route',
'source': 'route',
'type': 'line',
'layout': {
'line-join': 'round',
'line-cap': 'round'
},
'paint': {
'line-color': '#47576A',
'line-width': 3
}
});
});
}
...
const coords = [addressData.longitude, addressData.latitude];
const marker = new mapboxgl.Marker().setLngLat(coords).addTo(this.map);
I am using Vue.js to render the map. Mapbox version v0.45.0
Any help or leads are highly appreciated
Vue data() properties are reactive, they have getters and setters, so, when loading map object or adding vector tiles layer (geojson), Vue tries to add getters & setters to the map & map.layers which causes vue & vue-dev-tools to crash and mess up the map.
If you enable any raster layer, it would work successfully because raster tiles are loaded via the mapbox.css whereas vector tiles being geojson, are added to the map object.
Easiest solution would be to define a non-reactive variable in vue and then re-use it everywhere.
// edit: A correct/recommended way to set non-reactive data: GitHub link
Seems the issue was related with the fact that I'm pushing the marker instance to an observable (a vuejs data field). After pushing the marker instance to an array, the issue disappeared. This comment doesn't really answer why this happens, but hope it helps someone else that might face the same issue
I just faced this issue and realized that I didn't follow the documentation exactly as it was described (jumped right on to coding without reading properly). And the documentation says:
Storing Map object
Take note that it's generally bad idea to add to Vuex or component's
data anything but primitive types and plain objects. Vue adds getters
and setters to every property, so if you add Map object to Vuex store
or component data, it may lead to weird bugs. If you want to store map
object, store it as non-reactive property like in example below.
The problem was that I had also registered "map" inside the "data" object of my Vue component. But in the example code it's not declared in data, only in the "create" function.
https://soal.github.io/vue-mapbox/guide/basemap.html#map-loading
After hours spent on this problem, here is my working solution to access map instance from a store (thanks to https://github.com/vuejs/vue/issues/2637#issuecomment-331913620):
const state = reactive({
map: Object.freeze({ wrapper: /* PUT THE MAP INSTANCE HERE */ });
});
Here is an example with Vue Composition Api:
index.js
import { reactive, computed } from "#vue/composition-api";
export const state = reactive({
map: null
});
export const setMap = (map) => {
state.map = Object.freeze({ wrapper: map});
};
export const getMap = computed(() => state.map.wrapper);
export const initMap = (event) => {
setMap(event.map);
// now you can access to map instance from the "getMap" getter!
getMap.value.addSource("satellite-source", {
type: "raster",
url: "mapbox://mapbox.satellite",
});
getMap.value.addLayer({
id: "satellite-layer",
type: "raster",
source: "satellite-source"
});
};
App.vue
<template>
<MglMap :accessToken="..." :mapStyle="..." #load="onMapLoaded" />
</template>
<script>
import { defineComponent } from "#vue/composition-api";
import { MglMap } from "vue-mapbox";
import { initMap } from "./index.js";
export default defineComponent({
components: {
MglMap
},
setup() {
const onMapLoaded = (event) => {
initMap(event);
}
return { onMapLoaded };
}
});
</script>
I've got the same error.
This happens if you either put the map or the marker on an reactive vue.js instance.
Short and quick answer.
Explanation is similar to #mlb's answer. So you freeze the object to prevent the map from disorientated and for any actions done to the map, call back the data with an extra Object key which in case is 'wrapper'.
<template><MglMap :accessToken="..." :mapStyle="..." #load="onMapLoaded" /></template>
<script>
methods: {
onMapLoaded(event) {
this.mapboxEvent = Object.freeze({wrapper: event.map});
},
panMap(event) {
this.mapboxEvent.wrapper.panTo([lng, lat], {duration: 1000, zoom: 14});
}
}
</script>

how communicate two separate .vue files?

In a project with vue.js 2:
I've a component living in a .vue file that represents a list of elements. Also, I've a sidebar that is the summary of this list. This sidebar is another component in a .vue file.
So, how I can keep communication between each them, for example, if I removed a element from a list, reflect the change in a var declared in sidebar that is the total number of elements?To ilustrate:
SideBar.vue
<template>
...
<span></span> ===> here I need total of elements listed in ListElements.vue
...
<template>
ListElements.vue
<template>
...
#click="deleteEntry"
...
<template>
<script>
methods: {
deleteEntry(entry) {
//here I need to notify to SideBar.vue in order to update the total of elements in the this.entries list.
let index = this.entries.indexOf(entry);
if (window.confirm('Are you sure you want to delete this time entry?')) {
this.entries.splice(index, 1);
}
}
</script>
OK, I've created a simplified example of how this works. Your bus needs to be global so it is accessible by all Vue components, this simply means placing it outside of all other components and view models:
var bus = new Vue({});
var vm = new Vue({
// Main view model has access to bus
el: '#app'
});
Then you just need to emit the event on the bus on some event and catch that in the other component:
Component one emits a message to the bus on keyup:
Vue.component('component-one', {
template: '<div>Enter a message: <input v-model="msg" v-on:keyup="updateMessage"> </div>',
methods: {
updateMessage() {
bus.$emit('msg', this.msg);
}
},
data() {
return {
msg: ""
}
}
});
Component-two listens for the message:
Vue.component('component-two', {
template: "<div><b>Component one says: {{ msg }}</b></div>",
created() {
bus.$on('msg', (msg) => {
this.msg = msg;
});
},
data() {
return {
msg: ""
}
}
});
Here's the fiddle: https://jsfiddle.net/v7o6d2vL/
For your single page components to get access the the bus you just need to make sure your bus is in the global scope, which you can do by using window:
window.bus = new Vue({});
you can then use bus.$emit() and bus.$on() inside your components as normal

Accessing viewmodel from another viewmodel in durandaljs is creating a new instance instead of accessing the singleton instance

I have a div element in my shell.html that uses the compose binding to load a view and view model:
<div data-bind="compose: { model: 'viewmodels/subMenu' }"></div>
My shell.js looks like:
import common = require("helpers/common");
class Shell {
view: any; router: DurandalRootRouter;
activate: () => Q.Promise<any>;
constructor() {
var self = this;
self.view = common.view;
self.router = common.router;
common.router.map([ { route: '', title: 'Home', moduleId: 'viewmodels/Home', nav: true } ]);
self.activate = () => { return Q.when(common.router.activate()); }
}
}
export = Shell
My subMenu.js view model looks like the following:
define([], function () {
"use strict";
return {
isPreview: ko.observable(false)
};
});
I then have another view model which imports this subMenu viewmodel with requirejs and changes the 'isPreview' property value. But when it imports the submenu view model, it creates a new instance rather than accessing the existing one. Based on the durandal docs: http://durandaljs.com/documentation/Creating-A-Module.html
In the above example, both modules returned an object instance. This results in what would commonly be called a "singleton" object, since the same object instance is obtained by all who require it.
I understand that because it returns an object this should therefore be the same instance instead of being transient?

Ractive creating duplicate "ghost" components, but not displaying or calling lifecycle events

I'm at my wit's end with this one.
Essentially Ractive is creating two instances of a component, but only calling oninit of one of them — the one that isn't actually in the DOM. This happens after navigating to a different "page" (main.set('page', 'somepage')) and then navigating back to the previous page.
The main ractive instance is pretty simple:
var main = new Ractive({
el: 'main',
template: require('templates/main'),
partials: partials,
components: { ... },
data: {
loading: true,
page: 'loading',
component: function(name){
if (!!this.partials[name]) return name;
this.partials[name] = '<' + name + '/>';
return name;
},
},
});
and itss template:
{{> navigation}}
<div id='page'>
{{> component(page) }}
</div>
{{> footer}}
I tried reproducing this in a Plunkr, but was unsuccessful. Instead, I added debugger statements to a live, unminfied version.
edit: Live version removed
It happens after navigating back to the index page. Open the console. Click on the 'login' button in the top right, then click on the logo to go back to the index. There are source maps, files of interest are homepage.js (the component that is being 'ghosted') and router.js (where the main ractive instance is).
It looks like the router is calling the registered function from the first component that registered the route:
oninit: function() {
// self out here has new guid
var self = this;
route('/', getFeatured, getTrending);
function getFeatured(context, next) {
// self in here has "r-1"
next();
...
It doesn't seem page.js has the concept of unregistering. One option is to move the route registration to the root component and route to the child component:
// in root:
route('/', function(){
this.findComponent('homepage').getFeatured()
}, function(){
this.findComponent('homepage').getTrending()
}
// in homepage component, add as methods:
getFeatured: function(context, next){
...
},
getTrending: function(context, next){
...
},
Marty's insight enabled me to see what was going wrong, so he gets the accepted answer, but here is my solution to the problem.
I made a subclass that provides a route function which wraps any route's functions in a callback that rebinds them to the current component. It does assume some global scope which I try to avoid, but is a necessary evil in this case.
//Controller.js
module.exports = Ractive.extend({
route: function(path, fn) {
var self = this;
var fns = [].slice.call(arguments, 1).map(function(fn) {
return function() {
// assumes `main` is global — the root Ractive instance of your app
self = main.findComponent(self.component.name)
fn.apply(self, arguments);
}
});
// also assumes `page` is a global
page.apply(this, [path].concat(fns));
}
});
And for the component:
return Controller.extend({
...
oninit: function() {
this.route('/', getFeatured, getTrending);
//the `this` in `getFeatured()` will now be the correct instance
},
...
});
}

Categories