Categories
Advanced

Hash File Suffixing

Adding a hash to a file suffix is a common way to force the browser to update its cache.

Angular for example offers this option by default in its build process. Whenever you bundle your code with Angular you will see a hash suffix to the bundled files, which are results of hash functions on the file contents. You can then immediately deploy these files about browser caching without concerns.But not all frameworks offer this. 

Svelte does not automatically bundle files with hash suffixes. You would need to install rollup-plugin-hash, for example. But I feel that a better method is to add the hash after bundling instead.

Often, after I bundle my code, I will need to copy it somewhere else on my machine before I deploy it to a live server.  To do this I use gulp. Gulp is a great tool to create tasks and automate your work. My copy-output task using Gulp looks like this:

var fs = require('fs');
var gulp = require('gulp');
const del = require('del'); 
 
var json = JSON.parse(fs.readFileSync('./dist_folder_details.json')); // this file contains the destination of the bundles on my machine
    gulp.task('copy-output', async () => {
       var distFolder = json['dist-folder-js'];
       await del([distFolder + '/**', '!' + distFolder], 
       {force: true}); // I used del ( 
       https://www.npmjs.com/package/del) to remove   
       the current content
       await new Promise((resolve, reject) => {
         gulp.src('./public/build/*.js')
         .pipe(gulp.dest(distFolder)) // I am copying the 
         new build files to the destination
         .on("end", resolve);
     });
});

So, all I need to do in order to add a hash suffix is to use gulp-hash:

var fs = require('fs');
var gulp = require('gulp');
const del = require('del');
var hash = require('gulp-hash'); // require it
 
var json = JSON.parse(fs.readFileSync('./dist_folder_details.json')); 
 
var json = JSON.parse(fs.readFileSync('./dist_folder_details.json')); 
gulp.task('copy-output', async () => {
    var distFolder = json['dist-folder-js'];
    await del([distFolder + '/**', '!' + distFolder], 
    {force: true}); 
    await new Promise((resolve, reject) => {
        gulp.src('./public/build/*.js')
        .pipe(hash()) // Use it
        .pipe(gulp.dest(distFolder)) 
        .on("end", resolve);
    });
});

Now hashing is integrated into my deploy process. No need to install framework-specific hashing packages.

Thoughts? Comments? Suggestions?  Please let me know below!!

Categories
Advanced

Svelte vs jQuery Bootstrap Progress Bar

I wanted to check what would be easier: implementing a progress bar with jQuery or with Svelte. So I implemented both. I intended to create a progress bar that goes through some list of items so it would also have control buttons, one for start, one for pausing and another for resetting. 

The control buttons need to be user friendly, so when the progress bar is loading the start button and reset button need to be disabled, when paused the pause button should be disabled and when done the reset button should be the only one to be enabled.

jQuery

Check Demo….

The HTML looks like this:

<head>
    <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
    <script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
    <script src="progress.js"></script>
</head>
 
<div>
    <div class="progress" style="margin: 20px;">
        <div id="progress-bar" class="progress-bar progress-bar-striped" role="progressbar" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="width: 0%">0%</div>
    </div>
    <div style="text-align: center;">
        <button id="start-button" type="button" class="btn btn-primary">Start</button>
        <button id="pause-button" disabled="true" type="button" class="btn btn-secondary">Pause</button>
        <button id="reset-button" disabled="true" type="button" class="btn btn-danger">Reset</button>
    </div>
</div>

As you can see I’m importing jQuery and bootstrap.

Attempt 01:

Initially, I tried using jQuery with no custom events, I’m just hooking into button click events. The code looks like this:

$(document).ready(() => {
    var bar = $("#progress-bar");
    var currIndex = 0;
    const maxIndex = 5;
    var stop = false;

    var resetButton = $("#reset-button").on("click", () => 
    {
        setBar(0);
        currIndex = 0;
        setDisabledState(resetButton, true);
        setDisabledState(startButton,false);
    });

    var pauseButton = $("#pause-button").on("click", () => 
    {
        stop = true;
        setDisabledState(pauseButton, true);
    });

    var startButton = $("#start-button").on("click", () => 
    {
        setDisabledState(pauseButton, false);
        setDisabledState(startButton, true);
        setDisabledState(resetButton, true);
        bar.toggleClass("progress-bar-animated");
        nextItem();

    });

    function incrementIndex(){
        currIndex++;
        if(currIndex == maxIndex){
            stop = true;
            setDisabledState(startButton, true);
            setDisabledState(pauseButton, true);
        }
    }

    function setBar(val){
        bar.width(val + "%");
        bar.text(val + "%");
    }

    function setDisabledState(elem, disabled){
        elem.prop('disabled', disabled);
    }

    function nextItem(){
        setTimeout(() => {
            incrementIndex();
            setBar(currIndex/maxIndex*100);
            if(stop){
                bar.toggleClass("progress-bar-animated");
                stop = false;
                setDisabledState(resetButton, false);
                if(currIndex<maxIndex) setDisabledState(startButton,false);
                return;
            }
            nextItem();
        }, 1000)
    }

As you can see I’m struggling to maintain the logic I wanted, the code is both complicated to write and read.

Attempt 02- with custom events: 

With custom events, the code is a little longer but much more readable and conceptually easier to write. I basically have the progress bar listen to four events: loading, pause, reset, and done. Each of the buttons trigger these events, and the ‘done’ event is triggered when we’re completed with all the items. Final code looks like this:

$(document).ready(() => {
    var bar = $("#progress-bar");
    var currIndex = 0;
    const maxIndex = 5;
    var stop = false;
 
    bar.on("loading", () => {
        setDisabledState(pauseButton, false);
        setDisabledState(startButton, true);
        setDisabledState(resetButton, true);
        bar.toggleClass("progress-bar-animated");
    });
 
    bar.on("pause", () => {
        stop = true;
        setDisabledState(pauseButton, true);
    });
 
    bar.on("reset", () => {
        setBar(0);
        currIndex = 0;
        setDisabledState(resetButton, true);
        setDisabledState(startButton,false);
    });
 
    bar.on("done", () => {
        stop = true;
        setDisabledState(startButton, true);
        setDisabledState(pauseButton, true);
    });
 
    bar.on("stop", () => {
        bar.toggleClass("progress-bar-animated");
        stop = false;
        setDisabledState(resetButton, false);
        if(currIndex<maxIndex) setDisabledState(startButton,false);
    });
 
    var resetButton = $("#reset-button").on("click", () => 
    {
        bar.trigger("reset");
    });
 
    var pauseButton = $("#pause-button").on("click", () => 
    {
        bar.trigger("pause");
    });
 
    var startButton = $("#start-button").on("click", () => 
    {
        bar.trigger("loading");
        nextItem();
    });
 
    function incrementIndex(){
        currIndex++;
        if(currIndex == maxIndex){
            bar.trigger("done");
            
        }
    }
 
    function setBar(val){
        bar.width(val + "%");
        bar.text(val + "%");
    }
 
    function setDisabledState(elem, disabled){
        elem.prop('disabled', disabled);
    }
 
    function nextItem(){
        setTimeout(() => {
            incrementIndex();
            setBar(currIndex/maxIndex*100);
            if(stop){
                bar.trigger("stop");
                return;
            }
            nextItem();
        }, 1000)
    }
 
});

To sum up, without custom events jQuery is not easy to construct, but custom events make it a little better. Next: Svelte.

Svelte


I’m rather new to Svelte but I managed to create my progress bar with three components. The top component is the app itself which contains all of the logic connecting the control buttons to the progress bar. Upon starting the app I will go through some prefixed number of items (in this case 5), and will set a timeout between each to simulate some asynchronous action.

The other two components are the control buttons and the progress bar. 

In order to get it right I had to import the bootstrap SCSS into my app:

<svelte:head>
    @import "bootstrap/scss/functions";
 
    @import "bootstrap/scss/variables";
 
    @import "bootstrap/scss/mixins";
 
    @import 'bootstrap/scss/_progress.scss'; 
 
   @import 'bootstrap/scss/_buttons.scss';
 
</svelte:head>

Afterwards, I implemented the ControlButtons and ProgressBar components.

First, the ProgressBar will look like this: (TODO: fix it so it’s also striped)

<script>
    import Progress from 'sveltestrap/src/Progress.svelte';
 
    export let progressPercent = 0;
    export let loading = false;
</script>
 
<Progress value={progressPercent} animated={loading} striped=true>{progressPercent + "%"}</Progress>

You can see it has two input variables, one to show its loading, and another for the actual progress percentage.

Next, the control buttons:

<script>
    import { createEventDispatcher } from 'svelte';
    import { Button, Row } from "sveltestrap";
 
    export let disablePause = true;
    export let disableStart = false;
    export let disableReset = true;
    const dispatch = createEventDispatcher();
 
    function start(){
        dispatch('start');
    }
 
    function pause(){
        dispatch('pause');
    }
 
    function reset(){
        dispatch('reset');
    }
 
</script>
<div style="text-align: center">
    <Button color="primary" on:click={start} disabled={disableStart}>Start</Button>
    <Button color="secondary" on:click={pause} disabled={disablePause}>Pause</Button>
    <Button color="danger" on:click={reset} disabled={disableReset}>Reset</Button>
</div>

You can see that there are control elements to disable the buttons and there are output elements for binding with the clicks.

Finally, I implemented the logic in the app component:

<svelte:head>
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css">
</svelte:head>
<script>
    import ProggressBar from './ProgressBar.svelte';
    import ControlButtons from './ControlButtons.svelte';
    let currIndex = 0;
    let loading = false;
    let paused = false;
    const maxIndex = 5;
 
    $: done = maxIndex == currIndex;
 
    function nextItem(){
        setTimeout(() => {
            currIndex++;
            if(currIndex == maxIndex || paused){
                paused = false;
                loading = false;
                return;
            }
            nextItem();
        }, 1000)
    }
 
    function start(){
       loading = true;
       nextItem();
    }
 
    function pause(){
        paused = true;
    }
 
    function reset(){
        currIndex = 0;
    } 
</script>
 
<div style="margin: 20px;">
    <ProggressBar progressPercent={currIndex/maxIndex*100} loading={loading}></ProggressBar>
</div>
<ControlButtons on:start={start} on:pause={pause} on:reset={reset} 
disableStart={loading || done} disablePause={!loading} disableReset={currIndex==0 || loading}></ControlButtons>

As you can see, the main logic here deals with disabling and enabling the buttons whenever necessary. But svelte does make it simpler. So given that it took me only about an hour’s work, I believe Svelte is the way to go and definitely deserves a thumbs up.

Categories
Advanced

Asynchronous Actions in WordPress Table

In a previous post we added a “Select All” button to the Woocommerce product table. In this tutorial, we will create an asynchronous action that retrieves all of the table product Ids without refreshing the page.

Initially, define our action:

add_action( 'bulk_actions-edit-product', function($bulk_actions){
	$bulk_actions['my-action'] = __( 'Asynchornously get ids', 'my-action');
	return $bulk_actions;	
});

Now we have this option in the products table:

Now we will add the following Javascript:

add_action('current_screen', function($screen){
	if($screen->id == "edit-product"){
			?>
	<script>
	     document.addEventListener("DOMContentLoaded", function() {
                     jQuery("#posts-filter").on("submit", function(ev){
	             var action1 = jQuery("[name=action]").val();
		     if(action1!="my-action") return;
                     ev.preventDefault();

        //this is where we serialize the form
	
                     var unindexed_array =jQuery("#posts-filter").serializeArray();
		     var indexed_array = {};

		     jQuery.map(unindexed_array, function(n, i){
			   indexed_array[n['name']] = n['value'];
		     });

		     jQuery.ajax({
			    url: "admin-ajax.php",
			    type: 'GET',
			    dataType: 'json', // added data type
			    data: indexed_array,
			    success: function(res) {
					console.log(res);
			    },
			    error: function(res){
				    console.log(res)
			    }
		     });
             })
	});
	</script>
	     <?php
	}
});

You can see that we are listening to the form submit event, and checking the value of the action field (don’t forget there is also the action2 field which you may also want to implement). We then check if the action value matches my-action and if so we serialize the form and submit it with an ajax GET request.

In order for the server to handle this we will add the following action:
add_action("wp_ajax_my-action", function(){
	global $wp_query;
	
        add_filter("edit_posts_per_page", function($ppp){
		return -1;
	});
	
	wp_edit_posts_query();
	$posts = $wp_query->posts;
	$ret = [];
	foreach($posts as $post){
		$ret[] = $post->ID;
	}
	wp_send_json($ret);
});

You can notice two things. Firstly, we use the filter edit_posts_per_page to make sure we get all of the ids. Second we call wp_edit_posts_query() and get the results with $wp_query->posts.

Now that we have all of the Ids, we can do whatever we like (probably more asynchronous actions). But for now, we just log them to the console.

Categories
Advanced

How to Insert ‘Select All’ Button to a WordPress Table?

I wanted to share how I created a “Select All” button to a WordPress table. Generally, WordPress doesn’t have a button to select the entire current table. You can usually select only a prefixed number.

Basically, this is what the expected result should look like.

How to set it up?

After some work, I’m gonna walk you through the process. Its not hard to create the PHP side. So let’s focus on the javascript first.

Initially, create some variables:

var allSelected = false;
var itemCount = jQuery(".displaying-num").first().text();  var doAction1Text = jQuery("#doaction").val(); 
var doAction2Text = jQuery("#doaction2").val();

I simply got the values of the amount of items in the table and the text values of the submit buttons.

Now let’s create the new checkbox HTML:

var selectDropdown = 
'<div id="select-all-dropdown" style="display: inline-flex;"> \
  <input type="hidden" id="apply-on-everything-input" name="apply-on-everything"> \
  <span id="toggle-select-all-menu" class="ui-selectmenu-icon ui-icon ui-icon-triangle-1-s"></span> \
</div>';
var selectAllMenu = 
"<div id='select-all-menu' style='display: none';> \
  <button id='select-all-table-button' class='button' 
  style=''>All " + itemCount + "</button> \
</div>";

Notice that the menu has inline-flex display and that initially the menu itself has display none.

Next, I will detach the original checkbox and attach my menu instead:

var regularSelectAll = jQuery("#cb-select-all-1").detach();
jQuery("#cb").append(selectDropdown);
jQuery("#select-all-dropdown").append(regularSelectAll);
jQuery("#select-all-dropdown").after(selectAllMenu);

In order to get the proper look, you will need the following Cascading Style Sheets(CSS):

 #select-all-menu{
   margin-left: -10px;
   position: absolute;

   /* make the menu appear above the background: */
      z-index: 100;
 }

Finally, add some click event binding. I want that clicking on the dropdown arrow to toggle the appearance of the menu. I also want to click anywhere outside which results in closing the menu:

jQuery(document).on('click', function(e) {
   if(e.target.id == "toggle-select-all-menu"){
     jQuery("#select-all-menu").toggle();
   }else{
     jQuery('#select-all-menu').hide();
   }
  });

And finally, clicking the “Select All” button changes the submit button text and disables all of the checkboxes in the check column:

jQuery('#select-all-table-button').on("click", function(ev) {
  allSelected = !allSelected;
  ev.preventDefault();
  jQuery("#select-all-table-button").text(allSelected ? 
    "Deselect All" : "All " + itemCount);
  jQuery("#doaction").val(!allSelected ? doAction1Text : 
    doAction1Text + " (" + itemCount + ")");
  jQuery("#doaction2").val(!allSelected ? doAction2Text : 
    doAction2Text + " (" + itemCount + ")");


  jQuery("#apply-on-everything-input").val(allSelected);
  jQuery('.check-column 
    input[type=checkbox]').prop('checked', function(i, v) 
    { return allSelected; });
  jQuery('.check-column 
    input[type=checkbox]').prop('disabled', function(i, v) 
    { return allSelected; });
});

That’s it! We now have the UI ready. All we need to do now is to create the PHP side. For that, we have inserted the hidden input (Did you notice where we did that?) and now our form will also submit it with the key apply-on-everything . To use it we can just apply the filter edit_posts_per_page and return -1:

add_filter( "edit_posts_per_page", function($ppp){
   return -1;
});

So breaking it down, here is the PHP side (Assuming that we put the above into respective javascript and CSS files):

add_action('current_screen', function($screen){
  if($screen->id == "edit-product"){
    wp_enqueue_script( 'select_all_js', plugins_url( 
      'assets/js/check_all.js', __FILE__ ), array(), 1.3, 
      true );
    wp_enqueue_style( 'select_all_css', plugins_url( 
      'assets/css/check_all.css', __FILE__ ), array(), 
      1.4);
    if($_REQUEST['apply-on-everything']=="true"){
       add_filter( "edit_posts_per_page", function($ppp){
         return -1;
       });
    }
  }
});

You can see that I have targeted the product screen, but you can change that to any table you’d like.

Next up: how to make this asynchronous, such that requests don’t even have to reload the page? Stay tuned to find out!

Categories
Advanced

Add HTML to WordPress List Table

Lets try to add some HTML code in the product name column in the Woocommerce product table. 

We want the end result to be:

Just filtering the title is not enough. Let’s take a look at this example:

add_action('current_screen', function($screen){
   if($screen->id == "edit-product"){
      add_filter("the_title", function($title, $id){ 
         if(has_term('music', 'product_cat',$id)){
	    return $title . "<i style='color: green';> music<i/>";
	}
	 return $title;
      }, 10 ,2);
});

The end result will look like:

Because Woocommerce is escaping the HTML. There is no direct filter for this. You have to use the WordPress esc_html filter:

add_action('current_screen', function($screen){
   if($screen->id == "edit-product"){
      add_filter("the_title", function($title, $id){
         if(has_term('music', 'product_cat',$id)){
	     return $title . "<i style='color: green';> music<i/>";
	 }
	 return $title;
      }, 10 ,2);

      add_filter("esc_html", function($safe, $unsafe){
         if(has_term('music', 'product_cat')){
             return $unsafe;
	 }
	 return $safe;
      }, 10 ,2);
   }
});

If this is a good enough solution for you, awesome! But I will also include an alternate approach.

This approach makes more sense you will not doing anything that could potentially collide with other plugins.

We will use the filter post_class hook. to add our own custom class.

add_action('current_screen', function($screen){
   if($screen->id == "edit-product"){
      add_filter("post_class", function($classes,$class){
         $id = the_ID();
         if(has_term('music', 'product_cat',$id)){
               $classes[] = "music";
	 }
	 return $classes;
      }, 10 ,2);
   }
});

This adds the class ‘music’ to all products in the music category.

To finish we will use the jQuery append method to insert our HTML.

document.addEventListener("DOMContentLoaded", function() {
   jQuery("tr.music td.name a.row-title").append("<i style='color: green;'> music</i>");	
});

That’s it!

The final code might look like:

add_action('current_screen', function($screen){
   if($screen->id == "edit-product"){
      add_filter("post_class", function($classes,$class){
         $id = the_ID();
         if(has_term('music', 'product_cat',$id)){
               $classes[] = "music";
	 }
	 return $classes;
      }, 10 ,2);
      ?>
<script>
    document.addEventListener("DOMContentLoaded", function() {
      jQuery("tr.music td.name a.row-title").append("<i style='color: green;'> music</i>");
    });

</script>
    <?php
    }
}); 

But you may want to separate the javascript from the PHP. Good luck and happy coding!