Running JS After All Elements are Ready on Frontend
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,
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.
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, 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?
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,
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.
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.
So, it seems that the hook actually runs later. In that case, sorry, no idea.
@bainternet, can you chime in and help with this?
any news about this?
Nope! I don't think there ever will be.
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 );
} );
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.
Same thing here. The timeout trick solved it :)
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
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
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.
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");
});
none of the suggested solutions works on the current version.
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...
Dude, you can't even write a freaking JS code for your widget. What the hell is this nonsense!!!!!!!!!
I'm done wasting my time with this crap😂
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 :)
I have solved the problem with custom js with very effectually way..
@M-Bappy , can you share? :)
@M-Bappy please share your solution :)
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!
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
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' ) );
} )
} );
} )();
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
}
});
});
});
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)
})
})
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