Display a list in leaflet popup - vuejs2

I want to display an unorderd list or table in a leaflet popup.
The number of items and their content are different and depend on the type of element which was clicked.
So ideally the popup content should be created on the click event.
I tried to build the list inside the bindPopup function, but it's not working.
L.marker([mapElement.y * -1, mapElement.x], {
uniqueID: mapElement.elementID,
mapIconWidth: mapElement.width,
icon: new mapIcon({
iconUrl: icon.mapIcon.imageData,
iconSize: [elementSize, elementSize]
})
})
.addTo(markers)
.bindPopup(mapElement.element.nbr + ' ' + mapElement.element.name + "<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>");
That's the output:
Any ideas would be great!
Thanks!
Edited code (get this error message: You are using the runtime-only build of Vue where the template compiler is not available. Either pre-compile the templates into render functions, or use the compiler-included build.):
LoadMarker(mapID) {
console.log("Load Markers");
map.removeLayer(markers);
markers.clearLayers();
fetch(this.$apiUrl + "/mapelements/" + mapID)
.then(response => response.json())
.then(received => {
let mapElements = received;
let mapZoom = map.getZoom();
mapElements.forEach(function(mapElement) {
let elementSize = (mapElement.width / 8) * mapZoom;
let popup = new Vue({
template:
mapElement.element.nbr +
" " +
mapElement.element.name +
"<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>",
data: {
mapElement
}
}).$mount().$el;
let icon = mapIconSchemas.find(
schema => schema.mapIconSchemaID == mapElement.mapIconSchemaID
);
if (icon != null) {
L.marker([mapElement.y * -1, mapElement.x], {
uniqueID: mapElement.elementID,
mapIconWidth: mapElement.width,
icon: new mapIcon({
iconUrl: icon.mapIcon.imageData,
iconSize: [elementSize, elementSize]
})
})
.addTo(markers)
.bindPopup(popup);
}
});
});
map.addLayer(markers);
},

You can not use Vue templating syntax in the HTML String for the popup. But as can be seen from the docs the .bindPopup method can also accept HTML element. So your way to go would be like this:
first create the popup element:
let popup = new Vue({
template: mapElement.element.nbr + ' ' + mapElement.element.name + "<br/<ul> <li v-for='state in mapElement.element.states'>{{ state.definition.stateTypeTitle }}</li> </ul>",
data: {
mapElement
}
}).$mount().$el
and then use it in the .bindPopup method:
/*...*/
.bindPopup(popup)

There is a solution, if you want to use the vue templating engine to fill the popup content.
I explained it for this question.
You create a component with the content you want to display in the popup, but you hide it :
<my-popup-content v-show=False ref='foo'><my-popup-content>
Then you can access the generated html of that component in your code like this :
const template = this.$refs.foo.$el.innerHTML
and use it to fill your popup.
The big advantage of that method is that you can generate the popup content with all the vue functionalities (v-if, v-bind, whatever) and you don't need messy string concatenations anymore.

import Component from './Component.vue'
import router from './router'
import store from './store'
bindPopup(() => new Vue({
// router,
// store,
render: h => h(Component)
}).$mount().$el)
perfect

Related

How to handle event on html render by computed function in vue js?

I'm using vue 2. I have a text get from api.
"Hello everyone! My name is [input]. I'm [input] year old".
Now, I have to replace the [input] with an html input and handle the onKeyUp for this input.
What I have to do?
I used computed render html, but it not work with v-on:xxx.
content.replaceAll('[answer]', '<input type="text" class="input_answer" v-on:click="handleInput()"/>')
Thanks!
After spending an hour and so on this requirement, I came up with the solution.
Here you go (I added all the descriptive comments/steps in the below code snippet itself) :
// Template coming from API
var textFromAPI = "<p>Hello everyone! My name is [input]. I'm [input] year old</p>";
// getting the array of input tags. So that we can loop and create the proper input element.
const matched = textFromAPI.match(/(input)/g);
// Iterating over an array of matched substrings and creating a HTML element along with the required attributes and events.
matched.forEach((el, index) => {
textFromAPI = textFromAPI.replace('[input]', `<input type="text" id="${index + 1}" v-model="inputValue[${index}]" v-on:keyup="getValue"/>`);
})
// Here, we are compiling the whole string so that it will behave in a Vue way.
var res = Vue.compile(textFromAPI)
var app = new Vue({
el: '#app',
data: {
compiled: null,
inputValue: []
},
render: res.render,
staticRenderFns: res.staticRenderFns,
mounted() {
setTimeout(() => {
this.compiled = res;
})
},
methods: {
getValue() {
// Here you will get the updated values of the inputs.
console.log(this.inputValue);
}
}
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
</div>
Thanks # Rohit Jíndal, but!
When I use vue2 and it doesn't work. And there is an error:
TypeError: Cannot read properties of undefined (reading 'string')
I build this to a component and use anywhere in my project.
<render-html :text="question.quest_content" #handleAnswer="handleAnswer"></render-html>
I used and it's work.
this.$options.staticRenderFns = string.staticRenderFns;
return string.render.bind(this)(h)
Thanks so much!

how can a Buefy toast be made available to all components in a Vue app?

In a component in a Vue app the following method runs after a user clicks a Submit button on a form:
execute() {
let message = '';
let type = '';
const response = this.actionMode == 'create' ? this.createResource() : this.updateResource(this.data.accountId);
response.then(() => {
message = 'Account ' + this.actionMode + 'd for ' + this.data.name;
type = 'is-success';
})
.catch(e => {
message = 'Account <i>NOT</i> ' + this.actionMode + 'd<br>' + e.message;
type = 'is-danger';
})
.then(() => {
this.displayOutcome(message, type);
this.closeModal();
});
}
The displayOutcome() method in that same component looks like this:
displayOutcome(message, type) {
this.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
The code is working fine within the component. Now I'm trying to move the displayOutcome() method into a helpers.js file and export that function so any component in the app can import it. This would centralize maintenance of the toast and prevent writing individual toasts within each component that needs one. Anyhow, when displayOutcome() gets moved over to helpers.js, then imported into the component an error appears in the console when the function is triggered:
I suspect it has to do with referring to the Vue instance so I experimented with the main.js file and changed this
new Vue({
router,
render: h => h(App),
}).$mount('#app');
to this
var vm = new Vue({
router,
render: h => h(App),
}).$mount('#app');
then in helpers.js
export function displayOutcome(message, type) {
// this.$buefy.toast.open({
vm.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
but that resulted in a "Failed to compile." error message.
Is it possible to make displayOutcome() in helpers.js work somehow?
displayOutcome() requres a reference to this to work, which is fine if you define it as a method on your component object (the standard way). When you define it externally however, you just supply any function instead of a method, which is a function "targeted" on an object. This "targeting" is done through this. So when you're passing a simple function from an external file, there's no association to a specific object, and thus no this available.
To overcome this, you can use displayOutcome.apply(thisArg, methodArgs), where thisArg will be whatever is the this reference in your function, and methodArgs are the remaining arguments that are being passed to the function.
So displayOutcome.apply(4, ['some', 'thing']) would imply that the this reference in displayOutcome() becomes 4 in this case.
Further reading:
Understanding "This" in JavaScript
this on MDN
import { displayOutcome } from './component-utils'
// when calling displayOutcome() from within your component
displayOutcome.apply(this, ['Hello World', 'is-info'])
// component-utils.js
export function displayOutcome(message, type) {
// this.$buefy.toast.open({
this.$buefy.toast.open({
duration: type == 'is-danger' ? 10000 : 3500,
position: 'is-bottom',
message: message,
type: type
});
}
You can create an action in Vuex store and dispatch it from any component.

How can I use "<nuxt-link>" in content rendered with "v-html"?

I have a utilities plugin for Nuxt.js that I created with a method to parse hashtags and mentions and replace them with <nuxt-link>. I am then using v-html to inject the converted data back into the template. My issue is that the <nuxt-link> are not being parsed with v-html.
import Vue from 'vue';
Vue.mixin({
methods: {
replaceMentions(data) {
// Tags
const tagRegEx = /\[#tag:[a-zA-Z0-9_-]*\]/g;
let tagMatch;
while ((tagMatch = tagRegEx.exec(data)) !== null) {
const tag = Array.from(tagMatch[0].matchAll(/\[#tag:(.*?)\]/g));
data = data.replace(tag[0][0], '<nuxt-link to="/search?q=' + tag[0][1] + '">#' + tag[0][1] + '</a>');
};
// Users
const userRegEx = /\[#user:[a-zA-Z0-9_-]*\]/g;
let userMatch;
while ((userMatch = userRegEx.exec(data)) !== null) {
const user = Array.from(userMatch[0].matchAll(/\[#user:(.*?)\]/g));
data = data.replace(user[0][0], '<nuxt-link to="/users/' + user[0][1] + '">#' + user[0][1] + '</>');
};
return data;
}
}
});
Does anyone have any tips for how I could make these work as proper nuxt compatible links? I already tried using <a> and it works fine, I would just prefer to utilize proper nuxt compatible links.
I think the discussion here basically answers the question: https://github.com/nuxt-community/modules/issues/185
Summarized, there are two options:
Render the HTML with a full Vue build and then attach the rendered node.
(Preferred) Find the links in the HTML and make them call router push instead of their default action.

How to embed codepen as HTML in vue.js

I can't figure out how to embed a codepen using the recommended HTML method i a Vue application.
As <script> tag cannot be part of a Vue component template, I tried to add it to index.html where my Vue application is injected without luck. However, when I tried to paste the html code outside the div where Vue resides, the code got turned into an iFrame as it should.
Here is the HTML embed:
<p data-height="265" data-theme-id="0" data-slug-hash="JyxKMg" data-default-tab="js,result" data-user="sindael" data-embed-version="2" data-pen-title="Fullscreen image gallery using Wallop, Greensock and Flexbox" class="codepen">See the Pen Fullscreen image gallery using Wallop, Greensock and Flexbox by Dan (#sindael) on CodePen.</p>
And the script:
<script async src="https://static.codepen.io/assets/embed/ei.js"></script>
Embedding an iFrame directly works fine, but I wonder. Is there a way how to get the html working?
Look into the https://static.codepen.io/assets/embed/ei.js, then you will see it executes two steps:
check document.getElementsByClassName if exists, create it if not.
one IIFE to execute the embed.
So one hacky way as below simple demo:
copy the source codes from https://static.codepen.io/assets/embed/ei.js
copy the codes of first step then wrap it as one function = _codepen_selector_contructor
copy the codes of second step and remove () from the end, then wrap it as one function = _codepen_embed_method
create one vue-directive (I prefer using the directive to support the features which directly process Dom elements, you can use other solutions), then execute _codepen_selector_contructor and _codepen_embed_method
Probably you want to replace document inside _codepen_embed_method with el instead, then execute _codepen_embed_method(el). so that it will not affects other elements.
Below demo uses the hook='inserted', you could use other hooks if inserted can't meet your requirements.
let vCodePen = {}
vCodePen.install = function install (Vue) {//copy from https://static.codepen.io/assets/embed/ei.js
let _codepen_selector_contructor = function () {
document.getElementsByClassName||(document.getElementsByClassName=function(e){var n,t,r,a=document,o=[];if(a.querySelectorAll)return a.querySelectorAll("."+e);if(a.evaluate)for(t=".//*[contains(concat(' ', #class, ' '), ' "+e+" ')]",n=a.evaluate(t,a,null,0,null);r=n.iterateNext();)o.push(r);else for(n=a.getElementsByTagName("*"),t=new RegExp("(^|\\s)"+e+"(\\s|$)"),r=0;r<n.length;r++)t.test(n[r].className)&&o.push(n[r]);return o})
}
let _codepen_embed_method = //copy from https://static.codepen.io/assets/embed/ei.js then removed `()` from the end
function(){function e(){function e(){for(var e=document.getElementsByClassName("codepen"),t=e.length-1;t>-1;t--){var u=a(e[t]);if(0!==Object.keys(u).length&&(u=o(u),u.user=n(u,e[t]),r(u))){var c=i(u),l=s(u,c);f(e[t],l)}}m()}function n(e,n){if("string"==typeof e.user)return e.user;for(var t=0,r=n.children.length;t<r;t++){var a=n.children[t],o=a.href||"",i=o.match(/codepen\.(io|dev)\/(\w+)\/pen\//i);if(i)return i[2]}return"anon"}function r(e){return e["slug-hash"]}function a(e){for(var n={},t=e.attributes,r=0,a=t.length;r<a;r++){var o=t[r].name;0===o.indexOf("data-")&&(n[o.replace("data-","")]=t[r].value)}return n}function o(e){return e.href&&(e["slug-hash"]=e.href),e.type&&(e["default-tab"]=e.type),e.safe&&("true"===e.safe?e.animations="run":e.animations="stop-after-5"),e}function i(e){var n=u(e),t=e.user?e.user:"anon",r="?"+l(e),a=e.preview&&"true"===e.preview?"embed/preview":"embed",o=[n,t,a,e["slug-hash"]+r].join("/");return o.replace(/\/\//g,"//")}function u(e){return e.host?c(e.host):"file:"===document.location.protocol?"https://codepen.io":"//codepen.io"}function c(e){return e.match(/^\/\//)||!e.match(/https?:/)?document.location.protocol+"//"+e:e}function l(e){var n="";for(var t in e)""!==n&&(n+="&"),n+=t+"="+encodeURIComponent(e[t]);return n}function s(e,n){var r;e["pen-title"]?r=e["pen-title"]:(r="CodePen Embed "+t,t++);var a={id:"cp_embed_"+e["slug-hash"].replace("/","_"),src:n,scrolling:"no",frameborder:"0",height:d(e),allowTransparency:"true",allowfullscreen:"true",allowpaymentrequest:"true",name:"CodePen Embed",title:r,"class":"cp_embed_iframe "+(e["class"]?e["class"]:""),style:"width: "+p+"; overflow: hidden;"},o="<iframe ";for(var i in a)o+=i+'="'+a[i]+'" ';return o+="></iframe>"}function d(e){return e.height?e.height:300}function f(e,n){if(e.parentNode){var t=document.createElement("div");t.className="cp_embed_wrapper",t.innerHTML=n,e.parentNode.replaceChild(t,e)}else e.innerHTML=n}function m(){"function"==typeof __CodePenIFrameAddedToPage&&__CodePenIFrameAddedToPage()}var p="100%";e()}function n(e){/in/.test(document.readyState)?setTimeout("window.__cp_domReady("+e+")",9):e()}var t=1;window.__cp_domReady=n,window.__CPEmbed=e,n(function(){new __CPEmbed})}
let defaultProps = {class: 'codepen', 'data-height':265, 'data-theme-id':0, 'data-slug-hash':'', 'data-default-tab':'js,result', 'data-user':'sindael', 'data-embed-version':'2', 'data-pen-title':''}
Vue.directive('code-pen', {
inserted: function (el, binding, vnode) {
let options = Object.assign({}, defaultProps, binding.value)
Object.entries(options).forEach((item) => {
el.setAttribute(item[0], item[1])
})
setTimeout(() => {
_codepen_selector_contructor()
_codepen_embed_method() //_codepen_embed_method(el); you can pass el to take place of `document`
}, 100)
},
componentUpdated: function (el, binding, vnode) {
}
})
}
Vue.use(vCodePen)
Vue.config.productionTip = false
app = new Vue({
el: "#app",
data: {
keyword: '',
},
mounted: function () {
},
methods: {
}
})
<script src="https://unpkg.com/vue#2.5.16/dist/vue.js"></script>
<div id="app">
<p v-code-pen="{class: 'codepen', 'data-height':'265', 'data-theme-id':0, 'data-slug-hash':'JyxKMg', 'data-default-tab':'js,result', 'data-user':'sindael', 'data-embed-version':'2', 'data-pen-title':'Test'}">
</p>
</div>

Twitter typeahead.js not working in Vue component

I'm trying to use Twitter's typeahead.js in a Vue component, but although I have it set up correctly as tested out outside any Vue component, when used within a component, no suggestions appear, and no errors are written to the console. It is simply as if it is not there. This is my typeahead setup code:
var codes = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('code'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
prefetch: contextPath + "/product/codes"
});
$('.typeahead').typeahead({
hint: true,
highlight: true,
minLength: 3
},
{
name: 'codes',
display: 'code',
source: codes,
templates: {
suggestion: (data)=> {
return '<div><strong>' + data.code + '</strong> - ' + data.name + '</div>';
}
}
});
I use it with this form input:
<form>
<input id="item" ref="ttinput" autocomplete="off" placeholder="Enter code" name="item" type="text" class="typeahead"/>
</form>
As mentioned, if I move this to a div outside Vue.js control, and put the Javascript in a document ready block, it works just fine, a properly formatted set of suggestions appears as soon as 3 characters are input in the field. If, however, I put the Javascript in the mounted() for the component (or alternatively in a watch, I've tried both), no typeahead functionality kicks in (i.e., nothing happens after typing in 3 characters), although the Bloodhound prefetch call is made. For the life of me I can't see what the difference is.
Any suggestions as to where to look would be appreciated.
LATER: I've managed to get it to appear by putting the typeahead initialization code in the updated event (instead of mounted or watch). It must have been some problem with the DOM not being in the right state. I have some formatting issues but at least I can move on now.
The correct place to initialize Twitter Typeahead/Bloodhound is in the mounted() hook since thats when the DOM is completely built. (Ref)
Find below the relevant snippet: (Source: https://digitalfortress.tech/js/using-twitter-typeahead-with-vuejs/)
mounted() {
// configure datasource for the suggestions (i.e. Bloodhound)
this.suggestions = new Bloodhound({
datumTokenizer: Bloodhound.tokenizers.obj.whitespace('title'),
queryTokenizer: Bloodhound.tokenizers.whitespace,
identify: item => item.id,
remote: {
url: http://example.com/search + '/%QUERY',
wildcard: '%QUERY'
}
});
// get the input element and init typeahead on it
let inputEl = $('.globalSearchInput input');
inputEl.typeahead(
{
minLength: 1,
highlight: true,
},
{
name: 'suggestions',
source: this.suggestions,
limit: 5,
display: item => item.title,
templates: {
suggestion: data => `${data.title}`;
}
}
);
}
You can also find a working example: https://gospelmusic.io/
and a Reference Tutorial to integrate twitter typeahead with your VueJS app.