Dynamically add / remove items from form field list

Hi,

I’ve created a form field that includes the form list field.
It’s working perfectly but I’d like to interact with the list (add, modify and delete list items) without using the built-in buttons…

I think (I’m not sure) the needed javascript is there grav-plugin-admin/admin.min.js at master · getgrav/grav-plugin-admin · GitHub

But I don’t really know how to use it…

Any idea, any leads ?

Thanks :slight_smile:

I think this is what you’re looking for

Thanks @Karmalakas ! This is more like it, yes :slight_smile:

I’m not sure how to deal with it… The script is already loaded since I can use the form field buttons.

I don’t see how to call the addItem() or removeItem(), which is needed before populate the list fields…

How familiar are you with JS?
I believe these are your lines:

    list.on('click', '> .collection-actions [data-action="add"]', (event) => this.addItem(event));
    list.on('click', '> ul > li > .item-actions [data-action="confirm"]', (event) => this.confirmRemove(event));
    list.on('click', '> ul > li > .item-actions [data-action="delete"]', (event) => this.removeItem(event));
    list.on('click', '> ul > li > .item-actions [data-action="collapse"]', (event) => this.collapseItem(event));
    list.on('click', '> ul > li > .item-actions [data-action="expand"]', (event) => this.expandItem(event));
    list.on('click', '> .collection-actions [data-action-sort="date"]', (event) => this.sortItems(event));
    list.on('click', '> .collection-actions [data-action="collapse_all"]', (event) => this.collapseItems(event));
    list.on('click', '> .collection-actions [data-action="expand_all"]', (event) => this.expandItems(event));
    list.on('input change', '[data-key-observe]', (event) => this.observeKey(event));

But all these methods are available only inside CollectionsField class. So if you want to have your own logic, I’m afraid you might have to extend the class and methods. Just not sure how you would hook your child class to load and init instead of this original (if it’s even possible).

Maybe @pamtbaau knows how to :thinking:

Cannot think of anything without knowing what OP wants to achieve…

  • What’s wrong/missing with the available buttons? In other words, what problem(s) needs to be resolved?
  • Is it blocking the user if problem is not being solved? Or is it just “nice-to-have”?
  • What should the solution look like?

Hi @pamtbaau and @Karmalakas !

There is nothing wrong with the form field list as it is :slight_smile:

I’m trying to improve a previous work : a form field that embeds a Leaflet map on which there is an evenListener to add values to an input.

  • blueprints/default.yaml
extends@:
    type: default

form:
  fields:
    tabs:
      fields:
        geolocation:
          type: tab
          title: PLUGIN_LEAFLET_NEW.GEOLOCATION
          fields:
            header.coordinates:
              type: coordinates
              label: PLUGIN_LEAFLET_NEW.COORDINATES
  • templates/forms/coordinate/coordinates.html.twig
{% extends "forms/field.html.twig" %}


{% block prepend %}
	{% if config.plugins['leaflet-new'].private.load == true %}
		<div id="leaflet" style="width: 100%; height: {{ config.plugins['leaflet-new'].private.height }}px; margin-bottom: 24px; background-color: grey;"></div>
	{% endif %}
{% endblock %}

I’ve written a piece of JavaScript which loads the map et enable interactions : when clicking on the map, a marker is added and the coordinates field input is populated with its coordinates.

The main issue is that the input can contains multiples coordinates… Hence, the idea of using the list form field, something like that :

  • blueprints/default.yaml
extends@:
    type: default

form:
  fields:
    tabs:
      fields:
        geolocation:
          type: tab
          title: PLUGIN_LEAFLET_NEW.GEOLOCATION
          fields:
            header.coordinates:
              type: coordinates
              label: PLUGIN_LEAFLET_NEW.COORDINATES
              sort: false
              fields:
                .markerID:
                  type: int
                  label: PLUGIN_LEAFLET_NEW.MARKER_ID
                  validate:
                    type: int
                .latitude:
                  type: text
                  label: PLUGIN_LEAFLET_LEW.LATITUDE
                  validate:
                    pattern: [0-9]+.[0-9]+
                .longitude:
                  type: text
                  label: PLUGIN_LEAFLET_LEW.LATITUDE
                  validate:
                    pattern: "[0-9]+.[0-9]+"
  • templates/forms/coordinate/coordinates.html.twig
{% include "forms/fields/list/list.html.twig" %}

{% if config.plugins['leaflet-new'].private.load == true %}
	<div id="leaflet" style="width: 100%; height: {{ config.plugins['leaflet-new'].private.height }}px; margin-bottom: 24px; background-color: grey;"></div>
{% endif %}

The idea is to add a fields collection to the list when cllcking on the map and adding the marker.
But I don’t really see how to interact with the list field…

I’m lost here…

You want to have a list of markers (where marker has an ID, a longitude and a latitude), right? So you should use List field as it is and add your Coordinates (or maybe better yet - LeafletMarker) field as a list item. All JS should be done in the scope of coordinates field. List field should have nothing to do with coordinates

P. S. If you plan to publish this, please don’t name it as ..._new (I think I already mentioned that somewhere). What if someone else or you yourself decides after a couple of years to make a better plugin, what would be the name then? ..._new_new?

I’ll try to make my idea clearer…

Yes, I want to have a list of markers with these three properties (id, latitude, longitude).

The idea is to use the map interactions to fetch those properties and to populate the list.

But :

  • I don’t see how to include the map container and its script into the list field (it doesn’t have prepend / append blocks like the field.html.twig)
  • I don’t see how I can append a new “row” to the list when clicking on the map.

Your LeafletMarker field (I’ll call it that way, because it makes more sense to me) should be as a standalone field like any other (Text for example). List field deals with adding/removing markers.

Then marker field I imagine would display a map where you can pin the point and pinning or changing the point would save 3 values - ID, lat, long. You could probably show these values bellow the map as a text (just an idea).

So basically you would have a list of unlimited markers.

My point is, you should create a standalone single marker field and then use it in the list

BTW, benefit of a standalone field would make it usable for those who don’t need a list of markers, but just a single marker on a page. So I would strongly recommend making a plugin with a standalone field and then you can use it yourself however you want

Thanks !
For now, I’ve managed to add a geolocation tab to the default template. It contains an interactive map and a simple text field which is populated when you add a marker onto the map.
Here is the code : bricebou/grav-plugin-gis - grav-plugin-gis - reclic.dev vous délivre votre code

I’m not sure it’s what you have in mind @Karmalakas but I’m not sure I’m ready to do better for now on this point…

OK, looking at the code I get confused. There are two parts - backend (Admin) and frontend (output on page). So you need to decide how you want to do it.

I believe on frontend you want to show multiple markers on a single map and it kinda does it, but you don’t have multiple markers saved on backend (or even a structure on page frontmatter)

I understand on backend there must be a map shown, where you click on it and marker gets added. So there are two ways, both of which involve JS and hidden form fields. My suggestion was to have a single map for single marker and use list field to add multiple markers. Other way would be having single map and every time you click, marker gets added. In this case list field wouldn’t help much and you’d need to implement your own solution.

Currently your code prevents having multiple maps shown working on same page (backend or frontend), so I would try to solve that first (JS and HTML/Twig). Then make it work in backend so you could save multiple maps each having multiple markers (JS, field HTML, frontmatter structure, etc.). And only then solve output to frontend when you already have the frontmatter (also try to output multiple maps each having multiple markers).

Basically in backend depending which way you go, you’d have either a field for a map with multiple markers or a field for a single marker. Either way saved frontmatter should have a structure like:

maps:
  -
    mapName: Some name maybe
    markers:
      -
        id: randomStringOrWhatever
        lat: 0.0
        lng: 0.0
      -
        id: otherRandomStringOrWhatever
        lat: 10.0
        lng: 10.0
  -
    mapName: Some other name probably
    markers:
      -
        id: orMaybeUUID
        lat: 50.0
        lng: 50.0
      -
        id: otherUUID
        lat: 60.0
        lng: 60.0

Then have a separate template (can extend default if you wish to have a description and media) - not default - where you output all maps with their markers on frontend.

At least that’s how I imagine it more or less :thinking:

Thanks a lot :slight_smile:

First of all, I haven’t work on the frontend at the moment… But my idea is that geolocation is dealt with in the backend.

At this moment, the plugin can deals with multiple markers : the idea is to associate one or more coordinates to a content ; I don’t see any reason why more than one map would be needed (on the backend) for this need.

Multiple coordinates are saved within a single text field :

title: sdfsdfsdf
coordinates: '51.48890840245442,-0.406494140625;51.7106931293536,0.49987792968750006'

I’ve extended the default.yaml blueprint, as the Quark theme does ; I think that if you activate the plugin, it’s because you need the feature : it simply adds a field you can use or not. I won’t propose a default template for the frontend, that’s up to a theme I think.

For the frontend, the idea is to supply something like {{ gis }} that adds the map with all markers.
And then to work on the shortcode way to insert maps within the content.

You never know what other use cases might be. It’s better to at least plan ahead, that someone might want to have multiple maps. For example store locations around the country as a list.


How do I add a title for each of these coordinates? Or any other additional info :slight_smile: IMO marker should have it’s own set of fields also


Currently you’re extending the default blueprint itself. I’m not even sure what takes priority - theme or plugin. Or are they merged… And don’t know what happens if multiple plugins extend it this way. Quark does this, because it’s a theme (not a plugin) and it adds body_classes text field to it, which is necessary for theme to function as intended. Plugin should have its own template and separate blueprint for it. You can check an example on one of my own plugins to see what I try to say. There’s a separate page template and a blueprint for it, which extends the default as you want to.

Current approach eliminates an option of having a page without a map. If default blueprint is merged from theme and all the plugins (as I would expect), then most of the pages (I suspect even of other plugins, that are extending default for their own custom page blueprint) would have a map.

I mean my plugin linked above - image_comparer template page would also have a map (probably, because I extend default; need to test). I definitely wouldn’t want that to happen. Even if it doesn’t mess up things, I would still want to have an option to add a default page without a map.


My suggestion is to at least prepare for multiple maps solution


And here we have a problem if user decides to insert multiple maps in the content. There’s basically no way to prevent user doing that. If user will try and it won’t work, at least I would consider it as a bug.


TBH, if I needed such a plugin, I’d probably took quite a few parts of what you have already done, give you credit, but create my own with all the features I could think of, that anyone might find a use case for. Marker without at least a title for me would be a deal breaker.

Don’t get me wrong. You already did a good job, but in the current direction I don’t really see how it can’t be used in a variety of different cases.

Yeah, that would be great… But we are coming back to my first question… How can I interact with the form field list when clicking on the map…

The frontend scripts aren’t written yet.

Why would you need that? Same way how you’re adding now coordinates to one text field, instead add coordinates to some hidden fields (better separate for lat and lng) and create a text field for title. Do your own simple list of markers with specific fields for each of them instead of using List field, which deals with almost any other field inside it.


And yet you already pointed out the problem ahead of even realising it :slight_smile: Users can think of using your plugin in ways we can’t even guess :slight_smile: Especially if you give a shortcode to use

The task of building my own custom form field is currently out my sight, I’m afraid…

So, I’ve found a way that seems to do what you suggested, I suppose… I’ll try to show you.

First, we declare a new page blueprint that extends the default one, using an @import declaration. This way, anyone can use the geolocation fields in their own page blueprints.

  • blueprints/default_with_geolocation.yaml
extends@:
    type: default

form:
  fields:
    tabs:
      fields:
        geolocation:
          type: tab
          title: PLUGIN_GIS.GEOLOCATION
          import@:
            type: partials/geolocation
  • blueprints/partials/geolocation/geolocation.yaml:
form:
  fields:
    header.markers:
      name: markers
      type: list
      label: PLUGIN_GIS.MARKERS
      sort: false
      fields:
        .name:
          type: text
          label: PLUGIN_GIS.MARKER_NAME
          validate:
            required: true
        .icon:
          type: select
          label: PLUGIN_GIS.MARKER_ICON
        .latitude:
          type: text
          label: PLUGIN_GIS.LATITUDE
          validate:
            required: true
        .longitude:
          type: text
          label: PLUGIN_GIS.LONGITUDE
          validate:
            required: true

Then, with Javascript, we add an observer to the list <ul> and every time an item is added, we append an interactive map. Each item already saved has it’s own map.

Here is the page frontmatter:

title: Test
markers:
    -
        name: test
        icon: null
        latitude: '46.48628983293442'
        longitude: '2.759275841232225'
    -
        name: test
        icon: null
        latitude: '51.51763080199815'
        longitude: '-0.06712918926202739'

Here is the commit with all the changes : Refactoring the backend geolocation feature · 3576faa203 - grav-plugin-gis - reclic.dev vous délivre votre code

Looks much much better. But I’d still try to implement multiple maps. I don’t think there’s a need for such long name default_with_geolocation - just geolocation would be enough I suppose, but it’s up to you. Also maybe better to use a plugin name for a template - less chance of collision with some other plugins. And I noticed observer currently doesn’t watch for removed markers. Oh, nevermind, you currently generate a map for each marker…

I mean it’s definitely much better now. If it works for you and I assume you’re already running out of time, then probably it’s usable. There’s still plenty of room for improvement though

I’ll close this topic because its initial question has been answered :slight_smile:

I’ll keep posting about my progress on another topic ; but I take advantage of this post to let you know that I worked on the frontend part of the plugin and developed a Twig function : {{ gis() }} :

  • if can use arguments:
{{ gis({'height': 120, 'markers': [{'name': 'Test', 'icon': 'pink', 'latitude': '51.505', 'longitude': '-0.093'}]}) }}
  • without arguments, it searches inside the page.header to find markers definition
  • if you want an empty map, you can use {{ gis({'markers': [], 'center': [52,2], 'zoom': 9}) }}

And of course, you can use several {{ gis() }} inside a single page.

1 Like