2

I have a field type that I need to initialize. When the page loads I can use code like the following to handle certain behaviors of the field:

$('.myfieldwrapper').each(function() {
  new Craft.MyField(this);
});

Within the Craft.MyField object, I add a couple listeners like so:

this.addListener(this.$element, 'change', 'handleElementChange');
this.addListener(this.$element2, 'click', 'handleElement2Click');

That all works fine. However, this field doesn't just get added to the page on page load. It exists in the row of a table, and a user can add additional rows to the table which add a new row and field to the DOM.

I've tried to instantiate the newly added field in several ways, and I have been unable to get it right yet. The Craft.MyField seems to be initializing, and I appear to be able to re-initialize the default fields on the page, but my attempts do not have any affect on the dynamic fields that get added.

I don't think the following is the best way to do this, but it's currently the closest I've come and is the simplest example code I can share reflecting my attempts.

After a new field gets added dynamically, I trigger an event:

$('#tableId').trigger('rowAddOrRemove');

And I have additional code watching for that event and attempting to re-initialize all the fields on the page:

$('#tableId').on('rowAddOrRemove', function(event){
  $('.myfieldwrapper').each(function() {
    new Craft.MyField(this);
  });
});

When I do this the initial field on the page still works but none of the dynamically added fields behave as I need them too.

Any ideas on what I'm doing wrong or what concepts I need to understand better to get this right?

2

You might be looking at a simple race conditions issue, i.e. that the rowAddOrRemove event is triggering before the new .myfieldwrapper elements have been attached to the DOM. To make sure that you don't call new Craft.MyField too early, you can try to wrap the constructor call in an anonymous requestAnimationFrame handler:

$('#tableId').on('rowAddOrRemove', function(event){
    Garnish.requestAnimationFrame(function () {
        $('.myfieldwrapper').each(function() {
            new Craft.MyField(this);
        });
    });
});

Not sure if race conditions is even the issue here, but it's the only thing I can think of, based on the code you provided.

Some other thoughts:

I'm not sure you actually need the rowAddOrRemove event – unless other parts of your application also needs to respond to that event, I'd opt for calling a simple function, rather than triggering a jQuery event, e.g.:

# ... field gets added dynamically

Garnish.requestAnimationFrame(initFields); // Instead of $('#tableId').trigger('rowAddOrRemove');

function initFields()
{
    $('.myfieldwrapper').each(function () {
        new Craft.MyField(this);
    });
}

Sidenote: If you do need to trigger an event, I'd recommend looking at Signals.js, which is a much more robust Event/Promises implementation than $.trigger.

Another thing: I'm not sure if calling Craft.MyField(this) on DOM elements that have previously been initialized is an issue, but if it is, you could add a simple classname to the initialized elements, to prevent them being initialized multiple times:

function initFields()
{
    $('.myfieldwrapper:not(.js-initialized)').each(function () {
        $(this).addClass('js-initialized');
        new Craft.MyField(this);
    });
}

Finally, if you need to destroy instances of Craft.MyField that have previously been initialized but are now removed (to prevent memory leaks or other bugs), you could do something like this (note that this example assumes that your Craft.MyField class has a destroy method that e.g. removes event listeners, deletes variables etc):

var myFieldInstances = [];

function initFields()
{
    // Destroy removed fields
    var currentFieldInstances = myFieldInstances.slice(0);
    myFieldInstances = [];
    for (var i = 0; i < currentFieldInstances.length; ++i) {
        var field = currentFieldInstances[i],
            $el = field.$el;
        if (!$el.closest('#tableId').length) {
            // This field's DOM element is no longer a child of #tableId – kill it with fire
            field.destroy();
        } else {
            myFieldInstances.push(field);
        }
    }
    // Init new fields
    $('.myfieldwrapper:not(.js-initialized)').each(function () {
        $(this).addClass('js-initialized');
        myFieldInstances.push(new Craft.MyField(this));
    });
}
  • Thanks a lot for this answer, Mats. The issue turned out to be a mis-spelled classname on the dynamic element, so the code in the question actually turns out to work. Lame. However! You're right that triggering an event here has very little appeal. Your answer helped me understand my options and refactor things to solve the issue more elegantly and get rid of that event entirely, which is the right answer. – Ben Parizek May 15 '16 at 14:35
2

In my question above I was chasing a misspelled class name with a limited understanding of how I should be approaching the problem I was asking about. Mats answer points out several considerations on how the problem could be approached differently and how the discussed code can be improved. I'd like to document one more approach to adding a listener which Mats answer helped me come to, and which is similar to how Craft initializes it's own UI elements.

Throughout the Craft javascript, as elements are dynamically added to the page, you will see that many of them also initialize UI elements:

Craft.initUiElements($container);

The initUiElements method checks the page for a handful of class names that are used in the UI (i.e. .grid, .pane, .fieldToggle, .menubtn, and more). When found, each class calls it's respective initialization method which is a custom jQuery plugin that Craft prepares in craft.js.

I ended up using a similar approach for my situation. It's quite likely my code can still be improved, so take my javascript skills with a grain of salt, but for the sake of documenting multiple approaches, this approach includes:

Create a custom jQuery plugin

Using jQuery.fn.extend() we can merge the contents of an object onto the jQuery prototype to provide new jQuery instance methods.:

$.extend($.fn,
{
    myfield: function()
    {
        return this.each(function()
        {
            if (!$.data(this, 'myfield-class'))
            {
                new Craft.MyField(this);
            }
        });
    },
});

In this example, we are passing two objects to $.extend:

  • $.fn
  • { myfield: function() { ... } }

The result allows us to access our new method directly from the DOM:

$('.myfield', $container).myfield();

Passing the second argument $container to the jQuery method allows us to restrict the behavior of our custom method to a specific context, such as elements within the container of whatever we are initializing (instead of the whole page).

Create a way to initialize your elements

Using jQuery.extend we can merge the contents of two or more objects together into the first object.:

$.extend(Craft.MyField,
{
    initFields: function($container)
    {
        $('.myfield', $container).myfield();
    }
});

In this example, we are passing two objects to $.extend:

  • Craft.MyField
  • { initFields: function($container) { ... } }

The result is that we can now use the initField method in the second object on the first object:

Craft.MyField.initField($container);

Initialize your elements when the appropriate action is triggered

With the above items in place, now, each time a specific action happens we can initialize our fields:

Craft.MyField.initFields($container);

In my case, the field I needed to initialize was within a table row. So, each time new table row is added to the page, I can now initialize the fields within that row, treating the row as the container:

addRow: function()
{
  // Do stuff that adds the table row and fields

  // Initialize the fields
  Craft.MyField.initFields($tr);
}

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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