I'm testing a Backbone View in Jasmine. When I call the view's remove method, the element isn't actually removed.
I have this event handler in my view:
onModelChange: function() {
this.$el.html('');
this.render();
}
I have to have it written that way because manually setting the html is the only way to remove it. Calling remove doesn't do anything, and when the view renders itself again it just renders the new content appended to the old content. I even tried calling remove from the developer tools in Chromium but that didn't work either. However, remove does work when I manually test it in the browser, but it doesn't work in Jasmine and it's screwing up my tests.
I think the answer to the problem lies in the jQuery source:
remove: function( selector, keepData /* Internal Use Only */ ) {
var elem,
elems = selector ? jQuery.filter( selector, this ) : this,
i = 0;
for ( ; (elem = elems[i]) != null; i++ ) {
if ( !keepData && elem.nodeType === 1 ) {
jQuery.cleanData( getAll( elem ) );
}
if ( elem.parentNode ) {
if ( keepData && jQuery.contains( elem.ownerDocument, elem ) ) {
setGlobalEval( getAll( elem, "script" ) );
}
elem.parentNode.removeChild( elem ); // right here to be specific
}
}
return this;
},
The remove function is relying on the node's parent to do the removing. I'm guessing that when the tests run in karma, the backbone view's node has no parent. To explore a bit more, I debugged the test. In the console, if I query for a view element's child and I remove it, it works.
Related
WooCommerce-tables comes with classes like these, out of the box: shop_table shop_table_responsive cart woocommerce-cart-form__contents. So no table-class, which means no nifty Bootstrap-tables.
Huh!
And since overriding the WooCommerce-templates should only be done when absolutely necessary, then let's solve it with JavaScript!
My entire site it encapsulated by a Vue-div, like so:
<div id="app">
...
<table class="shop_table shop_table_responsive cart woocommerce-cart-form__contents">
...
...
</table>
...
</div>
So initially I wrote this code, to add the table-class to all tables:
let tableSelectors = [
'.some-class table',
'.woocommerce-product-attributes',
'.woocommerce-cart-form > table'
];
for( let t = 0; t < tableSelectors.length; t++ ){
let tables = document.querySelectorAll( tableSelectors[t] );
if( tables ){
for( let i = 0; i < tables.length; i++ ){
tables[i].classList.add( 'table' );
}
}
}
... Putting that in the mounted(){ ... }-section.
That worked! So far so good.
But WooCommerce is using jQuery quite a lot. And on the cart page, if I change the quantity (and press 'Update'), then the table-contents are updated using AJAX. If you're curious how it works, then you can check it out here.
And when that runs, I assume that WooCommerce grabs the initial cart-template and reloads that whole table; without the newly added table-class. Bah humbug!
So how can I solve this?
I can override the WooCommerce ./cart/cart.php-template and add the
class to the template. Seems like quite the overkill for adding a class.
I can scan the DOM for tables every second (or so) and apply the table class, if it's not there. Not cool... Regardless if it's done using jQuery or Vue.
Since the whole table is being replaced in the DOM, then it doesn't work to monitor the current table (using watch(){...} in Vue) and apply the class if it changes, - since it never changes (it's replaced).
I'm unable to find a Hook that I can use.
I also tried using ajaxComplete, but I can see in the network-tab that the XHR-request is firing, but this code here is never doing anything (in the console):
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
console.log( 'Test' );
});
Any other suggestions?
You could use the Mutation Observer API to listen for changes to a wrapper element's contents and re-apply the table classes.
This example is lifted nearly verbatim from the sample code on MDN. Clicking the button replaces the contents of the div, which you can see from the console output fires the observer callback.
// Select the node that will be observed for mutations
const targetNode = document.getElementById('some-id');
// Options for the observer (which mutations to observe)
const config = {
childList: true,
subtree: true
};
// Callback function to execute when mutations are observed
const callback = function(mutationsList, observer) {
for (let mutation of mutationsList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
}
}
};
// Create an observer instance linked to the callback function
const observer = new MutationObserver(callback);
// Start observing the target node for configured mutations
observer.observe(targetNode, config);
function doUpdate() {
targetNode.innerText = Math.random();
}
document.querySelector('button').addEventListener('click', doUpdate);
<div id="some-id">(container)</div>
<button>change</button>
The ajaxComplete() function of jQuery can do what you expected,
I don't know why it's not working for you.
I just open the link of cart page you gave above and paste add the below code in the developer console
and it works as expected after each update of the cart, the "table" class successfully appended to the table as per given selectors.
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});
It looks like you have not added the code in the proper place. Since ajaxComplete() function is dependent on jQuery you need to execute the above code after jQuery has been loaded successfully. To do that you can use wp_add_inline_script() function with wp_script_is()
Add the following code in your function.php file, It will add the below script to page after jQuery finish loading.
function my_custom_script() {
if ( ! wp_script_is( 'jquery', 'done' ) ) {
wp_enqueue_script( 'jquery' );
}
wp_add_inline_script( 'jquery-migrate', 'jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery(".some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table").addClass("table");
});' );
}
add_action( 'wp_enqueue_scripts', 'my_custom_script');
You can try to change the jquery ajax function
(function ($) {
var _oldAjax = $.ajax;
$.ajax = function (options) {
return _oldAjax(options).done(function (data) {
$('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
if (typeof (options.done) === "function")
options.done();
});
};
})(jQuery);
above code must be added before any ajax that is called
i've tested it on your site using the javascript console and actually
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
console.log( 'Test' );
});
fire pretty well.
maybe you are adding that code on wrong place.. or maybe you have "Group Similar" flagged on javascript console settings and you didn't notice the log
so you could just put your code together like this:
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
try{
let tableSelectors = [
'.some-class table',
'.woocommerce-product-attributes',
'.woocommerce-cart-form > table'
];
for( let t = 0; t < tableSelectors.length; t++ ){
let tables = document.querySelectorAll( tableSelectors[t] );
if( tables ){
for( let i = 0; i < tables.length; i++ ){
tables[i].classList.add( 'table' );
}
}
}
console.dir("ok");
}catch(ex){console.dir(ex);}
});
or use a jquery like solution like this:
jQuery( document ).ajaxComplete(function( event, xhr, settings ) {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});
i've tested both of them directly on your site and both solutions works pretty well
and of course if you want make it works even at start the same script must be applied to jQuery( document ).ready() too:
jQuery( document ).ready(function() {
jQuery('.some-class table, .woocommerce-product-attributes, .woocommerce-cart-form > table').addClass("table");
});
As of jQuery v3.0 the recommended way to bind DOM-ready-methods is $( myReadyFunction ) without any selector or delegation. Especially $(document).on('ready', myReadyFunction ) is removed in jQuery v3.
I now need to modify/override a specific, pre-existing ready-method before it is executed. I know that I can delay the execution of the ready-event by calling $.holdReady( true ) ... but how can I remove/override my old myReadyFunction ?
Existing code that should not be changed:
myReadyFunction = function() { console.info('old'); }
$( myReadyFunction );
I can add code before or after this part, here are some things I tried and did not work:
adding after
$.off( 'myReadyFunction' );
=> $.off is not a function
adding after
$(document).off( 'myReadyFunction' );
$(document).off( 'ready', 'myReadyFunction' );
=> no error messsage, but both variants don't work
myReadyFunction = function() { console.info('new'); }
$( myReadyFunction );
=> adds the new, overridden function, but does not remove the old one
I am trying to implement the following code snippet into my project: http://jsfiddle.net/Zmf6t/. But I am running into a bit of difficulty; I have wrapped the JavaScript in the jQuery(document).ready() callback function, and checking it in the Chrome debugger shows that the script is running. However, when I get to the if( jQuery(this).val() == "-1" ) (as my <select> value has -1 as its value), the debugger reports that the elements value property is "-1", but the statement within the if is never evaluated. Instead the else property is evaluated.
Does anyone have any idea why this might be happening? I cannot seem to isolate the issue to provide a minimal working example; my jQuery is as below:
jQuery(document).ready( function() {
jQuery('#test').change( function() {
if( jQuery(this).val() == "-1" )
{
jQuery(this).addClass( "empty-combo" );
}
else
{
jQuery(this).removeClass( "empty-combo" );
}
});
jQuery('#test').change();
});
Change your condition to:
if( jQuery(this).val() == null )
It worked.
Is there an elegant way in Marionette to test if a region's (el)ement exists in the DOM after the view is rendered? Preferably without rewriting the region selector and using jQuery to search the DOM.
For example - this layout view:
var view = new Marionette.LayoutView({
regions : {
'header' : '.header',
'footer' : '.footer'
},
onRender : function() {
if ( /* test for the existance of 'header' in the dom */ ) {
// do something
}
}
});
Elegant? No. But Marionette's view.getRegion()._ensureElement() returns true when the element exists and will throw an error if the element does not exist. So you could try...
onRender : function() {
try {
view.getRegion("header")._ensureElement()
// element exists
} catch {
//element does not exist
}
The annotated marionette source also suggests that there is a allowMissingEl option that you could set to have _ensureElement() return false when the element does not exist, but this might have negative repercussions for the rest of your project, and could potentially make debugging more difficult.
Looking at the source, it looks like Morslamina's answer is correct - however we can extend Marionette's region class and implement the testing behavior ourselves. For example:
var BaseRegion = Marionette.Region.extend({
// tests if the element exists for this region or not
hasEl : function() {
if ( _.isUndefined(this.getEl(this.el)[0]) ) {
return false;
}
return true;
}
});
and then in the layout view go
var Layout = Marionette.Layout.extend({
regionClass: BaseRegion,
regions : {
'header' : '.header',
'footer' : '.footer'
},
onRender : function() {
if ( this.getRegion('header').hasEl() ) {
// do something, e.g. show the region
}
}
});
I can't find any good resources on how custom events in jquery are actually implemented. Like how they simulate the event bubbling, etc.
This way:
// bubbling is internal
trigger: function( event, data, elem /*, bubbling */ ) {
// Event object or event type
var type = event.type || event,
bubbling = arguments[3];
// Handle a global trigger
if ( !elem ) {
// Don't bubble custom events when global (to avoid too much overhead)
event.stopPropagation();
// Only trigger if we've ever bound an event for it
if ( jQuery.event.global[ type ] ) {
jQuery.each( jQuery.cache, function() {
if ( this.events && this.events[type] ) {
jQuery.event.trigger( event, data, this.handle.elem );
}
});
}
}
// ... snip ...
// Trigger the event, it is assumed that "handle" is a function
var handle = elem.nodeType ?
jQuery.data( elem, "handle" ) :
(jQuery.data( elem, "__events__" ) || {}).handle;
if ( handle ) {
handle.apply( elem, data );
}
var parent = elem.parentNode || elem.ownerDocument;
// ... snip ....
if ( !event.isPropagationStopped() && parent ) {
jQuery.event.trigger( event, data, parent, true );
} else if ( !event.isDefaultPrevented() ) {
// ... snip ...
jQuery.event.triggered = true;
target[ targetType ]();
}
}
What's going on here is as follows:
When trigger is called jQuery checks to see if the event is being triggered globally ($.trigger("event_name");).
If it is not being triggered globally, and propagation has not been stopped and the element in question has a parent element (!event.isPropagationStopped() && parent) then jQuery calls the trigger event manually on the parent element.
jQuery.event.trigger( event, data, parent, true );
There is quite a bit more going on -- see event.js in the jQuery source code.
Check out the tutorials
$(document).bind("eventType", ...);
// This is equivalent to the plugin's $.subscribe("eventType", ...);
$(document).trigger("eventType");
// equivalent to plugin's $.publish("eventType");
Also checkout this SO question