elementor icon indicating copy to clipboard operation
elementor copied to clipboard

Running JS After All Elements are Ready on Frontend

Open meceware opened this issue 6 years ago • 24 comments

Hi,

I'm looking for a way to run code after all elements are ready on frontend.

Basically, I'm setting the necessary classes and data attributes on frontend/element_ready/section action (elementorFrontend.hooks), however I need to run a function after the preview is ready and all elements are loaded on the backend preview. On the real page, I solved this using window.onLoad but the backend is the problem since the preview loads in the iframe. There is no information about it on the JS Hooks documentation page either.

Is there a way to run a code after all elements are ready? Otherwise can you guys add this as a feature request?

Thank you, cheers,

meceware avatar Dec 05 '19 15:12 meceware

In the backend, instead of window.onLoad, you can use elementor/frontend/init on window and then add your frontend hook and its handler function. Use the elementor/frontend/after_enqueue_scripts action to enqueue this file.

parasshah195 avatar Dec 06 '19 07:12 parasshah195

Thank you for the answer. I would like to explain more. The code is below, which is loaded in the js file elementor/frontend/after_enqueue_scripts.

( function( $ ) {
  "use strict";

  $( window ).on( 'elementor/frontend/init', function() {
    var FrontEndExtended = elementorModules.frontend.handlers.Base.extend( {
      getFieldValue: function( option, raw = false ) {
        var modelCID = this.getModelCID();
        if ( modelCID ) {
          var settings = elementorFrontend.config.elements.data[ modelCID ];
          if ( settings ) {
            return settings.get( option );
          }
        }

        return this.getElementSettings( option );
      },

      isInner: function() {
        return this.getFieldValue( 'isInner', true )
      },

      run: function() {

        if ( this.isInner() ) {
          if ( elementorFrontend.getPageSettings( 'disable-anchors' ) !== 'yes' ) {
            var anchor = this.getFieldValue( 'slide-anchor' );
            if ( anchor ) {
              this.$element.attr( 'data-anchor', anchor );
            } else {
              this.$element.removeAttr( 'data-anchor' );
            }
          } else {
            this.$element.removeAttr( 'data-anchor' );
          }
        } else {
          this.$element.addClass( 'my-section' );
        }
      },

      onElementChange: function( option ) {
        this.run();
      },

      onInit: function() {
        elementorModules.frontend.handlers.Base.prototype.onInit.apply(this, arguments);
        this.run();
      },

      onDestroy: function() {
        elementorModules.frontend.handlers.Base.prototype.onDestroy.apply(this, arguments);
        // TODO: anthing to deactivate?
      }
    } );

    elementorFrontend.hooks.addAction( 'frontend/element_ready/section', function( $element ) {
      if ( 'section' === $element.data( 'element_type' ) ) {
        new FrontEndExtended( {
            $element: $element
        } );
      }
    });
  } );

  // TODO: the actual script is needed to run after all sections are finished.

} ( jQuery ) );

But the actual scripts need to run after all the sections are ready. How can I do this?

Thank you,

meceware avatar Dec 06 '19 07:12 meceware

@meceware, I believe the hook elementor/frontend/init loads after Elementor is fully loaded, which means, the code within that event should run after all the sections are ready.

So, if you add your code after the hook, it should work fine. Are you getting results otherwise?

parasshah195 avatar Dec 07 '19 05:12 parasshah195

Hi,

Sorry again for not being descriptive enough.

The thing here is, the sections are ready on elementor/frontend/init but I have some preparing to do inside frontend/element_ready/section. After that preparing, I need to run my code. So I need to know when all the frontend/element_ready actions are called.

Thank you,

meceware avatar Dec 07 '19 05:12 meceware

That I understood from your code above. But, did you try executing your final code after the hook within the .on('elementor/frontend/init')? If it's not working, then is it giving any error?

You can try logging output from that final code and your run function to determine which one runs last.

Also, the frontend/element_ready/section runs everytime whenever a new section is added. So, if you want your code to re-execute after every section is added, then you need to make it part of the class that is hooked to the action.

parasshah195 avatar Dec 07 '19 05:12 parasshah195

Hi,

Thank you for your response. When I add the console.log outputs as in

( function( $ ) {
  "use strict";

  $( window ).on( 'elementor/frontend/init', function() {
    var FrontEndExtended = elementorModules.frontend.handlers.Base.extend( {
      getFieldValue: function( option, raw = false ) {
        var modelCID = this.getModelCID();
        if ( modelCID ) {
          var settings = elementorFrontend.config.elements.data[ modelCID ];
          if ( settings ) {
            return settings.get( option );
          }
        }

        return this.getElementSettings( option );
      },

      isInner: function() {
        return this.getFieldValue( 'isInner', true )
      },

      run: function() {

        if ( this.isInner() ) {
          if ( elementorFrontend.getPageSettings( 'disable-anchors' ) !== 'yes' ) {
            var anchor = this.getFieldValue( 'slide-anchor' );
            if ( anchor ) {
              this.$element.attr( 'data-anchor', anchor );
            } else {
              this.$element.removeAttr( 'data-anchor' );
            }
          } else {
            this.$element.removeAttr( 'data-anchor' );
          }
        } else {
          this.$element.addClass( 'my-section' );
        }

        console.log('FrontEndExtended after run');
      },

      onElementChange: function( option ) {
        this.run();
      },

      onInit: function() {
        elementorModules.frontend.handlers.Base.prototype.onInit.apply(this, arguments);
        console.log('FrontEndExtended onInit');
        this.run();
      },

      onDestroy: function() {
        elementorModules.frontend.handlers.Base.prototype.onDestroy.apply(this, arguments);
        // TODO: anthing to deactivate?
      }
    } );

    elementorFrontend.hooks.addAction( 'frontend/element_ready/section', function( $element ) {
      console.log('frontend/element_ready/section');
      if ( 'section' === $element.data( 'element_type' ) ) {
        new FrontEndExtended( {
            $element: $element
        } );
      }
    });

    console.log('after elementor/frontend/init');
  } );

  // TODO: the actual script is needed to run after all sections are finished.

} ( jQuery ) );

The outpus are

after elementor/frontend/init
frontend/element_ready/section
FrontEndExtended onInit
FrontEndExtended after run
frontend/element_ready/section
FrontEndExtended onInit
FrontEndExtended after run

So if I put my code after elementor/frontend/init, it doesn't give any errors however it doesn't work either because it requires the initialization in FrontEndExtended after run event, which runs last.

meceware avatar Dec 07 '19 06:12 meceware

So, it seems that the hook actually runs later. In that case, sorry, no idea.

@bainternet, can you chime in and help with this?

parasshah195 avatar Dec 08 '19 09:12 parasshah195

any news about this?

r1si avatar May 12 '20 12:05 r1si

Nope! I don't think there ever will be.

meceware avatar May 12 '20 13:05 meceware

This was my solution it works, and yes I am ashamed of it

$( window ).on( 'elementor/frontend/init', function (){ 
    setTimeout( function() {
        alert('Your code here');
    }, 1000 );
} );

5imun avatar Aug 24 '20 20:08 5imun

Same issue here - I also solved it with a timeout... this is a bug in the frontend init code of Elementor and has to be fixed on their end.

mbaierl avatar May 27 '21 15:05 mbaierl

Same thing here. The timeout trick solved it :)

webastra-fr avatar Jul 30 '21 15:07 webastra-fr

I figured out why all event handlers for the window's load event are getting cancelled out. It's waypoints.min.js which comes packaged with elementor and may or may not be loaded (because you may not have scroll-triggered animations ... and this would explain why results will vary).

I filed #619 on the waypoints repo but I haven't seen any updates to that code for some time.

Elementor devs: can we rethink using waypoints? or rewrite the waypoints code used in the elementor plugin? window's listeners/handlers need to be attached properly like this https://stackoverflow.com/a/30915025/8524018

jrevillini avatar Aug 31 '21 19:08 jrevillini

What sucks is that the override occurs outside any function call in waypoints, so I can't even envision writing any code that would avoid this. I was about to code a PHP snippet and do a wp_add_inline_script( 'waypoints', ... ) to fix it but it's just firing in the void of an anonymous function! lol

jrevillini avatar Aug 31 '21 19:08 jrevillini

this isn't a solution but a temporary workaround:

function jrevillini_rewire_waypoints() {
    $wp_scripts = wp_scripts();

    if ( wp_script_is( 'elementor-waypoints' ) ) {
		// LOCATION IS TEMPORARY! Get your own copy of this from https://gist.github.com/jrevillini/0bf9853d5106d4e4303c4cbf1b5d79c0#file-jquery-waypoints-min-js
        $wp_scripts->registered['elementor-waypoints']->src = 'https://winsted.net/wp-content/uploads/tmp/jquery.waypoints.min.js';
    }
}
add_action( 'elementor/frontend/after_enqueue_scripts', 'jrevillini_rewire_waypoints' );

I recommend downloading that JS file and hosting it your own way until this is fixed in elementor. I sent a pull request to waypoints but idk if I did it right or if they'll ever merge it.

jrevillini avatar Aug 31 '21 21:08 jrevillini

This is a solution to detect the fully loaded status I came up with, which is better than a timeout I think.

        var countedElements = 0;
	elementorFrontend.hooks.addAction('frontend/element_ready/global', function ($scope) {
		var elementorElCount = document.querySelectorAll('.elementor-element').length;
		countedElements++;
		if (elementorElCount == countedElements) {
			var event = new CustomEvent( 'cmplz_elementor_loaded' );
			document.dispatchEvent(event);
		}
	});

      document.addEventListener('cmplz_elementor_loaded', function(e){
           console.log("Elementor fully loaded");
     });

rlankhorst avatar Nov 12 '21 17:11 rlankhorst

none of the suggested solutions works on the current version.

symplTech avatar Mar 30 '22 16:03 symplTech

In my projects I use the following code:

<script>
// When the window was loaded
window.addEventListener( 'DOMContentLoaded', () => {

	// When Elementor was loaded
	window.addEventListener( 'elementor/frontend/init', () => {

		// Optional, wait 2 seconds...
		setTimeout( () => {

			// Do something...

		}, 2000);

	});

});
</script>

Vanilla JS code, no jQuery. Hope this will help...

rami-elementor avatar Apr 26 '22 16:04 rami-elementor

Dude, you can't even write a freaking JS code for your widget. What the hell is this nonsense!!!!!!!!!

babakfp avatar Apr 26 '22 19:04 babakfp

I'm done wasting my time with this crap😂

babakfp avatar Apr 26 '22 19:04 babakfp

All I wanted was a simple, easy and necessary hook but all I got is timeout suggestions! I really don't like when people suggest using timeouts like it's 90s :)

meceware avatar Apr 26 '22 19:04 meceware

I have solved the problem with custom js with very effectually way..

M-Bappy avatar Sep 12 '22 04:09 M-Bappy

@M-Bappy , can you share? :)

meceware avatar Sep 12 '22 07:09 meceware

@M-Bappy please share your solution :)

woodydaniel avatar Sep 21 '22 17:09 woodydaniel

This is a solution to detect the fully loaded status I came up with, which is better than a timeout I think.

        var countedElements = 0;
	elementorFrontend.hooks.addAction('frontend/element_ready/global', function ($scope) {
		var elementorElCount = document.querySelectorAll('.elementor-element').length;
		countedElements++;
		if (elementorElCount == countedElements) {
			var event = new CustomEvent( 'cmplz_elementor_loaded' );
			document.dispatchEvent(event);
		}
	});

      document.addEventListener('cmplz_elementor_loaded', function(e){
           console.log("Elementor fully loaded");
     });

Wow, thank you - this actually does it for me. Thank you!

Deichscheich avatar Nov 09 '22 15:11 Deichscheich

Not the best solution, but way better than silly setTimeout functions

I use it as a script tag in the render() function of my widget.

Works for me

<script>
 try {
        initWhateverFunction();
    } catch(e) {
    
    }
    document.addEventListener("DOMContentLoaded", function() {
        initWhateverFunction();
    });
</script>

Works for me

patrickzach avatar Feb 01 '23 18:02 patrickzach

yeah it's too late. but maybe this works for others. did you try elementor.on('document:loaded') inside window.addEventListener( 'elementor/frontend/init' ) ?

( () => {

	window.addEventListener( 'elementor/frontend/init', () => {

		elementor.on( 'document:loaded', () => {

			// your code here
			const $body = elementorFrontend.elements.$body;
			console.log( $body.find( '.elementor-element' ) );

		} )

	} );

} )();

Mohsen-Khakbiz avatar Mar 07 '23 10:03 Mohsen-Khakbiz

This method works better because Elementor is not returning the same amount of elements like querySelectorAll returns.

$(window).on('elementor/frontend/init', () => {
  elementorFrontend.on('components:init', () => {
      const elements = document.querySelectorAll('.elementor-element');
      elements.forEach((e, i) => {
          if (elements.length-1 == i) {
              // your code here
          }
      });
  });
});

beratgashi avatar Mar 23 '23 11:03 beratgashi

Coming back to this after a year. I looked at the comments and the docs and I find my solution.

// 'elementor/frontend/init' loads after 'load'.
window.addEventListener( 'elementor/frontend/init', () => {
	// This will only run if a widget named 'fp-accordion' was used. Don't remove '.default'.
	elementorFrontend.hooks.addAction( 'frontend/element_ready/fp-accordion.default', function() {
		console.log(document.body)
	})
})

babakfp avatar May 22 '23 19:05 babakfp

Count me in with the "back-to-the-90s crowd that has to rely on setTimeout", because that's the only way I could get a popup to show up somewhat predictably. It's so ugly that it hurts.

All I want is to trigger a popup upon page load myself using JavaScript. It shouldn't take ultra ugly workarounds.

Related issue: https://github.com/elementor/elementor/issues/7077

llarsson avatar Aug 02 '23 08:08 llarsson