Express Form Block, advanced templating, filtering selection options based on associations and selections

The challenge I’m trying to get ahead of here is a scaling problem. In that, the website I’m building is going to use a lot of Express Objects, and lead to many Express Entries, over time forms for many things will get to have very large amount of selections (selecting Express Entries through Associations).

So I am trying to find a way to apply a template to the core block the Express Form Block. I do not want to write my own block, as I want to leverage all the work already gone into the Express Form Block (input validation/sanitisation, etc). Also, hoping to do this WITHOUT Ajax.

Additionally I plan to re-use this method on many different Express Forms for many different purposes.

Let’s take a few Express Objects as part of the example for functional explanation:

  1. Events
  2. Tournaments
  3. Games
  4. Teams

Naturally, one event is likely to have multiple tournaments, and each tournament will associate with one game, and each tournament can have multiple teams. Over time, there will be multiple events, many more tournaments, many more teams, and the game size will roughly stay the same due to its reuse of that object entries.

Naturally, each event page will have an Express Entry attribute, which is set to the Express Entry that is the event itself.

First goal:

For the form, on the event page, where a user creates/joins a team for a tournament, I want to filter out any tournaments that are for OTHER events, and only show tournaments for THIS event. In the form the option to select would be the Express Association to the Tournaments Express Object, but how do I filter what is shown in the Form to only show Tournaments associated with this event?

Second goal:

There are going to be times where I have a form with multiple Express Associations in the form to select things from. In addition to the First goal, where I filter out based on an Express Entry (in previous example, the event). How do I make it so when I pick (single, or multiple) things from an earlier part of the same form, that filters what is possible to be selected in later Express Association selections in the same form?

Let’s say it’s a Form that is not on the event page, and you first pick which Event you want and then there’s a second selection for which Tournament you want (which is filtered based on which Event you just selected, so it ONLY shows Tournaments for that event), so that way you then can create a Team for that tournament.

==============================================

How can this be accomplished? @dimger84 proposed some ideas relating to Cascade Dropdowns however I really am nowhere near experienced enough to see how I can use that with Express Objects/Associations, relevant variables/objects, to connect the dots between that method and how the Express Form Block templating works.

Again, looking to do this without ajax, without writing my own block.

===============================================

The reason that this is important to me is that if I do not figure out a way to filter these things out, the forms for creating teams, registering for tournaments, and stuff like that, will become so bloated with previous events/tournaments/etc that they will be undesirable and painful to use. I want to solve this future problem now so that it never becomes a problem. And I think if a solution can be found for this, others would benefit from the method too.

Any help appreciated!

If I understand correctly…

First goal:
I would just filter that association list in custom express form control template:
concrete/elements/express/form/form/association/xxxx.php
For example, in select_multiple.php, I would get event id from page attribute and compare it to $entry->getEvent()->getID() etc.

// pseudo code inside foreach loop (at the beginning)
if ($entry->getEntity()->getHandle()=='tournament' and $entry->getEvent()->getID() != $event->getID()) continue; // do not display that tournament

Propably you could dig deeper and override class that actually makes $allEntries array, but it should be a good start for a now.

Overriding template guide:
https://documentation.concretecms.org/developers/express/express-forms-controllers/form-theming/using-contexts-customize-form-and-attribute-markup

Maybe there are better ways to do it, but someone more experienced in Express development should chime in.
Modifying anything in Express form block is already over-complicated, so personally I would think about making some custom page type controller instead.


Second goal:
You could technically use only javascript, but you would still need to store full list, and filter it using javascript. Depends how complicated it will be in the end, ajax could be easier.

I’ll have to try the controller proposition for the First Goal, have been caught up in other things hence delayed response (sorry).

As for second goal, I’m fine with doing it in Javascript, but I am so green around the ears I really would need some example code to get me started if that’s possible. Are you able to help connect the dots for me on this?

Thanks!

Also, hopefully we can get others to chime in on other thoughts too? I really don’t see this functional need going away.

You should probably finish goal 1 before moving to goal 2.

You have actually linked code in your post how to reach goal 2. You just have to get all necessary data in page type controller and display it as js object in your view template.

Okay saying that I’ve linked code how to do it, does not connect the dots for how to use it for goal 2. That’s why I’m asking for help/examples. I don’t understand what you mean when you say “You just ahve to get all the necessary data in page type control and display it as js object…” (I get the in your view template part, I think). My lack of understanding is why that isn’t self-indicative to me.

I’m going to try and make time to go through your previous post more comprehensively in the next handful of days, but if you find yourself up to the task of some sort of example for goal 2 that would be appreciated. And if not, well I’ll try to get as far as I can with what you’ve said thus-far, but we’ll see if that takes me all the way or not…

Either way, I do appreciate your help and time! So thanks!

It’s mostly about encoding php array into JSON string and then using JSON.parse() to convert it into js object.

Below you will find some example code written in jquery, it should give you some general idea how to tackle it (using only js and not ajax).

Obviously if you have more fields like that, you should probably organize it somehow that you will be not lost later/add other on change events etc.

<?php
// Those arrays should be probably created in controller based on express entries.
// List of all events for select field.
$events = [
    '' => 'Select event',
    1 => 'Event 1',
    2 => 'Event 2',
    3 => 'Event 3',
];
// List of all tournaments for select field.
$tournaments = [
    '' => 'Select tournament',
    1 => '"Tournament 1" name',
    2 => 'Other tournament name',
    3 => 'Next tournament name',
    4 => 'Tournament 4 name',
    5 => 'Other tournament name 5',
    6 => 'Next tournament name 6',
    7 => 'Tournament 7 name',
    8 => 'Other tournament name 8',
    9 => 'Next tournament name 9',
];
// ... but if tournament list would be too big and caused issues in browser when displaying,
// then you should probably make it empty by default
// and populate with filtered tournaments only on $_POST action (when displaying errors)
/*
if ($this->post('event')) {
    // Some code that will create tournaments array filtered by event id
}
*/
?>

<?php
$app = \Concrete\Core\Support\Facade\Application::getFacadeApplication();
$form = $app->make('helper/form');
?>
<form action="" method="post">
    <?php // Remember about CSRF token and all that stuff ?>
    <div class="form-group">
        <?php echo $form->label('event', 'Event'); ?>
        <?php echo $form->select('event', $events, '', []); ?>
    </div>
    <div class="form-group">
        <?php echo $form->label('tournament', 'Tournament'); ?>
        <?php echo $form->select('tournament', $tournaments, '', ['disabled' => 'disabled']); ?>
    </div>
    <div class="form-actions">
        <button type="submit" name="Submit" class="btn btn-primary">Submit</button>
    </div>
</form>

<?php
// This array should be probably created in controller based on express entries
$eventsData = [
    1 => [
        ['id' => 1, 'name' => '"Tournament 1" name'],
        ['id' => 2, 'name' => 'Other tournament name'],
        ['id' => 3, 'name' => 'Next tournament name'],
    ],
    2 => [
        ['id' => 4, 'name' => 'Tournament 4 name'],
        ['id' => 5, 'name' => 'Other tournament name 5'],
        ['id' => 6, 'name' => 'Next tournament name 6'],
    ],
    3 => [
        ['id' => 7, 'name' => 'Tournament 7 name'],
        ['id' => 8, 'name' => 'Other tournament name 8'],
        ['id' => 9, 'name' => 'Next tournament name 9'],
    ],
];
?>

<?php // Now, let's convert it to JSON string and display somewhere as data-* attribute?>
<div id="events-data" data-events="<?php echo h(json_encode($eventsData)); ?>"></div>

<?php // I would put this code into separate .js file, but it will work like that anyway ?>
<script>
    $(function () {
        const eventControl = $('#event');
        const tournamentControl = $('#tournament');

        // Convert JSON string to js object
        const events = JSON.parse($('#events-data').attr('data-events'));

        // Now when we select event...
        eventControl.on('change', function () {
            // .. get currently selected tournament (we will need it later) ...
            const selectedTournamentID = tournamentControl.find(':selected').val();

            // ... remove existing options from tournament control and disable it ...
            tournamentControl.html('');
            tournamentControl.append(`<option value="">Select tournament</option>`);
            tournamentControl.val('');
            tournamentControl.attr('disabled', true)

            const selectedEventID = $(this).val();
            if (selectedEventID) {
                // ... populate tournament select field with options (based on events object) ...
                $.each(events[selectedEventID], function (index, tournament) {
                    tournamentControl.append(`
                        <option value="${tournament.id}">
                            ${tournament.name}
                        </option>
                    `);
                });
                // ... set previously selected tournament ...
                if (selectedTournamentID && tournamentControl.find(`option[value="${selectedTournamentID}"]`).length) {
                    tournamentControl.val(selectedTournamentID);
                }
                // ... remove disabled attribute
                tournamentControl.attr('disabled', false)
            }
        });

        // This will trigger change action on every page request
        // (especially on $_POST request when errors are being displayed)
        eventControl.trigger('change');
    });
</script>

Dang thanks @Parasek ! One of the thoughts that I had was passing the PHP (objects?) into JS to manipulate in JS. However I wasn’t successful finding how (which I’m going to have to read through on your method). But I also was (and am?) concerned that once the object is in JS it doesn’t connect back to PHP. In that, once I “select” the Tournament, the form being completed doesn’t create an Express Entry for the related Express Object (Tournament). This method you’ve posted here, does it make that JS/PHP bi-directional connection for objects?

It creates only “snapshot” of database entries at the time of rendering. It’s not bi-directional at all.
It’s only ui/frontend part. You would still need to do all work in php to ensure data integrity/validity:

  • check if both event and tournament exists in Express database at all (you are sending event id and tournament id using form).
  • check if relation between event and tournament exists (because you can easily modify id of event/tournament in browser using DevTools).

and then you can save/create Express entry based on user data.

So if the Form that I’m filtering isn’t going to itself create the Express Entries from the selections being filtered, what’s the point? It sounds like that breaks the ability for the Form (Express Block) to make the appropriate Express Entry that one would want in this scenario…

It will if you write some code. If you don’t want to do it, then use Express form block (and modify it as in documentation) which will do most of the work. Though you would still need to do all js and some additional validation.

Yeah I don’t want to create a block from scratch here, so my question is more, using these methods with the Express Form Block, with the PHP object enumerated into JS, and then the user makes the form selection, it still seems conceptually uncertain if that form will create the appropriate Express Entry with the one-way direction for the PHP->JS Object aspect. It seems preferable to use a way to make the PHP<->JS Object aspect bi-directional so the Form creates the Express Entry correctly. Am I just wrong? Or am I missing something?

Again I do appreciate your help with this! Please don’t consider this me not listening to you, this is still rather complicated for me so trying to understand.

When you are submitting event form, you are not creating express entry for tournament. You are just creating express entry for event + add relationship to already existing tournament.

In your form, tournament is just and id (+ name for displaying purpose) - nothing more is required to add relationship.

If you use Express block form, it will add that relationship when you submit form (based only on sent id).

Not sure what do you mean by bi-directional aspect? Javascript code used by me is just a “fluff” to filter available options for user. Everything else should be done in backend by php code - by you or it is already done by form block.