Ian Jones
Ian Jones

Reputation: 1509

How to re-run JavaScript when DOM mutates?

I'm using Template.rendered to setup a dropdown replacement like so:

Template.productEdit.rendered = function() {
    if( ! this.rendered) {
        $('.ui.dropdown').dropdown();
        this.rendered = true;
    }
};

But how do I re-run this when the DOM mutates? Helpers return new values for the select options, but I don't know where to re-execute my .dropdown()

Upvotes: 2

Views: 716

Answers (3)

Ian Jones
Ian Jones

Reputation: 1509

Here's my tentative answer, it works but I'm still hoping Meteor has some sort of template mutation callback instead of this more cumbersome approach:

Template.productEdit.rendered = function() {
    if( ! this.rendered) {
        $('.ui.dropdown').dropdown();

        var mutationOptions = {
            childList: true,
            subtree: true
        }

        var mutationObserver = new MutationObserver(function(mutations, observer){
            observer.disconnect(); // otherwise subsequent DOM changes will recursively trigger this callback

            var selectChanged = false;

            mutations.map(function(mu) {
                var mutationTargetName = Object.prototype.toString.call(mu.target).match(/^\[object\s(.*)\]$/)[1];
                if(mutationTargetName === 'HTMLSelectElement') {
                    console.log('Select Changed');
                    selectChanged = true;
                }
            });

            if(selectChanged) {
                console.log('Re-init Select');
                $('.ui.dropdown').dropdown('restore defaults');
                $('.ui.dropdown').dropdown('refresh');
                $('.ui.dropdown').dropdown('setup select');
            }

            mutationObserver.observe(document, mutationOptions); // Start observing again
        });

        mutationObserver.observe(document, mutationOptions);

        this.rendered = true;
    }
};

This approach uses MutationObserver with some syntax help I found here

Upvotes: 1

Suppen
Suppen

Reputation: 856

I think you don't want this to run before the whole DOM has rendered, or else the event handler will run on EVERY element being inserted:

var rendered = false;
Template.productEdit.rendered = function() {rendered: true};

To avoid rerunning this on elements which are already dropdowns, you could give new ones a class which you remove when you make them into dropdowns

<div class="ui dropdown not-dropdownified"></div>

You could add an event listener for DOMSubtreeModified, which will do something only after the page has rendered:

Template.productEdit.events({
    "DOMSubtreeModified": function() {
        if (rendered) {
            var newDropdowns = $('.ui.dropdown.not-dropdownified');
            newDropdowns.removeClass("not-dropdownified");
            newDropdowns.dropdown();
        }
    }
});

This should reduce the number of operations done when the event is triggered, and could stop the callstack from being exhausted

Upvotes: 2

Suppen
Suppen

Reputation: 856

Taking ad educated guess, and assuming you are using the Semantic UI Dropdown plugin, there are four callbacks you can define:

onChange(value, text, $choice): Is called after a dropdown item is selected. receives the name and value of selection and the active menu element

onNoResults(searchValue): Is called after a dropdown is searched with no matching values

onShow: Is called after a dropdown is shown.

onHide: Is called after a dropdown is hidden.

To use them, give the dropdown() function a parameter:

$(".ui.dropdown").dropdown({
    onChange: function(value, text, $choice) {alert("You chose " + text + " with the value " + value);},
    onNoResults: function(searchValue) {alert("Your search for " + searchValue + " returned no results");}
    onShow: function() {alert("Dropdown shown");},
    onHide: function() {alert("Dropdown hidden");}
});

I suggest you read the documentation of all plugins you use.

Upvotes: -1

Related Questions