Permalink
Browse files

feature(js): adds plugin boot modules and modules based on system events

To reduce race conditions, plugins can create boot modules, named like
`boot/<plugin_id>`. All these modules are loaded before the `init, system`
hook. Registering for plugin hooks should be done inside these.

Depending on the new `elgg/init` module ensures your code runs after this
process.

Similarly, you can depend on the new `elgg/ready` module to ensure all ready
hooks have been called.

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

A new hook `config, ckeditor` now filters the CKEditor config object, and
plugins can reliably register for it in a boot module.

Fixes #7131
Fixes #7926
  • Loading branch information...
mrclay committed Oct 2, 2015
1 parent fe5a927 commit 924355a7e52c359f430ff1be04ec968286c64480
View
@@ -191,6 +191,54 @@ 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 elgg = require("elgg");
+ var Plugin = require("elgg/Plugin");
+
+ // plugin logic
+ function my_init() { ... }
+
+ return new Plugin({
+ // executed in order of plugin priority
+ init: function () {
+ elgg.register_hook_handler("init", "system", my_init, 400);
+ }
+ });
+ });
+
+When your plugin is active, this module will automatically be loaded on each page. Other modules can depend on ``elgg/init`` to make sure all boot modules are loaded.
+
+Each boot module **must** return an instance of ``elgg/Plugin``. The constructor must receive an object with a function in the ``init`` key. The ``init`` function will be called in the order of the plugin in Elgg's admin area.
+
+.. 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 module **cannot** depend on the modules ``elgg/init`` or ``elgg/ready``.
+
+
+The elgg/init module
+--------------------
+
+``elgg/init`` loads and initializes all boot modules in priority order and triggers the [init, system] hook.
+
+Require this module to make sure all plugins are ready.
+
+
+The elgg/ready module
+---------------------
+
+``elgg/ready`` loads and initializes all plugin boot modules in priority order.
+
+Require this module to make sure all plugins are ready.
+
Migrating JS from Elgg 1.8 to AMD / 1.9
=======================================
@@ -297,8 +345,7 @@ 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()``
@@ -308,30 +355,37 @@ Get the GUID of the current page's owner.
``elgg.register_hook_handler()``
-Register a hook handler with the event system.
+Register a hook handler with the event system. For best results, do this in a plugin boot module.
-.. code:: js
-
- // old initialization style
- elgg.register_hook_handler('init', 'system', my_plugin.init);
+.. code-block:: js
- // new: AMD module
+ // boot module: /views/default/boot/example.js
define(function (require) {
var elgg = require('elgg');
+ var Plugin = require('elgg/Plugin');
+
+ elgg.register_hook_handler('foo', 'bar', function () { ... });
- // [init, system] has fired
+ return new Plugin();
});
``elgg.trigger_hook()``
-Emit a hook event in the event system.
+Emit a hook event in the event system. For best results depend on the elgg/init module.
-.. code:: js
+.. code-block:: js
- // allow other plugins to alter value
+ // old
value = elgg.trigger_hook('my_plugin:filter', 'value', {}, value);
+ define(function (require) {
+ require('elgg/init');
+ var elgg = require('elgg');
+
+ value = elgg.trigger_hook('my_plugin:filter', 'value', {}, value);
+ });
+
``elgg.security.refreshToken()``
@@ -426,26 +480,34 @@ The ``elgg/spinner`` module can be used to create an Ajax loading indicator fixe
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.
+The JS engine has a hooks system similar to the PHP engine's plugin hooks: hooks are triggered and plugins can register functions 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.
+Handler functions are registered using ``elgg.register_hook_handler()``. Multiple handlers 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
- elgg.provide('elgg.ui.initDatePicker');
- elgg.ui.initDatePicker = function() { ... }
-
- elgg.register_hook_handler('init', 'system', elgg.ui.initDatePicker);
+ define(function (require) {
+ var elgg = require('elgg');
+ var Plugin = require('elgg/Plugin');
+
+ function handleFoo(hook, type, params, value) {
+ // do something
+ }
+
+ elgg.register_hook_handler('foo', 'bar', handleFoo);
-The callback
-^^^^^^^^^^^^
+ return new Plugin();
+ });
+
+The handler function
+^^^^^^^^^^^^^^^^^^^^
-The callback accepts 4 arguments:
+The handler will receive 4 arguments:
- **hook** - The hook name
- **type** - The hook type
@@ -461,16 +523,26 @@ Plugins can trigger their own hooks:
.. code:: javascript
- elgg.hook.trigger_hook('name', 'type', {params}, "value");
+ define(function(require) {
+ require('elgg/init');
+ var elgg = require('elgg');
+
+ elgg.trigger_hook('name', 'type', {params}, "value");
+ });
+
+.. note:: Be aware of timing. If you don't depend on elgg/init, other plugins may not have had a chance to register their handlers.
Available hooks
^^^^^^^^^^^^^^^
-init, system
- This hook is fired when the JS system is ready. Plugins should register their init functions for this hook.
+**init, system**
+ Plugins should register their init functions for this hook. It is fired after Elgg's JS is loaded and all plugin boot modules have been initialized. Depend on the ``elgg/init`` module to be sure this has completed.
+
+**ready, system**
+ This hook is fired when the system has fully booted (after init). Depend on the ``elgg/ready`` module to be sure this has completed.
-ready, system
- This hook is fired when the system has fully booted.
+**getOptions, ui.popup**
+ This hook is fired for pop up displays (``"rel"="popup"``) and allows for customized placement options.
-getOptions, ui.popup
- This hook is fired for pop up displays ("rel"="popup") and allows for customized placement options.
+**config, ckeditor**
+ This filters the CKEditor config object. Register for this hook in a plugin boot module. The defaults can be seen in the module ``elgg/ckeditor/config``.
View
@@ -1599,6 +1599,10 @@ function elgg_views_boot() {
elgg_register_css('elgg', elgg_get_simplecache_url('elgg.css'));
elgg_load_css('elgg');
+ elgg_register_simplecache_view('elgg/init.js');
+ elgg_require_js('elgg/init');
+ elgg_require_js('elgg/ready');
+
// 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'));
View
@@ -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
*/
@@ -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);
@@ -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);
View
@@ -9,6 +9,8 @@ elgg.provide('elgg.config.triggered_hooks');
/**
* Registers a hook handler with the event system.
*
+ * For best results, depend on the elgg/ready module, so plugins will have been booted.
+ *
* 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.
*
@@ -19,7 +21,7 @@ elgg.provide('elgg.config.triggered_hooks');
* @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}
+ * @return {Boolean}
*/
elgg.register_hook_handler = function(name, type, handler, priority) {
elgg.assertTypeOf('string', name);
@@ -68,7 +70,7 @@ elgg.register_hook_handler = function(name, type, handler, priority) {
* @param {Object} params Optional parameters to pass to the handlers
* @param {Object} value Initial value of the return. Can be mangled by handlers
*
- * @return {Bool}
+ * @return {Boolean}
*/
elgg.trigger_hook = function(name, type, params, value) {
elgg.assertTypeOf('string', name);
@@ -132,7 +134,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);
View
@@ -48,4 +48,30 @@ define(function(require) {
});
});
});
+
+ // note elgg/init and a fake boot module are defined in prepare.js
+ describe("elgg/ready", function() {
+ it("requires init (boots plugins and fires init) and fires ready", function(done) {
+ elgg._test_signals = [];
+
+ require(['elgg/ready'], function () {
+ expect(elgg._test_signals).toEqual([
+ 'boot/example define',
+
+ // boot Plugin inits are called
+ 'boot/example init',
+
+ // init, system fired
+ 'boot/example init,system',
+
+ // ready, system fired
+ 'boot/example ready,system'
+ ]);
+
+ delete(elgg._test_signals);
+
+ done();
+ });
+ });
+ });
});
View
@@ -13,6 +13,8 @@ module.exports = function(config) {
// list of files / patterns to load in the browser
files: [
+ 'js/tests/prepare.js',
+
'vendor/bower-asset/jquery/dist/jquery.js',
'bower_components/sprintf/src/sprintf.js',
'js/lib/elgglib.js',
@@ -23,7 +25,7 @@ module.exports = function(config) {
{pattern:'js/tests/*Test.js',included: false},
{pattern:'views/default/**/*.js',included:false},
- 'js/tests/requirejs.config.js',
+ 'js/tests/requirejs.config.js'
],
View
@@ -0,0 +1,39 @@
+// These modules are typically built in PHP. We can't do that with the test runner.
+
+var elgg = elgg || {};
+
+define('elgg', function() {
+ return elgg;
+});
+
+// for ElggHooksTest.js
+define('boot/example', function(require) {
+ var elgg = require('elgg');
+ var Plugin = require('elgg/Plugin');
+
+ elgg._test_signals.push('boot/example define');
+
+ elgg.register_hook_handler('init', 'system', function() {
+ elgg._test_signals.push('boot/example init,system');
+ });
+ elgg.register_hook_handler('ready', 'system', function() {
+ elgg._test_signals.push('boot/example ready,system');
+ });
+
+ return new Plugin({
+ init: function () {
+ elgg._test_signals.push('boot/example init');
+ }
+ });
+});
+
+// for ElggHooksTest.js
+define('elgg/init', function (require) {
+ var elgg = require('elgg');
+ var plugin = require('boot/example');
+
+ console.log(plugin);
+ plugin._init();
+
+ elgg.trigger_hook('init', 'system');
+});
@@ -18,7 +18,3 @@ requirejs.config({
// start test run, once Require.js is done
callback: window.__karma__.start
});
-
-// This module is typically built in PHP. We can't do that with the test runner.
-define('elgg', function() { return elgg; });
-
@@ -1,7 +1,9 @@
define(function(require) {
var elgg = require('elgg');
- var $ = require('jquery'); require('jquery.ckeditor');
+ var $ = require('jquery');
+ require('jquery.ckeditor');
var CKEDITOR = require('ckeditor');
+ var config = require('elgg/ckeditor/config');
CKEDITOR.plugins.addExternal('blockimagepaste', elgg.get_simplecache_url('elgg/ckeditor/blockimagepaste.js'), '');
@@ -73,8 +75,7 @@ define(function(require) {
* You can find configuration information here:
* http://docs.ckeditor.com/#!/api/CKEDITOR.config
*/
- config: require('elgg/ckeditor/config')
-
+ config: config
};
CKEDITOR.on('instanceReady', elggCKEditor.fixImageAttributes);
Oops, something went wrong.

0 comments on commit 924355a

Please sign in to comment.