Cyril CHAPON Cyril CHAPON - 2 months ago 22
Javascript Question

Dynamic template with dynamic scope compilation

I have a very specific need that cannot realy be solved with standard data-binding.

I've got a leaflet map that I want to bind with a vue view-model.

I succeeded to display geojson features kinda bounds to my view, but I'm struggling at displaying a popup bound with vue.js

The main question is : "How to open a popup (possibly multiple popups at the same time) and bind it to a view property "

For now I've come to a working solution, but this is aweful :

map.html

<div id="view-wrapper">
<div id="map-container"></div>

<div v-for="statement in statements" id="map-statement-popup-template-${statement.id}" style="display: none">
<map-statement-popup v-bind:statement="statement"></map-statement-popup>
</div>
</div>

<!-- base template for statement map popup -->
<script type="text/template" id="map-statement-popup-template">
{{ statement.name }}
</script>


map.js

$(document).ready(function() {
var map = new L.Map('map-container');
map.setView(new L.LatLng(GLOBALS.MAP.STARTCOORDINATES.lng, GLOBALS.MAP.STARTCOORDINATES.lat), GLOBALS.MAP.STARTZOOM);

var osm = new L.TileLayer('http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png');
osm.addTo(map);

//Initialize map dynamic layers
var mapLayers = {};

//View-model data-bindings
var vm = new Vue({
el: '#view-wrapper',
data: {
statements: []
},
methods: {
getStatements: function() {
return $.get('api/statements');
},
updateStatements: function() {
var that = this;
return that.getStatements().then(
function(res) {
that.statements = res.data;
}
);
},
refreshStatements: function() {
mapLayers.statements.layer.clearLayers();

if(this.statements && this.statements.length){
var geoJsonStatements = geoJsonFromStatements(this.statements);
mapLayers.statements.layer.addData(geoJsonStatements);
}
},
handleStatementFeature: function(feature, layer) {
var popupTemplateEl = $('#map-statement-popup-template-' + feature.properties.statement.id);
layer.bindPopup(popupTemplateEl.html());

var statementIndex = _.findIndex(this.statements, {statement:{id: feature.properties.statement.id}});

if(feature.geometry.type === 'LineString') {
this.statements[statementIndex].layer = {
id: L.stamp(layer)
};
}
},
openStatementPopup: function(statement) {
if(statement.layer) {
var featureLayer = mapLayers.statements.layer.getLayer(statement.layer.id);
featureLayer.openPopup();
}
}
},
created: function() {
var that = this;

//Set dynamic map layers
var statementsLayer = L.geoJson(null, {
onEachFeature: this.handleStatementFeature
});

mapLayers.statements = {
layer: statementsLayer
};

map.addLayer(mapLayers.statements.layer);

this.updateStatements().then(this.refreshStatements);

this.$watch('statements', this.refreshStatements);
},
components: {
'map-statement-popup': {
template: '#map-statement-popup-template',
props: {
statement: null
}
}
}
});

function geoJsonFromStatementsLocations(statements){
var geoJson = {
type: "FeatureCollection",
features: _.map(statements, function(statement) {
return {
type: "Feature",
geometry: {
type: "LineString",
coordinates: statement.coordinates
},
properties: {
statement: statement
}
};
});
};
return geoJson;
}
});


This seems pretty aweful to me, because I have to loop over statements with a
v-for
, render a div for my custom element for every statement, hide it, then use it in the popup, grabbing it with a dynamic id technique.




I would like to do something like this :

map.html

<div id="view-wrapper">
<div id="map-container"></div>
</div>

<!-- base template for statement map popup -->
<script type="text/template" id="map-statement-popup-template">
{{ statement.name }}
</script>


map.js

$(document).ready(function() {
[...]

//View-model data-bindings
var vm = new Vue({
el: '#view-wrapper',
data: {
statements: []
},
methods: {
handleStatementFeature: function(feature, layer) {
var popupTemplateEl = $('<map-statement-popup />');
var scope = { statement: feature.properties.statement };
var compiledElement = this.COMPILE?(popupTemplateEl[0], scope);
layer.bindPopup(compiledElement);
}
},
components: {
'map-statement-popup': {
template: '#map-statement-popup-template',
props: {
statement: null
}
}
}
});

function geoJsonFromStatementsLocations(statements){
var geoJson = {
type: "FeatureCollection",
features: _.map(statements, function(statement) {
return {
type: "Feature",
geometry: {
type: "LineString",
coordinates: statement.coordinates
},
properties: {
statement: statement
}
};
});
};
return geoJson;
}
});


... but I couldn't find a function to "COMPILE?" based on a defined scope. Basically I want to :


  • Create a custom element instance

  • Pass it a scope

  • Compile it



EDIT : Actually, I could find $compile function. But it's often used to compile appended child to html. I don't want to append it THEN compile it. I'd like to compile it then let leaflet append it for me.

Answer

Would this work for you? Instead of using a component, you create a new element to be passed to bindPopup, and you new Vue on that element, with your data set appropriately.

new Vue({
  el: 'body',
  data: {
    popups: [1, 2, 3],
    message: "I'm Dad",
    statements: []
  },
  methods: {
    handleFeature: function(id) {
      const newDiv = document.createElement('div');
      const theStatement = {
        name: 'Some name for ' + id
        };
      newDiv.innerHTML = document.getElementById('map-statement-popup-template').innerHTML;
      new Vue({
        el: newDiv,
        data: {
          statement: theStatement
        },
        parent: this
      });

      // Mock call to layer.bindPopup
      const layerEl = document.getElementById(id);
      this.bindPopup(layerEl, newDiv);
    },
    bindPopup: function(layerEl, el) {
      layerEl.appendChild(el);
    }
  }
});
<script src="//cdnjs.cloudflare.com/ajax/libs/vue/1.0.26/vue.min.js"></script>
<div class="leaflet-zone">
  <div v-for="popup in [1,2,3]">
    <button @click="handleFeature('p-' + popup)">Bind</button>
    <div id="p-{{popup}}"></div>
  </div>
</div>

<template id="map-statement-popup-template">
  {{ statement.name }} {{$parent.message}}
</template>

I think you could do the same thing with $compile, but $compile is poorly (really un-) documented and intended for internal use. It is useful for bringing a new DOM element under control of the current Vue in the current scope, but you had a new scope as well as a new DOM element, and as you noted, that binding is exactly what Vue is intended to do.

You can establish a parent chain by specifying the parent option as I have updated my snippet to do.

Comments