1

I have a situation where I'd like to watch for a change in an Element Select field and determine if any Elements have been selected or not.

Element Select fields have two input elements. One, is always present and never changes, and the other gets added and removed from the page depending on if any items have been selected. Here's a simplified version of the HTML:

<div class="field" id="fields-fieldhandle-field">

    {# This input field is always present and never changes #}
    <input type="hidden" name="fields[assets]" value="">

    <div class="elements">

        {# When you add or remove elements, this .element div and input 
           get added and removed from the DOM #}
        <div class="element>
            <input type="hidden" name="fields[assets][]" value="430">
        </div>    
    </div>

</div>

I've tried various listeners but so far they all get destroyed when the second input gets removed from the page, or never get added to the right input field if it is not on the page.

How do I listen for an element that may or may not exist on the page?

1 Answer 1

2

I think there's a few ways to skin this cat, but the most robust way would probably be to listen to the the selectElements event for the BaseElementSelectInput instances bound to each field's container element (the instance will be bound to the <div class="elementselect"> node, except for Categories fields where it'll be bound to the <div class="categoriesfield"> node, for some reason) and can be accessed by calling $('.elementselect').data('elementSelect').

You can use the .on() method to listen to the actual events, here's one way to do it:

$('.elementselect, .categoriesfield').each(function () {
    $(this).data('elementSelect').on('selectElements', onSelectElements);
});

function onSelectElements(e) {
    var $field = $(e.target);
    var selectedElements = e.elements;
}

HUGE CAVEAT

Depending on when your plugin's JavaScript runs, the elementSelect data attribute for the element select input DOM nodes may return null.

This is because Craft actually initialises the BaseElementSelectInput instances in some inline JavaScript injected into the source, and this JavaScript is, unfortunately, written to the page after your plugin's JavaScript resources are queued up (after any inline JavaScript your plugin adds, too).

In other words, if you need to hook into the BaseElementSelctInput instances on pageload, you'll need to poll for the data('elementSelect) attribute (ew, I know):

$('.elementselect, '.categoriesfield').each(function () {
    var now = new Date().getTime();
    var getElementSelect = (function () {
        var elementSelect = $(this).data('elementSelect');
        if (elementSelect) {
            // Woo, I can haz elementSelect
            elementSelect.on('selectElements', onSelectElements);
        } else if (new Date().getTime() - now < 5000) {
            // Poll for 5 secs (then give up)
            Garnish.requestAnimationFrame(getElementSelect);
        }
    }).bind(this)
    getElementSelect();
});

Hack alert: If you don't want to poll for the elementSelect instances, there is another way. Credit due, I learnt this trick from looking at Benjamin Fleming's source code:

var elementSelectInit = Craft.BaseElementSelectInput.prototype.init;
Craft.BaseElementSelectInput.prototype.init = function () {
    elementSelectInit.apply(this, arguments);
    this.on('selectElements', onSelectElements);
    this.on('removeElements', onRemoveElements);
}

function onSelectElements (e) {
    console.log(e);
}

What the above does, is to basically hijack the BaseElementSelectInput class' init method, in order to add some custom event listeners when each BaseElementSelectInput instance is initialised. Works like a charm and – even if it's a huge hack – is quite a bit cleaner than the polling method, in my opinion.

2
  • Thanks Mats. That gets me up and running. I appreciate the additional considerations too! Sep 19, 2016 at 10:13
  • No worries Ben, happy it helped you out! Sep 19, 2016 at 10:45

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.