Help on creating dynamic form fields based on collections

Hello,
As in a few archived topic I have read, people would like to dynamically create options in frontmatter :

The first topic has no answer, and the second topic is more specific about the possible solution, but as noobs we need help on how to build such a function.

Here’s my situation :

  • I have a form in wich a series of checkboxes correspond to a bunch of things.
  • These things are by the way existing as a collection of pages as children of an other page.
  • Instead of manually adding each field in my form, corresponding to a thing, I would like to retrieve the page.collection() to automatically build the fields in the template.
  • Of course, the names, labels, unique ID’s, etc, would be accessible as specific fields in the frontmatters of each thing (i.e the children pages themselves :wink: ), so the fields automation would pick informations in them.

Now, this being said, and assuming that TWIG is not in use here neither any GRAV normal routine, what we strongly need is :

  1. Help to build a php “function” based on this documentation page : Advanced Blueprint Features | Grav Documentation, and also on the example given in the part (of the same page) called “CREATING NEW FORM FIELD TYPE”, where a kind of function is shortly drawed.
  2. Help on GRAV engine basis, like “where to place PHP functions in order to the engine to correctly load them ?”, or “what are the main steps for trying to retrieve a page collection regarding to all the engine classes ?”.

We could more easily work by ourselves if we had any clue on how to proceed.

I can’t help with #1, but for #2, see the Grav Lifecycle and the rest of that plugin section to learn how to extend Grav.

I’m afraid I don’t use blueprints myself, but the docs and the source code itself are going to give you the best information.

1 Like

The sourcecode is a bit indigestible (and let us in despair), but the Grav Lifecycle is the most valuable information you could bring me here. Thank you a lot ! :+1:

Nobody loves to learn from source code, but when you’re doing something new, it’s often the only way to figure it out. The lifecycle can help you locate what part of the process you need to hook in to. And don’t forget the API docs.

And when you do figure it out, don’t forget to submit issues or pull requests against the documentation so that others after you can see what you learned!

After a short moment during wich I couldn’t post due to a technical problem, I come back here to post the solution that a friend of mine found.

This solution won’t be analyzed and explained here, because it would take too much place, so please accept it as a self-sufficient series of elements that maybe only insiders will understand. Also, the method has been built for a peculiar need, in wich I was using fieldsets : thus the method uses fieldsets as placeholder to inject data, but you could adapt the method for a form without fieldsets.

The method consist of using TWIG and the template of the page generating the form, to inject the collection of items into the form variable with the help of TWIG arrays, before calling the final form template to display it.

Here’s the method.

IN THE MARKDOWN FRONTMATTER

For retrieving the wanted page collection, here’ what needs to be added BEFORE the form declaration :

content:
    items:
        '@page': '/path/to/desired_external_page'

Thus, the frontmatter obtains the collection of the desired external page.

Then, in the form declaration itself, here’s one of the fieldset that I deliberately let blank regarding the fields: option, and wich will be used as a placeholder to inject data at this exact position in the form :

dynamic_fieldset:
    type: fieldset
    id: options
    legend: 'Options'
    fields:

It is absolutely necessary to add these primary informations, for creating the placeholder. These informations will be read and merged with themselves, then with the new lines of each item found in the collection, added at each step of the for() loop as an individual field.

NB : notice the name of the fieldset, wich is “dynamic_fieldset” ; we will use it in the TWIG code in the final merging process.

IN THE ASSOCIATED TWIG TEMPLATE

{% extends 'partials/base.html.twig' %}

{% block content %}
	{% set fieldset_array = [] %}
	{% for p in page.collection %}
		{% set newform = { (p.slug) : {type :'checkbox', name: p.slug, label:p.header.title }} %}
		{% set fieldset_array = fieldset_array|merge(newform) %}
	{% endfor %}>
	{% set fieldset_merge = {fields:fieldset_array} %}

	{{ dump(fieldset_merge) }}

	{% set form = form|merge({
		fields : form.fields|merge({
			dynamic_fieldset : form.fields.dynamic_fieldset|merge(fieldset_merge)
		})
	})%}

	{{ dump(form) }}

	{{ page.content }}

	{% include 'forms/form.html.twig' %}
{% endblock %}

As I said, that code won’t be explained here, for pratical reasons.

But I can still say two things about it :

  1. TWIG can build arrays, but only by merging them. Indeed, it seems to be impossible to simply add a key and a value anywhere in an array, using TWIG.
  2. Consequently, the loop in the first part of the code is using a merging process, and the final series of complex merging consist of nested mergings as redefinition of the form variable ({% set form = ...). This nesting is absolutely necessary for the good conduct of the injection.

:mage: I will post the complete exegesis in my future website, in a few weeks (in French and English). I won’t forget to come back here to give you an hyperlink to the tutorial.

Thanks for sharing this tricky way of doing it. I basically understand what it does. But I do have an issue with the complex merging to form:

set form = form|merge(...

causes

Twig_Error_Runtime thrown with message "The merge filter only works with arrays or "Traversable", got "object" as first argument."

This maybe because I needed to gather “form” first, because it was simply not in twig available (my form is not defined in this page, but somewhere else). So I called set form = forms( {route:'/path/to/form'} ) first.

This returns an Grav\Plugin\Form\Form object of the form I like to merge into.

Note: I modified your proposal to do the same thing with select options to populate a select form element. An I dit not used the content.items.'@page'as you suggested I used set mypage = page.find('/rout/to/page_with_items)'.
In the dumped data everything looks fine for me, just the final “form” part is not working.

How did you accessed “form” in your situation? What am I doing wrong?

ps. I am using latest Grav 1.6.0-rc1

Okay,

maybe I found a solution for my issue I like to share.

As I got form as an object by using forms() I looked for functions publically provided by it. I discovered a public function setFields() - interesting :hugs:! So, I thought, let’s not merge the complete form, but compose the fields array as you suggested and then set all fields using this function. I replaced the final merge part

{% set form = form|merge({
	fields : form.fields|merge({
		dynamic_fieldset : form.fields.dynamic_fieldset|merge(fieldset_merge)
	})
})%}

by

{% set foo = form.setFields(form.fields|merge({
	dynamic_fieldset : form.fields.dynamic_fieldset|merge(fieldset_merge)
})%}

then I included the form twig with

{% include "forms/form.html.twig" %}

and - yes - I have now updated my form fields as wanted :grin:. Still I need to to check that all form actions and processing works, but I do not see any obstacle for now, why i shuldn’t.