diff --git a/Gruntfile.js b/Gruntfile.js
index 916d61c018dc755e3bed22cfe1535edd5b72e12a..2f8a257bc57de1d27a6ee2cb6e52c57c81795949 100644
--- a/Gruntfile.js
+++ b/Gruntfile.js
@@ -89,11 +89,7 @@ module.exports = function (grunt) {
                         expand: true,
                         dest: typo3_js,
                         flatten: true
-                    }
-                ]
-            },
-            jquery_devel: {
-                files: [
+                    },
                     {
                         cwd: 'bower_components/jquery/dist/',
                         src: [
@@ -115,11 +111,7 @@ module.exports = function (grunt) {
                         expand: true,
                         dest: typo3_js,
                         flatten: true
-                    }
-                ]
-            },
-            jquery_tablesorter_devel: {
-                files: [
+                    },
                     {
                         cwd: 'bower_components/tablesorter/dist/js/',
                         src: [
@@ -194,6 +186,28 @@ module.exports = function (grunt) {
                         dest: 'css/'
                     }
                 ]
+            },
+            eventEmitter: {
+                files: [
+                    {
+                        cwd: 'bower_components/eventEmitter/',
+                        src: [
+                            'EventEmitter.min.js'
+                        ],
+                        expand: true,
+                        dest: typo3_js,
+                        flatten: true
+                    },
+                    {
+                        cwd: 'bower_components/eventEmitter/',
+                        src: [
+                            'EventEmitter.min.js'
+                        ],
+                        expand: true,
+                        dest: 'js/',
+                        flatten: true
+                    }
+                ]
             }
         },
         uglify: {
@@ -285,6 +299,7 @@ module.exports = function (grunt) {
                     vendor: [
                         'js/jquery.min.js',
                         'js/bootstrap.min.js',
+                        'js/EventEmitter.min.js',
                         'js/jqx-all.js',
                         'js/qfq.debug.js'
                     ],
diff --git a/bower.json b/bower.json
index 0628c19f7ec7eb4e2ba96f27eeb6659ef07bdee5..d59a7e79a7269e7cf9a90734e109e87824a4e083 100644
--- a/bower.json
+++ b/bower.json
@@ -19,6 +19,7 @@
   "dependencies": {
     "bootstrap": "~3.3.6",
     "jqwidgets": "*",
-    "tablesorter": "jquery.tablesorter#^2.25.6"
+    "tablesorter": "jquery.tablesorter#^2.25.6",
+    "eventEmitter": "^4.3.0"
   }
 }
diff --git a/javascript/src/Alert.js b/javascript/src/Alert.js
index ac0ac28f2f8c9bed07fb78c0b92c62ec34348bfd..5958d9c6a5ae8d693cc21ab1c881522ca0848c17 100644
--- a/javascript/src/Alert.js
+++ b/javascript/src/Alert.js
@@ -3,10 +3,10 @@
  */
 
 /* global $ */
+/* global EventEmitter */
+/* @depend QfqEvents.js */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
@@ -64,11 +64,15 @@ if (!QfqNS) {
         this.fadeOutDuration = 400;
         this.timerId = null;
 
+        this.eventEmitter = new EventEmitter();
+
         this.userOkButtonHandlers = new n.Helper.FunctionList();
         this.userCancelButtonHandlers = new n.Helper.FunctionList();
         this.userSaveButtonHandlers = new n.Helper.FunctionList();
     };
 
+    n.Alert.prototype.on = n.EventEmitter.onMixin;
+
     /**
      *
      * @private
@@ -255,19 +259,6 @@ if (!QfqNS) {
 
     };
 
-    n.Alert.prototype.addOkButtonHandler = function (handler) {
-        this.userOkButtonHandlers.addFunction(handler);
-    };
-
-    n.Alert.prototype.addCancelButtonHandler = function (handler) {
-        this.userCancelButtonHandlers.addFunction(handler);
-    };
-
-    n.Alert.prototype.addSaveButtonHandler = function (handler) {
-        this.userSaveButtonHandlers.addFunction(handler);
-    };
-
-
     /**
      *
      * @param handler
@@ -276,7 +267,7 @@ if (!QfqNS) {
      */
     n.Alert.prototype.okButtonHandler = function (handler) {
         this.removeAlert();
-        this.userOkButtonHandlers.call(this);
+        this.eventEmitter.emitEvent('alert.ok', n.EventEmitter.makePayload(this, null));
     };
 
     /**
@@ -287,7 +278,7 @@ if (!QfqNS) {
      */
     n.Alert.prototype.saveButtonHandler = function (handler) {
         this.removeAlert();
-        this.userSaveButtonHandlers.call(this);
+        this.eventEmitter.emitEvent('alert.save', n.EventEmitter.makePayload(this, null));
     };
 
     /**
@@ -298,7 +289,7 @@ if (!QfqNS) {
      */
     n.Alert.prototype.cancelButtonHandler = function (handler) {
         this.removeAlert();
-        this.userCancelButtonHandlers.call(this);
+        this.eventEmitter.emitEvent('alert.cancel', n.EventEmitter.makePayload(this, null));
     };
 
     n.Alert.prototype.isShown = function () {
diff --git a/javascript/src/BSTabs.js b/javascript/src/BSTabs.js
index 75f8b56015fffc0bb7ad4bba104ebd4c3651b267..88a6b71b90dc9c7dc13b649549b292b048cd8b2f 100644
--- a/javascript/src/BSTabs.js
+++ b/javascript/src/BSTabs.js
@@ -4,10 +4,12 @@
 
 /* global $ */
 /* global console */
+/* global EventEmitter */
+
+/* @depend QfqEvents.js */
+
+var QfqNS = QfqNS || {};
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
 
 (function (n) {
     'use strict';
@@ -25,8 +27,7 @@ if (!QfqNS) {
         this._tabActiveSelector = '#' + this.tabId + ' .active a[data-toggle="tab"]';
         this.tabs = {};
         this.currentTab = this.getActiveTabFromDOM();
-        this.userTabShowHandlers = new n.Helper.FunctionList();
-
+        this.eventEmitter = new EventEmitter();
 
         // Fill this.tabs
         this.fillTabInformation();
@@ -35,6 +36,8 @@ if (!QfqNS) {
         this.installTabHandlers();
     };
 
+    n.BSTabs.prototype.on = n.EventEmitter.onMixin;
+
     /**
      * Get active tab from DOM.
      *
@@ -102,9 +105,8 @@ if (!QfqNS) {
         n.Log.debug('Enter: BSTabs.tabShowHandler()');
         this.currentTab = event.target.hash.slice(1);
 
-        var that = this;
         n.Log.debug("BSTabs.tabShowHandler(): invoke user handler(s)");
-        this.userTabShowHandlers.call(that);
+        this.eventEmitter.emitEvent('bootstrap.tab.shown', n.EventEmitter.makePayload(this, null));
         n.Log.debug('Exit: BSTabs.tabShowHandler()');
     };
 
@@ -162,15 +164,6 @@ if (!QfqNS) {
         return this.currentTab;
     };
 
-    /**
-     * Add tab show handler.
-     *
-     * @param {function} handler handler function. `this` will be passed as first and only argument to the handler.
-     */
-    n.BSTabs.prototype.addTabShowHandler = function (handler) {
-        this.userTabShowHandlers.addFunction(handler);
-    };
-
     n.BSTabs.prototype.getTabName = function (tabId) {
         if (!this.tabs[tabId]) {
             console.error("Unable to find tab with id: " + tabId);
diff --git a/javascript/src/Element/Checkbox.js b/javascript/src/Element/Checkbox.js
index fee1ab3ff69b44cdda57f4cbe15f4532dcb003e4..ebd8c23f3bc7989c169a09d26aeb7313c401985c 100644
--- a/javascript/src/Element/Checkbox.js
+++ b/javascript/src/Element/Checkbox.js
@@ -4,13 +4,8 @@
 
 /* @depend FormGroup.js */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/FormGroup.js b/javascript/src/Element/FormGroup.js
index 1f120d658c4648aa1393c975faf4ca34d19ca595..0d893d32ecc5b83505009969b94c719a1889b3fc 100644
--- a/javascript/src/Element/FormGroup.js
+++ b/javascript/src/Element/FormGroup.js
@@ -2,13 +2,8 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/NameSpaceFunctions.js b/javascript/src/Element/NameSpaceFunctions.js
index 014d03a6846f14924ff9441f1532883f5f9a5375..b177344dae389e8ce27edfc4a45de2d562403d9d 100644
--- a/javascript/src/Element/NameSpaceFunctions.js
+++ b/javascript/src/Element/NameSpaceFunctions.js
@@ -4,13 +4,8 @@
 
 /* global $ */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/Radio.js b/javascript/src/Element/Radio.js
index 9f3e3688f4e3c820b7d89aadd65ee4c9af91444f..d449bf9281b66c6b34495b3e56bea8b3dd1472f6 100644
--- a/javascript/src/Element/Radio.js
+++ b/javascript/src/Element/Radio.js
@@ -2,13 +2,8 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/Select.js b/javascript/src/Element/Select.js
index 2c80615a98837f9f0654623b6010b2fbb2309016..e1968cb0041c90f5904d5c73c1219b467b30125d 100644
--- a/javascript/src/Element/Select.js
+++ b/javascript/src/Element/Select.js
@@ -3,14 +3,8 @@
  */
 /* global $ */
 
-
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/Text.js b/javascript/src/Element/Text.js
index e94d1a69a3401c792a566a52d7b970c8b37909fc..61bc4dd40d80d3b5b4ac28d3c746eb4d86fde80b 100644
--- a/javascript/src/Element/Text.js
+++ b/javascript/src/Element/Text.js
@@ -2,13 +2,8 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Element/data.js b/javascript/src/Element/data.js
index 8719181f23aa99a4a34837be390762969ccd74ac..b86efcd68f5712328dcd05fcec15d94961f32209 100644
--- a/javascript/src/Element/data.js
+++ b/javascript/src/Element/data.js
@@ -2,14 +2,8 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Element) {
-    QfqNS.Element = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Element = QfqNS.Element || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/FileUpload.js b/javascript/src/FileUpload.js
index b7b5f6af2d6752dfe99b943b0fc846364724f08f..3a54530db2790494862fd6ce21683ee340c66251 100644
--- a/javascript/src/FileUpload.js
+++ b/javascript/src/FileUpload.js
@@ -3,10 +3,12 @@
  */
 
 /* global $ */
+/* global EventEmitter */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+/* @depend QfqEvents.js */
+
+
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
@@ -15,47 +17,12 @@ if (!QfqNS) {
         this.formSelector = formSelector;
         this.targetUrl = targetUrl;
         this.sip = sip;
-
-        // TODO: Seriously, I'd like to have an event system.
-        this.fileUploadStartCallbacks = new n.Helper.FunctionList();
-        this.fileUploadEndCallbacks = new n.Helper.FunctionList();
-        this.fileUploadSuccessCallbacks = new n.Helper.FunctionList();
-        this.fileUploadErrorCallbacks = new n.Helper.FunctionList();
+        this.eventEmitter = new EventEmitter();
 
         this.setupOnChangeHandler();
     };
 
-    /**
-     * @public
-     * @param handler
-     */
-    n.FileUpload.prototype.addFileUploadStartHandler = function (handler) {
-        this.fileUploadStartCallbacks.addFunction(handler);
-    };
-
-    /**
-     * @public
-     * @param handler
-     */
-    n.FileUpload.prototype.addFileUploadEndHandler = function (handler) {
-        this.fileUploadEndCallbacks.addFunction(handler);
-    };
-
-    /**
-     * @public
-     * @param handler
-     */
-    n.FileUpload.prototype.addFileUploadSuccessHandler = function (handler) {
-        this.fileUploadSuccessCallbacks.addFunction(handler);
-    };
-
-    /**
-     * @public
-     * @param handler
-     */
-    n.FileUpload.prototype.addFileUploadErrorHandler = function (handler) {
-        this.fileUploadErrorCallbacks.addFunction(handler);
-    };
+    n.FileUpload.prototype.on = n.EventEmitter.onMixin;
 
     /**
      *
@@ -70,7 +37,7 @@ if (!QfqNS) {
      * @param event
      */
     n.FileUpload.prototype.performFileUpload = function (event) {
-        this.fileUploadStartCallbacks.call(event.target);
+        this.eventEmitter.emitEvent('fileupload.started', n.EventEmitter.makePayload(event.target, null));
 
         var data = this.prepareData(event.target);
 
@@ -111,8 +78,12 @@ if (!QfqNS) {
      */
 
     n.FileUpload.prototype.ajaxSuccessHandler = function (uploadTriggeredBy, data, textStatus, jqXHR) {
-        this.fileUploadSuccessCallbacks.call(uploadTriggeredBy, data, textStatus);
-        this.fileUploadEndCallbacks.call(uploadTriggeredBy);
+        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, data, {
+            textStatus: textStatus,
+            jqXHR: jqXHR
+        });
+        this.eventEmitter.emitEvent('fileupload.upload.successful', eventData);
+        this.eventEmitter.emitEvent('fileupload.ended', eventData);
     };
 
     /**
@@ -122,8 +93,13 @@ if (!QfqNS) {
      * @param errorThrown
      */
     n.FileUpload.prototype.ajaxErrorHandler = function (uploadTriggeredBy, jqXHR, textStatus, errorThrown) {
-        this.fileUploadErrorCallbacks.call(uploadTriggeredBy, textStatus, errorThrown);
-        this.fileUploadEndCallbacks.call(uploadTriggeredBy);
+        var eventData = n.EventEmitter.makePayload(uploadTriggeredBy, null, {
+            textStatus: textStatus,
+            errorThrown: errorThrown,
+            jqXHR: jqXHR
+        });
+        this.eventEmitter.emitEvent('fileupload.upload.failed', eventData);
+        this.eventEmitter.emitEvent('fileupload.ended', eventData);
     };
 
 
diff --git a/javascript/src/Form.js b/javascript/src/Form.js
index 0899f180d3f727b0f70e69e58778ac17e10bc5e0..81245710ca93f3458b81cb8eea84ea5dbadfa31c 100644
--- a/javascript/src/Form.js
+++ b/javascript/src/Form.js
@@ -3,27 +3,23 @@
  */
 
 /* global $ */
+/* global EventEmitter */
+/* @depend QfqEvents.js */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
 
     n.Form = function (formId) {
         this.formId = formId;
+        this.eventEmitter = new EventEmitter();
 
         if (!document.forms[this.formId]) {
             throw new Error("Form '" + formId + "' does not exist.");
         }
 
         this.formChanged = false;
-        this.userFormChangeHandlers = new QfqNS.Helper.FunctionList();
-        this.userResetHandlers = new QfqNS.Helper.FunctionList();
-        this.userSubmitSuccessHandlers = new QfqNS.Helper.FunctionList();
-        this.userSubmitFailureHandlers = new QfqNS.Helper.FunctionList();
-
         this.$form = $(document.forms[this.formId]);
         this.$form.on("change", this.changeHandler.bind(this));
 
@@ -32,6 +28,8 @@ if (!QfqNS) {
         this.$form.find("input[type=text]").on("input paste", this.changeHandler.bind(this));
     };
 
+    n.Form.prototype.on = n.EventEmitter.onMixin;
+
     /**
      *
      * @param event
@@ -40,23 +38,8 @@ if (!QfqNS) {
      */
     n.Form.prototype.changeHandler = function (event) {
         this.formChanged = true;
-        this.userFormChangeHandlers.call(this);
-    };
-
-    n.Form.prototype.addChangeHandler = function (callback) {
-        this.userFormChangeHandlers.addFunction(callback);
-    };
-
-    n.Form.prototype.addResetHandler = function (callback) {
-        this.userResetHandlers.addFunction(callback);
-    };
-
-    n.Form.prototype.addSubmitSuccessHandler = function (callback) {
-        this.userSubmitSuccessHandlers.addFunction(callback);
-    };
-
-    n.Form.prototype.addSubmitFailureHandler = function (callback) {
-        this.userSubmitFailureHandlers.addFunction(callback);
+        this.eventEmitter.emitEvent('form.changed', n.EventEmitter.makePayload(this, null));
+        // REMOVE: this.userFormChangeHandlers.call(this);
     };
 
     n.Form.prototype.getFormChanged = function () {
@@ -65,7 +48,8 @@ if (!QfqNS) {
 
     n.Form.prototype.resetFormChanged = function () {
         this.formChanged = false;
-        this.userResetHandlers.call(this);
+        this.eventEmitter.emitEvent('form.reset', n.EventEmitter.makePayload(this, null));
+        // REMOVE: this.userResetHandlers.call(this);
     };
 
     n.Form.prototype.submitTo = function (to) {
@@ -87,7 +71,11 @@ if (!QfqNS) {
      * @private
      */
     n.Form.prototype.ajaxSuccessHandler = function (data, textStatus, jqXHR) {
-        this.userSubmitSuccessHandlers.call(this, data, textStatus);
+        this.eventEmitter.emitEvent('form.submit.successful',
+            n.EventEmitter.makePayload(this, data, {
+                textStatus: textStatus,
+                jqXHR: jqXHR
+            }));
     };
 
     /**
@@ -96,7 +84,12 @@ if (!QfqNS) {
      * @private
      */
     n.Form.prototype.submitFailureHandler = function (jqXHR, textStatus, errorThrown) {
-        this.userSubmitFailureHandlers.call(this, textStatus, jqXHR, errorThrown);
+        this.eventEmitter.emitEvent('form.submit.failed', n.EventEmitter.makePayload(this, null, {
+            textStatus: textStatus,
+            errorThrown: errorThrown,
+            jqXHR: jqXHR
+        }));
+        // REMOVE: this.userSubmitFailureHandlers.call(this, textStatus, jqXHR, errorThrown);
     };
 
 })(QfqNS);
diff --git a/javascript/src/Helper/FunctionList.js b/javascript/src/Helper/FunctionList.js
index 9a877641fbf84f242b29117bb3028557f44ee757..f0ccd20cb3907766636b03d0f5fee5123f8cb188 100644
--- a/javascript/src/Helper/FunctionList.js
+++ b/javascript/src/Helper/FunctionList.js
@@ -4,17 +4,16 @@
 
 /* global $ */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Helper) {
-    QfqNS.Helper = {};
-}
+var QfqNS = QfqNS || {};
+QfqNS.Helper = QfqNS.Helper || {};
 
 (function (n) {
     'use strict';
 
+    /**
+     * @deprecated
+     * @constructor
+     */
     n.FunctionList = function () {
         this.functions = [];
     };
diff --git a/javascript/src/Helper/NameSpaceFunctions.js b/javascript/src/Helper/NameSpaceFunctions.js
index 5f32b8adf60feab9f19dbad3838860b1e46777c5..e4fb0da622d2044a5d3394eea4fa93b4a9d8bb66 100644
--- a/javascript/src/Helper/NameSpaceFunctions.js
+++ b/javascript/src/Helper/NameSpaceFunctions.js
@@ -4,14 +4,8 @@
 
 /* global $ */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
-
-if (!QfqNS.Helper) {
-    QfqNS.Helper = {};
-}
-
+var QfqNS = QfqNS || {};
+QfqNS.Helper = QfqNS.Helper || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/Log.js b/javascript/src/Log.js
index ddd084a85b1e8c0e8605459e45620e367898e0b7..3361f5985c779124cf5001b3f2c858c256c539df 100644
--- a/javascript/src/Log.js
+++ b/javascript/src/Log.js
@@ -4,9 +4,7 @@
 
 /* global console */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/PageState.js b/javascript/src/PageState.js
index 5138827efcb2fdce07b98dc6f1fe9283d5f92c40..52f371a38e09fafd4c4c8a955c41ea087f8d43de 100644
--- a/javascript/src/PageState.js
+++ b/javascript/src/PageState.js
@@ -2,9 +2,10 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+/* @depend QfqEvents.js */
+/* global EventEmitter */
+
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
@@ -14,10 +15,12 @@ if (!QfqNS) {
         this.pageState = location.hash.slice(1);
         this.data = null;
         this.inPoppingHandler = false;
-        this.userPopStateHandlers = new n.Helper.FunctionList();
+        this.eventEmitter = new EventEmitter();
 
         window.addEventListener("popstate", this.popStateHandler.bind(this));
     };
+
+    n.PageState.prototype.on = n.EventEmitter.onMixin;
     /**
      *
      * @param event
@@ -34,7 +37,7 @@ if (!QfqNS) {
 
         n.Log.debug("PageState.popStateHandler(): invoke user pop state handler(s)");
 
-        this.userPopStateHandlers.call(this);
+        this.eventEmitter.emitEvent('pagestate.state.popped', n.EventEmitter.makePayload(this, null));
 
         this.inPoppingHandler = false;
         n.Log.debug("Exit: PageState.popStateHandler()");
@@ -72,9 +75,4 @@ if (!QfqNS) {
         this.data = data;
     };
 
-    n.PageState.prototype.addStateActivationHandler = function (handler) {
-        this.userPopStateHandlers.addFunction(handler);
-    };
-
-
 })(QfqNS);
\ No newline at end of file
diff --git a/javascript/src/PageTitle.js b/javascript/src/PageTitle.js
index 29e45b25f32a9705c7fbfa857401ff19d4aff21e..e5e4567b8d707ad52dc562e7039999838d3800ca 100644
--- a/javascript/src/PageTitle.js
+++ b/javascript/src/PageTitle.js
@@ -2,10 +2,7 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
diff --git a/javascript/src/QfqEvents.js b/javascript/src/QfqEvents.js
new file mode 100644
index 0000000000000000000000000000000000000000..26be83cc0395ee412c55c7f7b2a4ae27542f7cd3
--- /dev/null
+++ b/javascript/src/QfqEvents.js
@@ -0,0 +1,28 @@
+/**
+ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
+ */
+
+/* global EventEmitter */
+/* global $ */
+
+var QfqNS = QfqNS || {};
+
+(function (n) {
+    'use strict';
+
+    n.EventEmitter = {
+        makePayload: function (target, data, additionalArgs) {
+            return [$.extend({},
+                typeof additionalArgs === "object" ? additionalArgs : null,
+                {
+                    target: target,
+                    data: data
+                }
+            )];
+        },
+        onMixin: function (event, func) {
+            this.eventEmitter.addListener(event, func);
+        }
+    };
+
+})(QfqNS);
\ No newline at end of file
diff --git a/javascript/src/QfqForm.js b/javascript/src/QfqForm.js
index fc69cead71e5974392477a76b37ae11b01c22bc5..5898b57d5f96ae543977cec87db6263f91df5342 100644
--- a/javascript/src/QfqForm.js
+++ b/javascript/src/QfqForm.js
@@ -3,14 +3,15 @@
  */
 
 /* global $ */
+/* global EventEmitter */
+/* @depend QfqEvents.js */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
 
+    // TODO: This object is getting too big. Start refactoring.
     n.QfqForm = function (formId, submitTo, deleteUrl, dataRefreshUrl, fileUploadTo) {
         this.formId = formId;
         this.submitTo = submitTo;
@@ -21,14 +22,16 @@ if (!QfqNS) {
         // This is required when displaying validation messages, in to activate the tab, which has validation issues
         this.bsTabs = null;
         this.lastButtonPress = null;
-        this.destroyFormUserCallbacks = new n.Helper.FunctionList();
+        this.eventEmitter = new EventEmitter();
 
         this.getSaveButton().addClass("disabled").attr("disabled", "disabled");
 
-        this.form.addChangeHandler(this.changeHandler.bind(this));
-        this.form.addResetHandler(this.resetHandler.bind(this));
-        this.form.addSubmitSuccessHandler(this.submitSuccessDispatcher.bind(this));
-        this.form.addSubmitFailureHandler(this.submitFailureHandler.bind(this));
+        this.form.on('form.changed', this.changeHandler.bind(this));
+        this.form.on('form.reset', this.resetHandler.bind(this));
+        this.form.on('form.submit.successful', this.submitSuccessDispatcher.bind(this));
+        this.form.on('form.submit.failed', function (obj) {
+            n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
+        });
 
         this.getSaveButton().click(this.handleSaveClick.bind(this));
         this.getCloseButton().click(this.handleCloseClick.bind(this));
@@ -37,15 +40,41 @@ if (!QfqNS) {
 
         this.setupFormUpdateHandler();
 
-        this.fileUploader = new QfqNS.FileUpload('#' + this.formId, this.fileUploadTo, this.getSip());
-        this.fileUploader.addFileUploadStartHandler(this.startUploadHandler);
-        this.fileUploader.addFileUploadEndHandler(this.endUploadHandler);
+        this.fileUploader = new n.FileUpload('#' + this.formId, this.fileUploadTo, this.getSip());
+        this.fileUploader.on('fileupload.started', this.startUploadHandler);
+        this.fileUploader.on('fileupload.upload.success', this.fileUploadSuccessHandler);
+
+        this.fileUploader.on('fileupload.upload.failed',
+            function (obj) {
+                n.Helper.showAjaxError(null, obj.textStatus, obj.errorThrown);
+            });
+        this.fileUploader.on('fileupload.ended', this.endUploadHandler);
     };
 
+    n.QfqForm.prototype.on = n.EventEmitter.onMixin;
+
+    /**
+     * @public
+     * @param bsTabs
+     */
     n.QfqForm.prototype.setBsTabs = function (bsTabs) {
         this.bsTabs = bsTabs;
     };
 
+    /**
+     * @private
+     */
+    n.QfqForm.prototype.fileUploadSuccessHandler = function (obj) {
+        if (!obj.data.status) {
+            throw Error("Response on file upload missing status");
+        }
+
+        if (obj.data.status === "error") {
+            var alert = new n.Alert(obj.data.message, "error");
+            alert.show();
+        }
+    };
+
     /**
      *
      * @param $button
@@ -87,7 +116,7 @@ if (!QfqNS) {
         }
 
         if (data.status === "error") {
-            var alert = new QfqNS.Alert("Error while updating form:<br>" + (data.message ? data.message : "No reason" +
+            var alert = new n.Alert("Error while updating form:<br>" + (data.message ? data.message : "No reason" +
                 " given"), "error");
             alert.show();
             return;
@@ -112,7 +141,7 @@ if (!QfqNS) {
     n.QfqForm.prototype.destroyFormAndSetText = function (text) {
         this.form = null;
         $('#' + this.formId).replaceWith($("<p>").append(text));
-        this.destroyFormUserCallbacks.call();
+        this.eventEmitter.emitEvent('qfqform.destroyed', n.EventEmitter.makePayload(this, null));
     };
 
     /**
@@ -120,7 +149,7 @@ if (!QfqNS) {
      */
     n.QfqForm.prototype.handleSaveClick = function () {
         this.lastButtonPress = "save";
-        QfqNS.Log.debug("save click");
+        n.Log.debug("save click");
         // First, remove all validation states, in case a previous submit has set a validation state, thus we're not
         // stockpiling them.
         this.clearAllValidationStates();
@@ -133,12 +162,12 @@ if (!QfqNS) {
     n.QfqForm.prototype.handleCloseClick = function () {
         this.lastButtonPress = "close";
         if (this.form.getFormChanged()) {
-            var alert = new QfqNS.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave");
+            var alert = new n.Alert("You have unsaved changes. Do you want to close?", "warning", "yesnosave");
             var that = this;
-            alert.addSaveButtonHandler(function () {
+            alert.on('alert.save', function () {
                 that.form.submitTo(that.submitTo);
             });
-            alert.addOkButtonHandler(function () {
+            alert.on('alert.ok', function () {
                 window.history.back();
             });
             alert.show();
@@ -152,7 +181,7 @@ if (!QfqNS) {
      */
     n.QfqForm.prototype.handleNewClick = function () {
         this.lastButtonPress = "new";
-        QfqNS.Log.debug("new click");
+        n.Log.debug("new click");
     };
 
     /**
@@ -160,10 +189,10 @@ if (!QfqNS) {
      */
     n.QfqForm.prototype.handleDeleteClick = function () {
         this.lastButtonPress = "delete";
-        QfqNS.Log.debug("delete click");
-        var alert = new QfqNS.Alert("Do you really want to delete the record?", "warning", "yesno");
+        n.Log.debug("delete click");
+        var alert = new n.Alert("Do you really want to delete the record?", "warning", "yesno");
         var that = this;
-        alert.addOkButtonHandler(function () {
+        alert.on('alert.ok', function () {
             $.post(that.deleteUrl)
                 .done(that.ajaxDeleteSuccessDispatcher.bind(that))
                 .fail(n.Helper.showAjaxError);
@@ -215,7 +244,7 @@ if (!QfqNS) {
         }
 
         if (data.redirect === "no") {
-            var alert = new QfqNS.Alert("redirect=='no' not allowed", "error");
+            var alert = new n.Alert("redirect=='no' not allowed", "error");
             alert.show();
             return;
         }
@@ -236,17 +265,17 @@ if (!QfqNS) {
         if (!data.message) {
             throw Error("Status is 'error' but required 'message' attribute is missing.");
         }
-        var alert = new QfqNS.Alert(data.message, "error");
+        var alert = new n.Alert(data.message, "error");
         alert.show();
     };
 
     /**
      *
-     * @param form {QfqNS.QfqForm}
+     * @param form {n.QfqForm}
      *
      * @private
      */
-    n.QfqForm.prototype.changeHandler = function (form) {
+    n.QfqForm.prototype.changeHandler = function (obj) {
         this.getSaveButton().removeClass("disabled");
         this.getSaveButton().addClass("alert-warning");
         this.getSaveButton().removeAttr("disabled");
@@ -254,11 +283,11 @@ if (!QfqNS) {
 
     /**
      *
-     * @param form {QfqNS.QfqForm}
+     * @param form {n.QfqForm}
      *
      * @private
      */
-    n.QfqForm.prototype.resetHandler = function (form) {
+    n.QfqForm.prototype.resetHandler = function (obj) {
         this.getSaveButton().removeClass("alert-warning");
         this.getSaveButton().addClass("disabled");
         this.getSaveButton().attr("disabled", "disabled");
@@ -304,36 +333,24 @@ if (!QfqNS) {
         return $("#new-button");
     };
 
-    /**
-     *
-     * @param jqXHR
-     * @param textStatus
-     * @param errorThrown
-     *
-     * @private
-     */
-    n.QfqForm.prototype.submitFailureHandler = function (form, textStatus, jqXHR, errorThrown) {
-        n.Helper.showAjaxError(errorThrown);
-    };
-
 
     /**
      * @private
      */
-    n.QfqForm.prototype.submitSuccessDispatcher = function (form, data, textStatus) {
-        if (!data.status) {
+    n.QfqForm.prototype.submitSuccessDispatcher = function (obj) {
+        if (!obj.data.status) {
             throw new Error("No 'status' property in 'data'");
         }
 
-        switch (data.status) {
+        switch (obj.data.status) {
             case "error":
-                this.handleLogicSubmitError(form, data);
+                this.handleLogicSubmitError(obj.target, obj.data);
                 break;
             case "success":
-                this.handleSubmitSuccess(form, data);
+                this.handleSubmitSuccess(obj.target, obj.data);
                 break;
             default:
-                throw new Error("Status '" + data.status + "' unknown.");
+                throw new Error("Status '" + obj.data.status + "' unknown.");
         }
 
     };
@@ -349,7 +366,7 @@ if (!QfqNS) {
         if (!data.message) {
             throw Error("Status is 'error' but required 'message' attribute is missing.");
         }
-        var alert = new QfqNS.Alert(data.message, "error");
+        var alert = new n.Alert(data.message, "error");
         alert.show();
 
         if (data["field-name"] && this.bsTabs) {
@@ -371,12 +388,12 @@ if (!QfqNS) {
      * @private
      */
     n.QfqForm.prototype.handleSubmitSuccess = function (form, data) {
-        QfqNS.Log.debug('Reset form state');
+        n.Log.debug('Reset form state');
         form.resetFormChanged();
 
         if (this.lastButtonPress === 'save' || !data.redirect || data.redirect === "no") {
             if (data.message) {
-                var alert = new QfqNS.Alert(data.message);
+                var alert = new n.Alert(data.message);
                 alert.timeout = 1500;
                 alert.show();
             }
@@ -404,7 +421,7 @@ if (!QfqNS) {
     n.QfqForm.prototype.getFormGroupByControlName = function (formControlName) {
         var $formControl = $("[name='" + formControlName + "']");
         if ($formControl.length === 0) {
-            QfqNS.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName + "'");
+            n.Log.debug("QfqForm.setValidationState(): unable to find form control with name '" + formControlName + "'");
             return null;
         }
 
@@ -484,7 +501,7 @@ if (!QfqNS) {
             var configurationItem = configuration[i];
             var formElementName = configurationItem["form-element"];
             if (formElementName === undefined) {
-                QfqNS.Log.error("configuration lacks 'form-element' attribute. Skipping.");
+                n.Log.error("configuration lacks 'form-element' attribute. Skipping.");
                 continue;
             }
             try {
@@ -503,7 +520,7 @@ if (!QfqNS) {
 
                 }
             } catch (e) {
-                QfqNS.Log.error(e.message);
+                n.Log.error(e.message);
             }
         }
     };
@@ -512,8 +529,8 @@ if (!QfqNS) {
      * @private
      * @param triggeredBy
      */
-    n.QfqForm.prototype.startUploadHandler = function (triggeredBy) {
-        $(triggeredBy).after(
+    n.QfqForm.prototype.startUploadHandler = function (obj) {
+        $(obj.target).after(
             $('<i>').addClass('spinner')
         );
     };
@@ -522,15 +539,11 @@ if (!QfqNS) {
      * @private
      * @param triggeredBy
      */
-    n.QfqForm.prototype.endUploadHandler = function (triggeredBy) {
-        var $siblings = $(triggeredBy).siblings();
+    n.QfqForm.prototype.endUploadHandler = function (obj) {
+        var $siblings = $(obj.target).siblings();
         $siblings.filter("i").remove();
     };
 
-    n.QfqForm.prototype.ajaxFileUploadErrorHandler = function (triggeredBy, jqHXR, textStatus, errorThrown) {
-        n.Helper.showAjaxError(jqHXR, textStatus, errorThrown);
-    };
-
     /**
      * Retrieve SIP as stored in hidden input field.
      *
diff --git a/javascript/src/QfqPage.js b/javascript/src/QfqPage.js
index de80fa53db75356d93917540e67c86f7337b3cc1..e07a10d40abbd3da9a7d53ce8351f926632129d7 100644
--- a/javascript/src/QfqPage.js
+++ b/javascript/src/QfqPage.js
@@ -4,10 +4,9 @@
 
 /* global $ */
 /* global console */
+/* @depend QfqEvents.js */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
@@ -36,9 +35,8 @@ if (!QfqNS) {
                 this.settings.pageState.setPageState(this.bsTabs.getCurrentTab(), n.PageTitle.get());
             }
 
-
-            this.bsTabs.addTabShowHandler(this.tabShowHandler.bind(this));
-            this.settings.pageState.addStateActivationHandler(this.popStateHandler.bind(this));
+            this.bsTabs.on('bootstrap.tab.shown', this.tabShowHandler.bind(this));
+            this.settings.pageState.on('pagestate.state.popped', this.popStateHandler.bind(this));
         } catch (e) {
             n.Log.message(e.message);
             this.bsTabs = null;
@@ -52,7 +50,7 @@ if (!QfqNS) {
                 this.settings.refreshUrl,
                 this.settings.fileUploadTo);
             this.qfqForm.setBsTabs(this.bsTabs);
-            this.qfqForm.destroyFormUserCallbacks.addFunction(this.destroyFormHandler.bind(this));
+            this.qfqForm.on('qfqform.destroyed', this.destroyFormHandler.bind(this));
         } catch (e) {
             n.Log.error(e.message);
             this.qfqForm = null;
@@ -62,12 +60,12 @@ if (!QfqNS) {
     /**
      * @private
      */
-    n.QfqPage.prototype.destroyFormHandler = function () {
+    n.QfqPage.prototype.destroyFormHandler = function (obj) {
         this.settings.qfqForm = null;
         $('#' + this.settings.tabsId).remove();
     };
 
-    n.QfqPage.prototype.tabShowHandler = function (bsTabs) {
+    n.QfqPage.prototype.tabShowHandler = function (obj) {
         // tabShowHandler will be called every time the tab will be shown, regardless of whether or not this happens
         // because of BSTabs.activateTab() or user interaction.
         //
@@ -78,16 +76,15 @@ if (!QfqNS) {
                 " restoration.");
             return;
         }
-        var currentTabId = bsTabs.getCurrentTab();
+        var currentTabId = obj.target.getCurrentTab();
         n.Log.debug('Saving state: ' + currentTabId);
-        n.PageTitle.setSubTitle(bsTabs.getTabName(currentTabId));
+        n.PageTitle.setSubTitle(obj.target.getTabName(currentTabId));
         this.settings.pageState.setPageState(currentTabId, n.PageTitle.get());
-
     };
 
-    n.QfqPage.prototype.popStateHandler = function (pageState) {
-        this.bsTabs.activateTab(pageState.getPageState());
-        n.PageTitle.set(pageState.getPageData());
+    n.QfqPage.prototype.popStateHandler = function (obj) {
+        this.bsTabs.activateTab(obj.target.getPageState());
+        n.PageTitle.set(obj.target.getPageData());
     };
 
 })(QfqNS);
\ No newline at end of file
diff --git a/javascript/src/QfqRecordList.js b/javascript/src/QfqRecordList.js
index 3eae6b052e7d34c52ae42d175fae435088266b6b..18ea78a6f8a20ee521b5c269c7488829cf6cbe03 100644
--- a/javascript/src/QfqRecordList.js
+++ b/javascript/src/QfqRecordList.js
@@ -4,9 +4,7 @@
 /* global $ */
 /* global console */
 
-if (!QfqNS) {
-    var QfqNS = {};
-}
+var QfqNS = QfqNS || {};
 
 (function (n) {
     'use strict';
@@ -44,7 +42,7 @@ if (!QfqNS) {
 
         var alert = new n.Alert("Do you really want to delete the record?", "warning", "yesno");
         var that = this;
-        alert.addOkButtonHandler(function () {
+        alert.on('alert.ok', function () {
             $.post(that.deleteUrl + "?s=" + sip)
                 .done(that.ajaxDeleteSuccessDispatcher.bind(that, $recordElement))
                 .fail(n.Helper.showAjaxError);
diff --git a/mockup/alert.html b/mockup/alert.html
index c0327e5328d092e20b9cba98413a5fb7b88503d6..7e640c77a6adebcc612518d9b05bc7875e799718 100644
--- a/mockup/alert.html
+++ b/mockup/alert.html
@@ -17,6 +17,7 @@
 
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 
 <script>
diff --git a/mockup/api/uploadhandler.php b/mockup/api/uploadhandler.php
index 6690da171a36aca1b185d71fdfcfe6181fad88f2..2025a068ad0cf87fa6b6141d72dcdfe283215566 100644
--- a/mockup/api/uploadhandler.php
+++ b/mockup/api/uploadhandler.php
@@ -3,10 +3,15 @@
  * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
  */
 
-var_dump($_POST);
+header("Content-Type: text/json");
 
-foreach ($_FILES as $key => $value) {
-    echo "$key";
-    echo file_get_contents($value['tmp_name']);
+foreach ($_FILES as $key => &$value) {
+    $value['file_content'] = file_get_contents($value['tmp_name']);
 }
 
+echo json_encode([
+    'status' => "ok",
+    'files_received' => $_FILES,
+    'request_variables' => $_REQUEST
+]);
+
diff --git a/mockup/api/uploadhandler_error.php b/mockup/api/uploadhandler_error.php
new file mode 100644
index 0000000000000000000000000000000000000000..02ef17ea838ab9002c61613e4a872e097b05e367
--- /dev/null
+++ b/mockup/api/uploadhandler_error.php
@@ -0,0 +1,12 @@
+<?php
+/**
+ * @author Rafael Ostertag <rafael.ostertag@math.uzh.ch>
+ */
+
+header("Content-Type: text/json");
+
+echo json_encode([
+    'status' => "error",
+    'message'
+]);
+
diff --git a/mockup/elementconfiguration.html b/mockup/elementconfiguration.html
index 5e97dfcc122806f70a12aa8f475a593431452a2c..fad1efb7fda9ea4b5869bd23f32e8a1902802591 100644
--- a/mockup/elementconfiguration.html
+++ b/mockup/elementconfiguration.html
@@ -175,6 +175,7 @@
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
 <script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     $(function () {
diff --git a/mockup/emptyqfqpage.html b/mockup/emptyqfqpage.html
index 149958a10220af8d47bf1fe0bfe52e87baf93fcb..d32bb1252b7e90910ebacfc98e46cb69b1e9dc39 100644
--- a/mockup/emptyqfqpage.html
+++ b/mockup/emptyqfqpage.html
@@ -11,6 +11,7 @@
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
 <script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     var qfqPage = new QfqNS.QfqPage({
diff --git a/mockup/form.html b/mockup/form.html
index 655b661fdaf51b8610c1cdacf4a262d0494ba80e..fa97ecaa93b67f5f1c8b6a80fdb18fde83aac078 100644
--- a/mockup/form.html
+++ b/mockup/form.html
@@ -34,6 +34,7 @@
 </form>
 
 <script src="../js/jquery.min.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 
 <script>
diff --git a/mockup/navstate.html b/mockup/navstate.html
index eb8369746b73dc560530979a503baaa187415ac9..d48ac1da47e1485093122e0a1d3b8f29e8bbaa3d 100644
--- a/mockup/navstate.html
+++ b/mockup/navstate.html
@@ -140,9 +140,10 @@
 </section>
 
 
-<script src="../packages/jquery/js/jquery.min.js"></script>
-<script src="../packages/bootstrap/js/bootstrap.min.js"></script>
-<script src="../packages/jqwidgets/js/jqx-all.js"></script>
+<script src="../js/jquery.min.js"></script>
+<script src="../js/bootstrap.min.js"></script>
+<script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     $(function () {
diff --git a/mockup/personmock.html b/mockup/personmock.html
index 036c60ae25a750f2dc501dba88413372bfa001cd..edc72d8ebe53d857e9b5e02dcfadc5115695285f 100644
--- a/mockup/personmock.html
+++ b/mockup/personmock.html
@@ -37,6 +37,14 @@
     </select>
 </label>
 
+<label>Upload to
+    <select name="uploadTo" id="uploadTo">
+        <option>404 error</option>
+        <option>uploadhandler.php</option>
+        <option>uploadhandler_error.php</option>
+    </select>
+</label>
+
 
 <div class="container-fluid">
     <div class="row hidden-xs">
@@ -751,6 +759,7 @@
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
 <script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     $(function () {
@@ -973,7 +982,7 @@
             formId: 'myForm',
             submitTo: 'api/' + $("#submitTo").val(),
             deleteUrl: 'api/' + $("#deleteUrl").val(),
-            fileUploadTo: 'api/uploadhandler.php'
+            fileUploadTo: 'api/' + $("#uploadTo").val()
         });
 
         $("#submitTo").on("change", function (evt) {
@@ -986,6 +995,11 @@
             qfqPage.qfqForm.deleteUrl = 'api/' + $(evt.target).val();
         });
 
+        $("#uploadTo").on("change", function (evt) {
+            qfqPage.settings.fileUploadTo = 'api/' + $(evt.target).val();
+            qfqPage.qfqForm.fileUploader.targetUrl = 'api/' + $(evt.target).val();
+        });
+
         QfqNS.Log.level = 0;
     });
 </script>
diff --git a/mockup/readonly.html b/mockup/readonly.html
index b7b3115cff92bf8dafefeed9a68747c68a5eb6a6..0ac97f6739841ea4ebf95b09c427bc6492d64773 100644
--- a/mockup/readonly.html
+++ b/mockup/readonly.html
@@ -104,6 +104,7 @@
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
 <script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     $(function () {
diff --git a/mockup/recordlist.html b/mockup/recordlist.html
index fcea88d9cd4f269e786760e1d319b28d15ff7a52..a42b6a3850da1212dc75edc9e1a387bb07446359 100644
--- a/mockup/recordlist.html
+++ b/mockup/recordlist.html
@@ -61,6 +61,7 @@
 <script src="../js/jquery.min.js"></script>
 <script src="../js/bootstrap.min.js"></script>
 <script src="../js/jqx-all.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 <script type="text/javascript">
     $(function () {
diff --git a/mockup/upload.html b/mockup/upload.html
index 6ee74c3284a330052674826da601bd151d6e93aa..88d991b858fcb8d4b2a115d4a1f209665b84e7a5 100644
--- a/mockup/upload.html
+++ b/mockup/upload.html
@@ -18,24 +18,25 @@
 <pre id="display"></pre>
 
 <script src="../js/jquery.min.js"></script>
+<script src="../js/EventEmitter.min.js"></script>
 <script src="../js/qfq.debug.js"></script>
 
 <script>
     var fileUpload = new QfqNS.FileUpload('#myForm', 'api/uploadhandler.php', 'the_sip');
-    fileUpload.addFileUploadStartHandler(function () {
+    fileUpload.on('fileupload.started', function () {
         $('#progress').empty().append('<p>Upload started</p>');
     });
 
-    fileUpload.addFileUploadEndHandler(function () {
+    fileUpload.on('fileupload.ended', function () {
         $('#progress').append('<p>Upload finished</p>');
     });
 
-    fileUpload.addFileUploadSuccessHandler(function (data) {
+    fileUpload.on('fileupload.upload.successful', function (obj) {
         $('#progress').append('<p>Upload success</p>');
-        $('#display').empty().append(data);
+        $('#display').empty().append(obj.data.file_content);
     });
 
-    fileUpload.addFileUploadErrorHandler(function () {
+    fileUpload.on('fileupload.upload.failed', function () {
         $('#progress').append('<p>Upload made a booboo</p>');
     });
 
diff --git a/tests/jasmine/SpecRunner.html b/tests/jasmine/SpecRunner.html
index 3ae7adf567c8034940ca18ea57c0b81261645346..2b1365c68d32e2e1e49e5bfec9d6aaf7e3af966b 100644
--- a/tests/jasmine/SpecRunner.html
+++ b/tests/jasmine/SpecRunner.html
@@ -14,8 +14,9 @@
     <script src="lib/jasmine-2.4.1/boot.js"></script>
     <script src="helper/mock-ajax.js"></script>
 
-    <script src="../../packages/jquery/js/jquery.min.js"></script>
-    <script src="../../packages/bootstrap/js/bootstrap.min.js"></script>
+    <script src="../../js/jquery.min.js"></script>
+    <script src="../../js/bootstrap.min.js"></script>
+    <script src="../../js/EventEmitter.min.js"></script>
 
     <!-- include source files here... -->
     <script src="../../js/qfq.debug.js"></script>