Skip to content

Commit

Permalink
feature(javascript): moves hooks to dedicated AMD modules, adds plugi…
Browse files Browse the repository at this point in the history
…n boot modules

Deprecates `elgg.register_hook_handler()` and `elgg.trigger_hook()` in favor
of dedicated `elgg/hooks/register` and `elgg/hooks/trigger` modules. These and
other new modules ensure all hooks are registered before being triggered:

Plugins should register inside boot modules, named `boot/<plugin_id>`. The
trigger module depends on a new `elgg/booted` module, which loads and
initializes all plugin boot modules in priority order, then triggers
the system hooks.

Plugin boot modules return an instance of `elgg/Plugin`.

Formally deprecates the `boot, system` hook.

Fixes Elgg#7926
  • Loading branch information
mrclay committed Oct 1, 2015
1 parent 7601a71 commit 28b8e67
Show file tree
Hide file tree
Showing 39 changed files with 488 additions and 172 deletions.
134 changes: 108 additions & 26 deletions docs/guides/javascript.rst
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,64 @@ Some things to note
#. Return the value of the module instead of adding to a global variable.
#. Static (.js,.css,etc.) files are automatically minified and cached by Elgg's simplecache system.

Booting your plugin
===================

To add functionality to each page, or make sure your hook handlers are registered early enough, you may create a boot module for your plugin, with the name ``boot/<plugin_id>``.

.. code-block:: javascript
// in views/default/boot/example.js
define(function(require) {
var Plugin = require("elgg/Plugin");
var register = require("elgg/hooks/register");
function my_init() { ... }
// using [init, system] to call this before others
register("init", "system", my_init, 400);
return new Plugin({
// optional
init: function () {
// this is executed strictly in order of plugin priority
},
// optional
exports: {
// resources available to other plugins
}
});
});
When your plugin is active, this module will automatically be loaded on each page. Other modules can depend on ``elgg/booted`` to make sure all boot modules are loaded.

Each boot module **must** return an instance of ``elgg/Plugin``. The constructor accepts a configuration object that helps initialize code in plugin order, and/or provide an "exports" value other modules can use.

.. note:: Though not strictly necessary, you may want to use the ``init, system`` event to control when your initialization code runs with respect to other modules.

.. warning:: A boot plugin **cannot** depend on the ``elgg/booted`` module either directly or through its hierarchy of dependents.


The elgg/booted module
----------------------

``elgg/booted`` loads and initializes all plugin boot modules in priority order. Require this module to make sure all plugins are ready.

It includes the function ``get_plugin(id)``, which will return a Plugin object if the plugin has one (and is activated). This allows you to conditionally detect and/or get access to boot module resources without a hard dependency:

.. code-block:: javascript
define(function(require) {
var booted = require("elgg/booted");
var example_plugin = booted.get_plugin("example");
if (example_plugin) {
example_plugin.get_exports();
}
});
Migrating JS from Elgg 1.8 to AMD / 1.9
=======================================
Expand Down Expand Up @@ -297,41 +355,51 @@ Parse a URL into its component parts:
// path: "/file.php",
// query: "arg=val"
// }
elgg.parse_url(
'http://community.elgg.org/file.php?arg=val#fragment');
elgg.parse_url('http://community.elgg.org/file.php?arg=val#fragment');
``elgg.get_page_owner_guid()``

Get the GUID of the current page's owner.


``elgg.register_hook_handler()``
``elgg.register_hook_handler()`` (Deprecated: Use the ``elgg/hooks/register`` module.)

Register a hook handler with the event system.

.. code:: js
.. code-block:: js
// old initialization style
elgg.register_hook_handler('init', 'system', my_plugin.init);
// old
elgg.register_hook_handler('foo', 'bar', my_plugin.handle_foo);
// new: AMD module
// new: AMD boot module
define(function (require) {
var elgg = require('elgg');
var Plugin = require('elgg/Plugin');
var register = require('elgg/hooks/register');
register('foo', 'bar', function () { ... });
// [init, system] has fired
return new Plugin();
});
``elgg.trigger_hook()``
``elgg.trigger_hook()`` (Deprecated: Use the ``elgg/hooks/trigger`` module)

Emit a hook event in the event system.

.. code:: js
.. code-block:: js
// allow other plugins to alter value
// old
value = elgg.trigger_hook('my_plugin:filter', 'value', {}, value);
// new
define(function (require) {
// this blocks until plugins are booted
var trigger = require('elgg/hooks/trigger');
value = trigger('my_plugin:filter', 'value', {}, value);
});
``elgg.security.refreshToken()``

Expand Down Expand Up @@ -428,24 +496,32 @@ Hooks

The JS engine has a hooks system similar to the PHP engine's plugin hooks: hooks are triggered and plugins can register callbacks to react or alter information. There is no concept of Elgg events in the JS engine; everything in the JS engine is implemented as a hook.

Registering a callback to a hook
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
Registering hook handlers
^^^^^^^^^^^^^^^^^^^^^^^^^

Callbacks are registered using ``elgg.register_hook_handler()``. Multiple callbacks can be registered for the same hook.
You may register hook handlers using the ``elgg/hooks/register`` module (ideally inside your plugin boot module). Multiple callbacks can be registered for the same hook.

The following example registers the ``elgg.ui.initDatePicker`` callback for the *init*, *system* event. Note that a difference in the JS engine is that instead of passing a string you pass the function itself to ``elgg.register_hook_handler()`` as the callback.
The following example registers the ``handleFoo`` function for the ``foo, bar`` hook.

.. code:: javascript
.. code-block:: javascript
define(function (require) {
var Plugin = require('elgg/Plugin');
var register = require('elgg/hooks/register');
function handleFoo(hook, type, params, value) {
// do something
}
elgg.provide('elgg.ui.initDatePicker');
elgg.ui.initDatePicker = function() { ... }
elgg.register_hook_handler('init', 'system', elgg.ui.initDatePicker);
register('foo', 'bar', handleFoo);
The callback
^^^^^^^^^^^^
return new Plugin();
});
The callback accepts 4 arguments:
The handler function
^^^^^^^^^^^^^^^^^^^^

The handler will receive 4 arguments:

- **hook** - The hook name
- **type** - The hook type
Expand All @@ -457,11 +533,17 @@ The ``value`` will be passed through each hook. Depending on the hook, callbacks
Triggering custom hooks
^^^^^^^^^^^^^^^^^^^^^^^

Plugins can trigger their own hooks:
Plugins can trigger their own hooks via the ``elgg/hooks/trigger`` module.:

.. code:: javascript
elgg.hook.trigger_hook('name', 'type', {params}, "value");
define(function(require) {
var trigger = require("elgg/hooks/trigger");
trigger('name', 'type', {params}, "value");
});
.. note:: You cannot trigger hooks in your boot module, or in a module that also registers hooks.

Available hooks
^^^^^^^^^^^^^^^
Expand Down
5 changes: 5 additions & 0 deletions engine/lib/views.php
Original file line number Diff line number Diff line change
Expand Up @@ -1539,6 +1539,11 @@ function elgg_views_boot() {
elgg_register_css('elgg', elgg_get_simplecache_url('elgg.css'));
elgg_load_css('elgg');

elgg_register_simplecache_view('elgg/booted.js');
elgg_require_js('elgg/booted');
elgg_require_js('elgg/hooks/register');
elgg_require_js('elgg/hooks/trigger');

// optional stuff
elgg_register_js('lightbox', elgg_get_simplecache_url('lightbox.js'));
elgg_register_css('lightbox', elgg_get_simplecache_url('lightbox/elgg-colorbox-theme/colorbox.css'));
Expand Down
4 changes: 3 additions & 1 deletion js/lib/comments.js
Original file line number Diff line number Diff line change
Expand Up @@ -129,4 +129,6 @@ elgg.comments.init = function() {
});
};

elgg.register_hook_handler('init', 'system', elgg.comments.init);
require(['elgg/hooks/register'], function(register) {
register('init', 'system', elgg.comments.init);
});
10 changes: 5 additions & 5 deletions js/lib/elgglib.js
Original file line number Diff line number Diff line change
Expand Up @@ -422,9 +422,9 @@ elgg.forward = function(url) {
/**
* Parse a URL into its parts. Mimicks http://php.net/parse_url
*
* @param {String} url The URL to parse
* @param {Int} component A component to return
* @param {Bool} expand Expand the query into an object? Else it's a string.
* @param {String} url The URL to parse
* @param {Number} component A component to return
* @param {Boolean} expand Expand the query into an object? Else it's a string.
*
* @return {Object} The parsed URL
*/
Expand Down Expand Up @@ -562,7 +562,7 @@ elgg.getSelectorFromUrlFragment = function(url) {
*
* @param {Object} object The object to add to
* @param {String} parent The parent array to add to.
* @param {Mixed} value The value
* @param {*} value The value
*/
elgg.push_to_object_array = function(object, parent, value) {
elgg.assertTypeOf('object', object);
Expand All @@ -584,7 +584,7 @@ elgg.push_to_object_array = function(object, parent, value) {
*
* @param {Object} object The object to add to
* @param {String} parent The parent array to add to.
* @param {Mixed} value The value
* @param {*} value The value
*/
elgg.is_in_object_array = function(object, parent, value) {
elgg.assertTypeOf('object', object);
Expand Down
68 changes: 31 additions & 37 deletions js/lib/hooks.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,9 @@ elgg.provide('elgg.config.instant_hooks');
elgg.provide('elgg.config.triggered_hooks');

/**
* Registers a hook handler with the event system.
*
* The special keyword "all" can be used for either the name or the type or both
* and means to call that handler for all of those hooks.
*
* Note that handlers registering for instant hooks will be executed immediately if the instant
* hook has been previously triggered.
*
* @param {String} name Name of the plugin hook to register for
* @param {String} type Type of the event to register for
* @param {Function} handler Handle to call
* @param {Number} priority Priority to call the event handler
* @return {Bool}
* @private
*/
elgg.register_hook_handler = function(name, type, handler, priority) {
elgg._register_hook_handler = function(name, type, handler, priority) {
elgg.assertTypeOf('string', name);
elgg.assertTypeOf('string', type);
elgg.assertTypeOf('function', handler);
Expand All @@ -47,30 +35,26 @@ elgg.register_hook_handler = function(name, type, handler, priority) {
};

/**
* Emits a hook.
*
* Loops through all registered hooks and calls the handler functions in order.
* Every handler function will always be called, regardless of the return value.
*
* @warning Handlers take the same 4 arguments in the same order as when calling this function.
* This is different from the PHP version!
*
* @note Instant hooks do not support params or values.
*
* Hooks are called in this order:
* specifically registered (event_name and event_type match)
* all names, specific type
* specific name, all types
* all names, all types
*
* @param {String} name Name of the hook to emit
* @param {String} type Type of the hook to emit
* @param {Object} params Optional parameters to pass to the handlers
* @param {Object} value Initial value of the return. Can be mangled by handlers
* Registers a hook handler with the event system.
*
* @return {Bool}
* @deprecated Use the elgg/hooks/register module in a boot module
*/
elgg.trigger_hook = function(name, type, params, value) {
elgg.register_hook_handler = function(name, type, handler, priority) {
if (name === 'boot' && type === 'system') {
elgg.deprecated_notice("The hook [boot, system] is deprecated. Use [init, system] in a plugin boot" +
" module", "2.1");
} else {
elgg.deprecated_notice("elgg.register_hook_handler is deprecated. Use the elgg/hooks/register module" +
" in your plugin boot module", "2.1");
}

return elgg._register_hook_handler(name, type, handler, priority);
};

/**
* @private
*/
elgg._trigger_hook = function(name, type, params, value) {
elgg.assertTypeOf('string', name);
elgg.assertTypeOf('string', type);

Expand Down Expand Up @@ -121,6 +105,16 @@ elgg.trigger_hook = function(name, type, params, value) {
return returnValue;
};

/**
* Emits a hook.
*
* @deprecated Use the elgg/hooks/trigger module
*/
elgg.trigger_hook = function(name, type, params, value) {
elgg.deprecated_notice("elgg.trigger_hook is deprecated. Use the elgg/hooks/trigger module", "2.1");
return elgg._trigger_hook(name, type, params, value);
};

/**
* Registers a hook as an instant hook.
*
Expand All @@ -132,7 +126,7 @@ elgg.trigger_hook = function(name, type, params, value) {
*
* @param {String} name The hook name.
* @param {String} type The hook type.
* @return {Int}
* @return {Number} integer
*/
elgg.register_instant_hook = function(name, type) {
elgg.assertTypeOf('string', name);
Expand Down
4 changes: 3 additions & 1 deletion js/lib/security.js
Original file line number Diff line number Diff line change
Expand Up @@ -104,4 +104,6 @@ elgg.security.init = function() {
elgg.security.tokenRefreshTimer = setInterval(elgg.security.refreshToken, elgg.security.interval);
};

elgg.register_hook_handler('boot', 'system', elgg.security.init);
require(['elgg/hooks/register'], function(register) {
register('init', 'system', elgg.security.init);
});
4 changes: 3 additions & 1 deletion js/lib/ui.autocomplete.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,6 @@ elgg.autocomplete.init = function() {
});
};

elgg.register_hook_handler('init', 'system', elgg.autocomplete.init);
require(['elgg/hooks/register'], function(register) {
register('init', 'system', elgg.autocomplete.init);
});
4 changes: 3 additions & 1 deletion js/lib/ui.avatar_cropper.js
Original file line number Diff line number Diff line change
Expand Up @@ -77,4 +77,6 @@ elgg.avatarCropper.selectChange = function(img, selection) {
$('input[name=y2]').val(selection.y2);
};

elgg.register_hook_handler('init', 'system', elgg.avatarCropper.init);
require(['elgg/hooks/register'], function(register) {
register('init', 'system', elgg.avatarCropper.init);
});
12 changes: 7 additions & 5 deletions js/lib/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ elgg.ui.popupOpen = function(event) {
collision: 'fit fit'
};

options = elgg.trigger_hook('getOptions', 'ui.popup', params, options);
options = elgg._trigger_hook('getOptions', 'ui.popup', params, options);

// allow plugins to cancel event
if (!options) {
Expand Down Expand Up @@ -472,7 +472,9 @@ elgg.ui.initAccessInputs = function () {
});
};

elgg.register_hook_handler('init', 'system', elgg.ui.init);
elgg.register_hook_handler('init', 'system', elgg.ui.initDatePicker);
elgg.register_hook_handler('getOptions', 'ui.popup', elgg.ui.loginHandler);
elgg.ui.registerTogglableMenuItems('add-friend', 'remove-friend');
require(['elgg/hooks/register'], function(register) {
register('init', 'system', elgg.ui.init);
register('init', 'system', elgg.ui.initDatePicker);
register('getOptions', 'ui.popup', elgg.ui.loginHandler);
elgg.ui.registerTogglableMenuItems('add-friend', 'remove-friend');
});
Loading

0 comments on commit 28b8e67

Please sign in to comment.