Leaflet – Creating Popup Forms with Leaflet and Implementing Listeners

html5leafletleaflet-drawpopup

I am attempting to create a map-based survey using Leaflet popups and specifically L.Draw, because I want users to be able to insert markers where ever they want to and then answer certain questions.

What eludes me is the way to create functional listeners to remember the values placed in the popup. The listeners which I use now work really well with elements with one row, like HTML <input type="datetime-local"> and HTML <input type="number">. But with the same formula I can't get input groups to be listened, specifically HTML <input type="radio"> and HTML <option> selected Attribute.

Below is the listener code inside the function layerClickHandler(e) which is run every time a marker is clicked on the L.geojson layer (onEachFeature is specified during layer creation). And for the radio button group I have named "likert", this does not work:

L.DomUtil.get('likert').textContent = properties.likert;
var likertVar = L.DomUtil.get('likert');
likertVar.value = properties.likert;
L.DomEvent.addListener(likertVar, "change", function (e){
    properties.likert = e.target.value;
});

In the current state of my code the listener above handles single line outputs. For input select the above listener breaks the element completely. For "likert" I am currently using the following code to save elements to L.geojson, but the popup doesn't remember the state:

$(document).ready(function(){
    $('input[name=likert]').click(function(){
        properties.likert = this.value;
    });
});

The HTML form itself is stored in the varible popupContent. To my best knowledge HTML code has to be inserted this way to L.Popup. My implementation for the event listener is from this Plunker: http://embed.plnkr.co/8qVoW5/

To my question:

How do I revise my code to make these input groups to be listened in the same way as the single line inputs?

Please find below the current state of my code.

<!DOCTYPE html>

<html>
    <head>
        <title>Leaflet-form</title>
        <meta http-equiv="content-type" content="text/html; charset=UTF-8" />
        <script>L_PREFER_CANVAS=false; L_NO_TOUCH=false; L_DISABLE_3D=false;
</script>

    <!-- import leaflet and leaflet.draw-->
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.4.0/leaflet.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.EasyButton/2.4.0/easy-button.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.js"></script>

    <!-- other stylesheets -->
    <link href="http://netdna.bootstrapcdn.com/font-awesome/4.0.3/css/font-awesome.css" rel="stylesheet">
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.4.0/leaflet.css"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/Leaflet.EasyButton/2.4.0/easy-button.css"/>
    <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/leaflet.draw/1.0.4/leaflet.draw.css"/>

    <!-- font -->
    <link href="https://fonts.googleapis.com/css?family=Montserrat" rel="stylesheet">

    <meta name="viewport" content="width=device-width,
        initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>

    <style>
        body {
            width: 100%;
            height: 100%;
            margin: 0;
            padding: 0;
        }
        html,
        body,
        #mymap {
            height: 100%;
        }

        p {
            font-family: 'Montserrat', sans-serif;
        }

        .leaflet-left .leaflet-control {
            margin-left: 13px;
        }

        .leaflet-container .leaflet-control-zoom {
            margin-left: 13px;
            margin-top: 70px;
        }

        .leaflet-bottom .leaflet-control {
            margin-bottom: 5px;
        }

        #mymap {
            position: relative;
            width: 100.0%;
            height: 100.0%;
            left: 0.0%;
            top: 0.0%;
        }

        #mymap {
            position:absolute;
            top:0;
            bottom:0;
            right:0;
            left:0;
        }

        .infoheader {
            z-index: 2;
            width: 210px;
            position: absolute;
            left: 50%;
            margin-left: -125px;
            padding: 10px 20px;
            background: white;
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.2);
            border-radius: 5px;
        }

        .infoheader p {
            margin: 0 0 5px;
            font-size: 14px;
            float: left;
            margin-top: 5px;
            margin-left: 20px;
            margin-bottom: 0px;
            color: black;
        }

        .infoheader h1 {
            float: left;
            font-family: 'Quicksand', sans-serif;
            font-size: 20px;
            letter-spacing: 15px;
            margin-bottom: 0px;
            margin-left: 14px;
            margin-top: 0px;
        }

        .infobox {
            width: 400px;
            padding: 6px 8px;
            font-family: 'Montserrat', sans-serif;
            background: rgba(255, 255, 255, 0.8);
            box-shadow: 0 0 15px rgba(0, 0, 0, 0.9);
            border-radius: 5px;
        }

        .infobox p {
            line-height: 600%;
        }

        .infotext p {
            font-size: small;
        }

        .likert li {
            float: none;
            list-style-type: none;
        }
    </style>

</head>
<body>
    <div class="" id="mymap" ></div>

    <script>
        // INITIALISE MAP AND LAYERS

        //initialise map
        var mymap = L.map(
            'mymap', {
            center: [57.708870, 11.974560],
            zoom: 12,
            layers: [],
            worldCopyJump: false,
            crs: L.CRS.EPSG3857,
            zoomControl: true
        });

        //initialise background tiles
        var tileLayer = L.tileLayer(
                'https://cartodb-basemaps-{s}.global.ssl.fastly.net/dark_all/{z}/{x}/{y}.png',
                {
                "attribution": null,
                "detectRetina": false,
                "maxNativeZoom": 18,
                "maxZoom": 18,
                "minZoom": 0,
                "noWrap": false,
                "opacity": 1,
                "subdomains": "abc",
                "tms": false
        }).addTo(mymap);

        // import empty GeoJSON to be populated by user
        var geojson = L.geoJson({
            "type": "FeatureCollection",
            "features": []
            },{
                onEachFeature: function (feature, layer){
                layer.on('click', layerClickHandler);
            }
            }).addTo(mymap);

        function myfunction() {
            var sub = $('#input').val();
            //console.log(sub);
        }

        //Listen to changes in form
        //this listener idea from: http://embed.plnkr.co/8qVoW5/
        function layerClickHandler (e) {
            var marker = e.target,
                    properties = e.target.feature.properties;
                    console.log(properties);

            if(marker.hasOwnProperty('_popup')){
                marker.unbindPopup();
            }
            marker.bindPopup(popupContent);
            marker.openPopup();

            //event listeners
            //DATETIME
            L.DomUtil.get('datetime').textContent = properties.datetime;
            var dateTime = L.DomUtil.get('datetime');
            dateTime.value = properties.datetime;
            L.DomEvent.addListener(dateTime, "change", function (e){
                properties.datetime = e.target.value;
            });

            //PARKING TIME
            L.DomUtil.get('parktime').textContent = properties.parktime;
            var parkTime = L.DomUtil.get('parktime');
            parkTime.value = properties.parktime;
            L.DomEvent.addListener(parkTime, "change", function (e){
                properties.parktime = e.target.value;
            });

            //LIKERT
            // Does not work properly 
            //idea http://jsfiddle.net/XFc2j/
            $(document).ready(function(){
                $('input[name=likert]').click(function(){
                    //alert(this.value);
                    properties.likert = this.value;
                    console.log(this.value);
                });
            });

            //PARK SPOT TYPE
            //This listener makes the html option element buggy
            L.DomUtil.get('parkspot').textContent = properties.parkspot;
            var parkSpot = L.DomUtil.get('parkspot');
            console.log(parkSpot);
            parkSpot.value = properties.parkspot;
            L.DomEvent.addListener(parkSpot, "change", function (e){
                properties.parkspot = e.target.value;
            });

            //TEST
            L.DomUtil.get('testtext').textContent = properties.testtext;
            var testText = L.DomUtil.get('testtext');
            testText.value = properties.testtext;
            L.DomEvent.addListener(testText, "change", function (e){
                properties.testtext = e.target.value;
            });

            //SAVE BUTTON
            var buttonSubmit = L.DomUtil.get('button-submit');
            L.DomEvent.addListener(buttonSubmit, "click", function (e){
                marker.closePopup();
            });
        }

        // INFOTEXT AND INFOBUTTON
        // infotext and infobutton and associated stuff    
        var infoText = L.control({position: 'bottomleft'});

        infoText.onAdd = function (map) {
            this._div = L.DomUtil.create('div', 'infotext');
            this.update("");
            return this._div;
        };

        infoText.update = function(text){
            this._div.innerHTML = text;
        };

        infoText.addTo(mymap);

        var infoBox = L.control({position: 'bottomleft'});

        infoBox.onAdd = function (map) {
            this._div = L.DomUtil.create('div', 'infobox');
            this._div.innerHTML = 'Did you ever hear the tragedy of Darth Plagueis The Wise? I thought not. It\'s not a story the Jedi would tell you. It\'s a Sith legend. Darth Plagueis was a Dark Lord of the Sith, so powerful and so wise he could use the Force to influence the midichlorians to create life… He had such a knowledge of the dark side that he could even keep the ones he cared about from dying. The dark side of the Force is a pathway to many abilities some consider to be unnatural. He became so powerful… the only thing he was afraid of was losing his power, which eventually, of course, he did. Unfortunately, he taught his apprentice everything he knew, then his apprentice killed him in his sleep. Ironic. He could save others from death, but not himself.';
            return this._div;
        };

        var infoButton = L.easyButton({
            states: [{
                stateName: 'infoOpen',
                icon: 'fa fa-info',
                title: 'open info',
                onClick: function (btn, map) {
                    infoBox.addTo(map);
                    btn.state('infoClose');
                }
            }, {
                stateName: 'infoClose',
                icon: 'fa fa-info',
                title: 'close info',
                onClick: function (btn, map) {
                    infoBox.remove(map);
                    btn.state('infoOpen');
                }
            }]
        });
        infoButton.addTo(mymap);

        //initialise drawing tools, disable a bunch
        var draw_control = new L.Control.Draw({
            "edit": {
                "featureGroup": geojson
            },
            "draw": {
                marker: {
                    icon: new L.Icon({
                        iconUrl: 'http://cdn.leafletjs.com/leaflet-0.6.4/images/marker-icon.png',
                        shadowUrl: 'http://cdn.leafletjs.com/leaflet-0.6.4/images/marker-shadow.png',
                        iconAnchor: [12, 40],
                        popupAnchor: [0, -41]
                    }),
                    shapeOptions: {
                        clickable: true
                    }

                },
                polyline: false,
                polygon: false,
                rectangle: false,
                circle: false,
                circlemarker: false
            }
            }).addTo(mymap);



        //determine behaviour of popups
        var popup = L.popup();

        function onMapClick(e){
            popup.setLatLng(e.latlng).setContent(e.latlng.toString() + popupContent).openOn(mymap);
        }

        mymap.on(L.Draw.Event.CREATED, function (e) {
            var layer = e.layer;
            var lat = layer.getLatLng().lat;
            var lng = layer.getLatLng().lng;
            //var coords = JSON.stringify(layer.toGeoJSON());
            var geojsonFeature = {
                    "type": "Feature",
                    "geometry": {
                        "type": "Point",
                        "coordinates": [lng, lat]
                    },
                    "properties":{
                        "datetime": null,
                        "parktime": null,
                        "likert": null,
                        "parkspot": null,
                        "testtext": [lng, lat]
                    }
            };

            layer.on('click', layerClickHandler); //"mouseover", "click"
            geojson.addData(geojsonFeature);
        });

        mymap.on('draw:created', function(e) {
            var layer = e.layer;
            var lat = layer.getLatLng().lat;
            var lng = layer.getLatLng().lng;
            var geojsonFeature = {
                    "type": "Feature",
                    "geometry": {
                        "type": "Point",
                        "coordinates": [lng, lat]
                    },
                    "properties":{
                        "parktime": 4343,
                        "testtext": [lng, lat]
                    }
            };


        });

        var popupContent = '<form name="myform" role="form" id="form" enctype="multipart/form-data" class = "form-horizontal" onsubmit="addMarker()">'+
            //datetime
            '<div class="form-group">'+
                '<label class="control-label col-sm-5"><strong>timedate question<br></strong></label>'+
                '<input type="datetime-local" placeholder="Required" id="datetime" name="datetime" class="form-control"/>'+ 
            '</div>'+

            //time to search parking
            '<div class="form-group">'+
                '<label class="control-label col-sm-5"><strong>how long question<br></strong></label>'+
                '<input type="number" min="0" max="120" placeholder="Required" class="form-control" id="parktime" name="parktime">'+
            '</div>'+

            //likert familiarity of parking area
            '<div class="row">'+
                '<label class="control-label col-sm-5"><strong>area question</strong></label>'+
                '<div class="small-3 column">'+
                    '<ul class="likert" onclick="myfunction()">'+
                        '<li><input value="1" id="likert" name="likert" type="radio">area very frequently</li>'+
                        '<li><input value="2" id="likert" name="likert" type="radio">area frequently</li>'+
                        '<li><input value="3" id="likert" name="likert" type="radio">area sometimes</li>'+
                        '<li><input value="4" id="likert" name="likert" type="radio">area seldomly</li>'+
                        '<li><input value="5" id="likert" name="likert" type="radio">area never</li>'+
                    '</ul>'+
                '</div>'+
            '</div>'+

            //type of parking spot
            '<div class="form-group">'+
                '<label for="parkspot">Parkspot question<br></label>'+
                '<select id="parkspot" name="parkspot" onclick="myfunction()">'+
                    '<option disabled selected value> -- select an option -- </option>'+
                    '<option value="3" name="parkspot" label="parkspot1"></option>'+
                    '<option value="2" name="parkspot" label="parkspot2 (parkkipaikka)"></option>'+
                    '<option value="3" name="parkspot" label="parkspot3"></option>'+
                    '<option value="4" name="parkspot" label="parkspot4"></option>'+
                '</select>'+ 
            '</div>'+

            //text box for testing
            '<div class="form-group">'+
                'testing: <input type="text" placeholder="Required" id="testtext" name="testtext">'+
            '</div>'+

            //submit button
            '<button id="button-submit" type="button">Save Changes</button>'+
        '</form>';

    </script>
</body>

Best Answer

Here's some changes to have the likert field persist:

        //LIKERT
        $(L.DomUtil.get('likert')).find("input[value='" + properties.likert+ "']").attr('checked', true);
        $(document).ready(function(){
            $('input[name=likert]').click(function(){
                //alert(this.value);
                properties.likert = this.value;
                console.log(this.value);
            });
        });

        ...

        //likert familiarity of parking area
        '<div class="row">'+
            '<label class="control-label col-sm-5"><strong>area question</strong></label>'+
            '<div class="small-3 column">'+
                '<ul class="likert" id="likert" onclick="myfunction()">'+
                    '<li><input value="1" name="likert" type="radio">area very frequently</li>'+
                    '<li><input value="2" name="likert" type="radio">area frequently</li>'+
                    '<li><input value="3" name="likert" type="radio">area sometimes</li>'+
                    '<li><input value="4" name="likert" type="radio">area seldomly</li>'+
                    '<li><input value="5" name="likert" type="radio">area never</li>'+
                '</ul>'+
            '</div>'+
        '</div>'+

L.DomUtil.get expects an ID to be passed to it and there was assigned the same ID to multiple inputs (ID's are supposed to be unique and only occur once in html). jQuery has been used to assign the 'çhecked' attribute to the input that has a value matching the stored value in the properties object.