Skip to content

Instantly share code, notes, and snippets.

@rheinardkorf
Last active April 2, 2024 09:49
Show Gist options
  • Save rheinardkorf/c6592b59fb061f9f8310 to your computer and use it in GitHub Desktop.
Save rheinardkorf/c6592b59fb061f9f8310 to your computer and use it in GitHub Desktop.
Simple WordPress like hooks system for JavaScript.
/**
* @file A WordPress-like hook system for JavaScript.
*
* This file demonstrates a simple hook system for JavaScript based on the hook
* system in WordPress. The purpose of this is to make your code extensible and
* allowing other developers to hook into your code with their own callbacks.
*
* There are other ways to do this, but this will feel right at home for
* WordPress developers.
*
* @author Rheinard Korf
* @license GPL2 (https://www.gnu.org/licenses/gpl-2.0.html)
*
* @requires underscore.js (http://underscorejs.org/)
*/
/**
* Hooks object
*
* This object needs to be declared early so that it can be used in code.
* Preferably at a global scope.
*/
var Hooks = Hooks || {}; // Extend Hooks if exists or create new Hooks object.
Hooks.actions = Hooks.actions || {}; // Registered actions
Hooks.filters = Hooks.filters || {}; // Registered filters
/**
* Add a new Action callback to Hooks.actions
*
* @param tag The tag specified by do_action()
* @param callback The callback function to call when do_action() is called
* @param priority The order in which to call the callbacks. Default: 10 (like WordPress)
*/
Hooks.add_action = function( tag, callback, priority ) {
if( typeof priority === "undefined" ) {
priority = 10;
}
// If the tag doesn't exist, create it.
Hooks.actions[ tag ] = Hooks.actions[ tag ] || [];
Hooks.actions[ tag ].push( { priority: priority, callback: callback } );
}
/**
* Add a new Filter callback to Hooks.filters
*
* @param tag The tag specified by apply_filters()
* @param callback The callback function to call when apply_filters() is called
* @param priority Priority of filter to apply. Default: 10 (like WordPress)
*/
Hooks.add_filter = function( tag, callback, priority ) {
if( typeof priority === "undefined" ) {
priority = 10;
}
// If the tag doesn't exist, create it.
Hooks.filters[ tag ] = Hooks.filters[ tag ] || [];
Hooks.filters[ tag ].push( { priority: priority, callback: callback } );
}
/**
* Remove an Anction callback from Hooks.actions
*
* Must be the exact same callback signature.
* Warning: Anonymous functions can not be removed.
* @param tag The tag specified by do_action()
* @param callback The callback function to remove
*/
Hooks.remove_action = function( tag, callback ) {
Hooks.actions[ tag ] = Hooks.actions[ tag ] || [];
Hooks.actions[ tag ].forEach( function( filter, i ) {
if( filter.callback === callback ) {
Hooks.actions[ tag ].splice(i, 1);
}
} );
}
/**
* Remove a Filter callback from Hooks.filters
*
* Must be the exact same callback signature.
* Warning: Anonymous functions can not be removed.
* @param tag The tag specified by apply_filters()
* @param callback The callback function to remove
*/
Hooks.remove_filter = function( tag, callback ) {
Hooks.filters[ tag ] = Hooks.filters[ tag ] || [];
Hooks.filters[ tag ].forEach( function( filter, i ) {
if( filter.callback === callback ) {
Hooks.filters[ tag ].splice(i, 1);
}
} );
}
/**
* Calls actions that are stored in Hooks.actions for a specific tag or nothing
* if there are no actions to call.
*
* @param tag A registered tag in Hook.actions
* @options Optional JavaScript object to pass to the callbacks
*/
Hooks.do_action = function( tag, options ) {
var actions = [];
if( typeof Hooks.actions[ tag ] !== "undefined" && Hooks.actions[ tag ].length > 0 ) {
Hooks.actions[ tag ].forEach( function( hook ) {
actions[ hook.priority ] = actions[ hook.priority ] || [];
actions[ hook.priority ].push( hook.callback );
} );
actions.forEach( function( hooks ) {
hooks.forEach( function( callback ) {
callback( options );
} );
} );
}
}
/**
* Calls filters that are stored in Hooks.filters for a specific tag or return
* original value if no filters exist.
*
* @param tag A registered tag in Hook.filters
* @options Optional JavaScript object to pass to the callbacks
*/
Hooks.apply_filters = function( tag, value, options ) {
var filters = [];
if( typeof Hooks.filters[ tag ] !== "undefined" && Hooks.filters[ tag ].length > 0 ) {
Hooks.filters[ tag ].forEach( function( hook ) {
filters[ hook.priority ] = filters[ hook.priority ] || [];
filters[ hook.priority ].push( hook.callback );
} );
filters.forEach( function( hooks ) {
hooks.forEach( function( callback ) {
value = callback( value, options );
} );
} );
}
return value;
}
/***
* EXAMPLES
*
* Note: Using the Hooks object assumes that it is available at the scope your
* code is executing.
*
* Simplest way to test, if you have `node` installed run `node Hooks.js`
*/
// Filters ---------------------------------------------
// Note: Add filters before you apply filters. Its up to you to decide how to implement app wide.
// Anonymous example
Hooks.add_filter( 'my_filter', function( value, options ) {
return value + ' [Option:' + options.option1 + ']'
} ) // Default priority 10
// Non-anonymous example
function non_anon_filter( value, options ) {
return 'Awesome: ' + value;
}
Hooks.add_filter( 'my_filter', non_anon_filter, 1 ); // Priority 1
var my_value = 'Will be awesome'
var my_filtered_value = Hooks.apply_filters( 'my_filter', my_value, { option1: 'Optional option' } );
console.log( my_filtered_value );
// Remove filter
Hooks.remove_filter( 'my_filter', non_anon_filter );
var my_value_2 = 'Will not be awesome';
var my_filtered_value_2 = Hooks.apply_filters( 'my_filter', my_value_2, { option1: 'Another option' } );
console.log( my_filtered_value_2 );
// Actions ---------------------------------------------
// Note: Add actions before you call do_action()
// Anonymous example
Hooks.add_action( 'my_action', function( options ) {
console.log( 'Now you can perform custom actions at this exact moment of code execution.' );
} ) // Default priority 10
// Non-anonymous example
function non_anon_action( value, options ) {
console.log( 'This line should execute before the previously defined action. Priority 1!' );
}
Hooks.add_action( 'my_action', non_anon_action, 1 ); // Priority 1
Hooks.do_action( 'my_action' ); // Not using options in this example
@stracker-phil
Copy link

I notice the remove_action function actually removed a filer.
But in WordPress filters and actions are actually the same thing - i.e. add_action() is an alias for add_filter()

Excellent work! :)

@samnajian
Copy link

Fantastic!

@rheinardkorf
Copy link
Author

As per Phil's comment, I fixed the remove_action method.
https://gist.github.com/rheinardkorf/c6592b59fb061f9f8310#gistcomment-1593908

@rheinardkorf
Copy link
Author

Anybody still reading this.... don't use this anymore... look at this instead: https://core.trac.wordpress.org/changeset/41375

@coccoinomane
Copy link

Hi @rheinardkorf,
Thanks for the link to the Core project; I have checked it out and it seems very complete!
There's also the one-file project by @carldanley (https://github.com/carldanley/WP-JS-Hooks) which is probably less complete but easier to include in existing projects.

@zorida
Copy link

zorida commented May 30, 2018

Great!
@rheinardkorf I'm sorry, where is the code which depends on underscorejs?

@MohammedAl-Mahdawi
Copy link

@miljushm
Copy link

Awesome work mate!

@debabratakarfa
Copy link

@imVinayPandya
Copy link

Awesome

@aiiddqd
Copy link

aiiddqd commented Jan 11, 2022

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment