Using a Vue3 component as a Leaflet popup - leaflet

This previous SO question shows how we can use a Vue2 component as the content of a LeafletJS popup. I've been unable to get this working with Vue3.
Extracting the relevant section of my code, I have:
<script setup lang="ts">
import { ref } from 'vue'
import L, { type Content } from 'leaflet'
import type { FeatureCollection, Feature } from 'geojson'
import LeafletPopup from '#/components/LeafletPopup.vue'
// This ref will be matched by Vue to the element with the same ref name
const popupDialogElement = ref(null)
function addFeaturePopup(feature:Feature, layer:L.GeoJSON) {
if (popupDialogElement?.value !== null) {
const content:Content = popupDialogElement.value as HTMLElement
layer.bindPopup(() => content.$el)
}
}
</script>
<template>
<div class="map-container">
<section id="map">
</section>
<leaflet-popup ref="popupDialogElement" v-show="false">
</leaflet-popup>
</div>
</template>
This does produce a popup when I click on the map, but it has no content.
If, instead, I change line 14 to:
layer.bindPopup(() => content.$el.innerHTML)
then I do get a popup with the HTML markup I expect, but unsurprisingly I lose all of the Vue behaviours I need (event handling, etc).
Inspecting the addFeaturePopup function in the JS debugger, the content does seem to be an instance of HTMLElement, so I'm not sure why it's not working to pass it to Leaflet's bindPopup method. I assume this has something to do with how Vue3 handles references, but as yet I can't see a way around it.
Update 2022-06-09
As requested, here's the console.log output: I've put it in a gist as it's quite long

So just to document the solution I ended up using, I needed to add an additional style rule in addition to the general skeleton outlined in the question:
<style>
.leaflet-popup-content >* {
display: block !important;
}
</style>
This overrides the display:none that is attached to the DOM node by v-show=false. It would be nice not to need the !important, but I wasn't able to make the rule selective enough in my experiments.

Related

Jquery not working when click ajax button

I use jquery datepicker and it not display after I click ajax button.
Is there any way to show datepicker again after click? I use wicket 8.
BasePage.java
public class BasePage extends WebPage {
...
}
BasePage.html
<body>
...
<script src="/js/jquery.min.js"></script>
<script src="/js/jqueryui.min.js"></script>
<script src="/js/main.js"></script>
</body>
HomePage.java
public class HomePage extends BasePage {
public HomePage() {
SearchForm searchForm = new SearchForm();
Form<SearchForm> form = new Form<>(new CompoundPropertyModel<SearchForm>(searchForm))
AjaxButton btn = new AjaxButton() {
protected void onSubmit(AjaxRequest target) {
// Handle search data
...
target.add(form);
}
};
TextField<String> date = new TextField<>("searchDate");
form.add(date);
form.add(btn);
}
}
HomePage.html
<wicket:extend>
<form wicket:id="form">
<input wicket:id="searchDate" class="datepicker" />
<button wicket:id="btn">Search</button>
</form>
</wicket:extend>
main.js
$(function() {
$(".datepicker").datepicker();
...
});
After click ajax button all script in file main.js not working
Please help me.
when you update form via AJAX you replace each element inside it, which includes the input field you use with datepicker. But doing so you loose the javascript setting done by main.js when page was first loaded.
You can solve this in two ways. First, you could update only those elements that need to be refreshed, for example the component that you use to show search result (I suppose there must be such an element in your code).
The second solution, more heavier and complicated, is to make a custom TextField component that execute the datepicker javascript code each time is rendered.
An example of such solution can be found is in the user guide: https://wicket-guide.herokuapp.com/wicket/bookmarkable/org.wicketTutorial.ajaxdatepicker.HomePage
I would recommend to follow the first solution as it's more natural and simpler and requires less code.
UPDATE:
If you want to refresh the textfield another simple solution is to use target.appendJavaScript​ to reapply the datepicker plugin:
target.add("$('#" + date.getMarkupId() + "').datepicker();");
this should add the datepicker to the fresh new field.

vuejs: How to change the class of each item in v-for

My component template have an img
<img src="img/loading.gif" :data-src="url" class="live-snapshot-img" :class="{lazy:true, 'lazy-loaded': false}">
When it rendered, I'll trigger out the lazy event. The class will changed to 'lazy-loaded' from 'lazy'.
<img src="img/loading.gif" :data-src="url" class="live-snapshot-img lazy-loaded">
Then if the data changed, I want the class change back to 'lazy'. The bind:class doesn't work.
My solution is
Create a method in component
lazyload: function() {
var clazz = {lazy: true, 'lazy-loaded': false}
clazz['dummy' + Math.random()] = true
return clazz
},
and change the img to
<img src="img/loading.gif" :data-src="url" class="live-snapshot-img" :class="lazyload()">
I think you're approaching this in slightly the wrong way. You have the right idea, but I feel like your thinking and jQuery post render dom manipulation manner, not that this is bad, just not right for this situation. Vue works a little differently. If you want to change the class after the event in this particular instance, you bind your :class to a variable in your vue component.
If I wanted to make this happen, I would probably start by setting a variables in my component data for the state. (see: Class and Style Bindings)
Vue.component('my-component-name', {
data: function () {
return {
imageState: {'lazy': true, 'lazy-loaded': false}
}
}
})
Then bind that to your element:
<img src="img/loading.gif" :data-src="url" class="live-snapshot-img" :class="imageState">
You can then manipulate the imageState in your component methods in any way you want.

ClipboardJS with React, using document.getElementById()

Originally, I had it working fine.
Then I did this and now I can't get it to work
ClipboardField.js
import React from 'react';
export default (props) => {
return(
<div id="clip" data-clipboard-text={props.code} onClick={props.onClick}>
<p> Copy to clipboard.</p>
</div>
);
}
Field.js
class DashWizardTwoCore extends Component {
componentDidMount(){
const btns = document.getElementById('clip');
const clipboard = new Clipboard(btns);
}
componentDidUpdate() {
clipboard.on('success', function(e) {
console.log(e);
});
clipboard.on('error', function(e) {
console.log(e);
});
}
render(){
const someCode = "const foo = 1"
return (
<div>
<ClipboardField code={this.someCode} /> }
</div>
);
}
}
If you take the code out of ClipboardField and into Field it works. It's mostly, how do I use document.getElementById() in my parent component to find something in my child?
Their examples:
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-selector.html#L18
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-node.html#L16-L17
https://github.com/zenorocha/clipboard.js/blob/master/demo/constructor-nodelist.html#L18-L19
Your code is fine you just have a few issues:
you are binding clipboard.on in componentDidUpdate which won't trigger here since you are not really changing anything (in the ClipboardField component) that triggers this event.
You are passing {this.someCode} in the code prop which would be undefined should just be {someCode}
So it's just a matter of moving your clipboard.on to the componentDidMount right after the new Clipboard and use code={someCode}
https://jsfiddle.net/yy8cybLq/
--
In React whenever you want to access the actual dom element of your component we use what react calls as refs, I would suggest you do this rather than using getElementById as this is the "best practice".
However stateless components (like your ClipboardField component above) can't have refs so you just need to change it to be a normal component.
Here's a fiddle with your code but using refs instead: https://jsfiddle.net/e5wqk2a2/
I tried including links to the react docs for stateless components and refs but apparently don't have enough "rep" to post more than 2 links, anyways quick google search should point you in the right direction.
I adjusted your code and created a simple integration of clipboard.js with React.
Here's the fiddle: https://jsfiddle.net/mrlew/ehrbvkc1/13/ . Check it out.

Changing the document title in React?

I'm trying to update the title of the document in a React app. I have very simple needs for this. The title is essentially used to put the Total component on display even when you're on a different tab.
This was my first instinct:
const React = require('react');
export default class Total extends React.Component {
shouldComponentUpdate(nextProps) {
//otherstuff
document.title = this.props.total.toString();
console.log("Document title: ", document.title);
return true;
}
render() {
document.title = this.props.total;
return (
<div className="text-center">
<h1>{this.props.total}</h1>
</div>
);
}
}
I thought this would just update the document.title every time this component was rendered, but it doesn't appear to do anything.
Not sure what I'm missing here. Probably something to do with how React runs this function - maybe somewhere that the document variable isn't available?
EDIT:
I'm starting a bounty for this question, as I still haven't found any solution. I've updated my code to a more recent version.
A weird development is that the console.log does print out the title I'm looking for. But for some reason, the actual title in the tab isn't updating. This issue is the same across Chrome, Safari, and Firefox.
I now use react-helmet for this purpose, as it allows to customize different meta tags and links, and it also supports SSR.
import { Helmet } from 'react-helmet'
const Total = () => (
<div className="text-center">
<Helmet>
<meta charSet="utf-8" />
<title>{this.props.total}</title>
</Helmet>
<h1>{this.props.total}</h1>
</div>
)
Original answer: there's actually a package by gaeron for this purpose, but in a declarative way:
import React, { Component } from 'react'
import DocumentTitle from 'react-document-title'
export default class Total extends Component {
render () {
return (
<DocumentTitle title={this.props.total}>
<div className='text-center'>
<h1>{this.props.total}</h1>
</div>
</DocumentTitle>
)
}
}
Inside your componentDidMount() function in App.js (or wherever), simply have:
componentDidMount() {
document.title = "Amazing Page";
}
The reason this works is anywhere in your react project you have access to the Js global scope. Go ahead and type window in your sites console. Basically everything there you will be able to access in your React project.
I think webpack-dev-server runs in an iframe mode by default:
https://webpack.github.io/docs/webpack-dev-server.html#iframe-mode
So that might be why your attempts to set the title are failing. Try setting the inline option to true on webpack-dev-server, if you haven't already.
If the react-document-title package isn't working for you, the quick'n'dirty way to do that would be in a lifecycle method, probably both componentDidMount and componentWillReceiveProps (you can read more about those here):
So you would do something like:
const React = require('react');
export default class Total extends React.Component {
// gets called whenever new props are assigned to the component
// but NOT during the initial mount/render
componentWillReceiveProps(nextProps) {
document.title = this.props.total;
}
// gets called during the initial mount/render
componentDidMount() {
document.title = this.props.total;
}
render() {
return (
<div className="text-center">
<h1>{this.props.total}</h1>
</div>
);
}
}
There is a better way of dynamically changing document title with react-helmet package.
As a matter of fact you can dynamically change anything inside <head> tag using react-helmet from inside your component.
const componentA = (props) => {
return (
<div>
<Helmet>
<title>Your dynamic document/page Title</title>
<meta name="description" content="Helmet application" />
</Helmet>
.....other component content
);
}
To change title, meta tags and favicon dynamically at run time react-helmet provides a simple solution. You can also do this in componentDidMount using the standard document interface. In the example below I am using the same code for multiple sites, so helmet is looking for favicon and title from an environment variable
import { Helmet } from "react-helmet";
import { getAppStyles } from '../relative-path';
import { env } from '../relative-path';
<Helmet>
<meta charSet="utf-8" />
<title>{pageTitle[env.app.NAME].title}</title>
<link rel="shortcut icon" href={appStyles.favicon} />
</Helmet>

DOM elements not accessible after onsen pageinit in ons.ready

I am using the Onsen framework with jQuery and jQuery mobile, it appears that there is no way to catch the event that fires once the new page is loaded.
My current code, which executes in the index.html file (the master page)
<script src="scripts/jQuery.js"></script>
<script src="scripts/jquery.mobile.custom.min.js"></script>
<script src="scripts/app.js"></script>
<script>
ons.bootstrap();
ons.ready(function() {
$(document.body).on('pageinit', '#recentPage', function() {
initRecentPage();
});
});
in app.js is the following code
function initRecentPage() {
$("#yourReports").on("tap", ".showReport", recentShowReport);
var content = document.getElementById("yourReports");
ons.compile(content);
}
and the HTML:
<ons-page id="recentPage">
<ons-toolbar id="myToolbar">
<div id="toolBarTitle" class="center">Recent Checks</div>
<div class="right">
<ons-toolbar-button ng-click="mySlidingMenu.toggleMenu()">
<ons-icon icon="bars"></ons-icon>
</ons-toolbar-button>
</div>
</ons-toolbar>
<ons-scroller>
<h3 class="headingTitle"> Checks</h3>
<div id="Free" class="tabArea">
<ons-list id="yourReports">
</ons-list>
<ons-button id="clearFreeRecentButton">
<span id="clearRecentText" class="bold">Clear Recent Checks</span>
</ons-button>
</div>
</ons-scroller>
</ons-page>
in this instance the variable 'content' is always null. I've debuged significantly, and the element I am trying to get is not present when this event fires. It is loaded later.
So, the question is, how do I ensure that all of the content is present before using a selector. It feels like this is an onsen specific issue.
In the end I could only find one reliable way of making this work.
Essentially I had to wait, using setTimeout 300 milliseconds for the DOM elements to be ready. It feels like a hack, but honestly there is no other reliable way of making this behave. The app is in the app store and works well, so even though it seems like a hack, it works:
$(document).on('pageinit', '#homePage', function() {
initHomePage();
});
function initHomePage() {
setTimeout(function() {
setUpHomePage();
}, 300);
}
Move your $(document.body).on('pageinit', '#recentPage', function() { outside of ons.ready block.
JS
ons.bootstrap();
ons.ready(function() {
console.log("ready");
});
$(document.body).on('pageinit', '#recentPage', function() {
initRecentPage();
});
function initRecentPage() {
//$("#yourReports").on("tap", ".showReport", recentShowReport);
var content = document.getElementById("yourReports");
alert(content)
ons.compile(content);
}
I commented out a line because I do not have access to that "recentShowReport"
You can see how it works here: 'http://codepen.io/vnguyen972/pen/xCqDe'
The alert will show that 'content' is not NULL.
Hope this helps.