Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
41 changes: 41 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,47 @@ If your component generates or regenerates markup, you will need to call `gpii.b
whenever it changes. The `gpii.binder.bindOnDomChange` grade included in this package will reapply the bindings
whenever an `onDomChange` event is fired.

## Markup Event Bindings

In addition to bi-directional binding of model variables to DOM elements denoted by selectors, another feature is
convenient binding of DOM events by selectors. This allows a streamlined syntax taking a view component selector as
a key, and specifying a jQuery event type and arguments to be bound to that selector. These arguments can contain
fluid IOC references, making it easy to attach an invoker on a component to an event.

### `gpii.binder.bindMarkupEvents`

Inheriting this grade provides the event `onMarkupRendered` which will trigger the event binding. This event is automatically
fired by `gpii.handlebars.templateAware` when rendering, making this a useful mix in grade for handlebars work. You can also
fire the `onDomBind` event to trigger binding. Similar to the `binding` options detailed above, this component adds a
section `markupEventBindings` for attaching events to selectors.

Currently, all events are standard jQuery events. In the future other event models may be supported. The `args` listed
will be passed to the jQuery event, meaning you can use infusion syntax to pass a components invoker as the first
argument to be the callback.

| Option | Type | Description |
| ------------------ | -------- | ----------- |
| `selectors` | `{Object}` | You must define one or more [selectors](http://docs.fluidproject.org/infusion/development/tutorial-gettingStartedWithInfusion/ViewComponents.html#selectors) that can be used in a binding. |
| `markupEventBindings` | `{Object}` | Defines the relationship between selectors and event bindings. This takes a string or array of strings listing DOM events, and a set of arguments to be passed to the event listener. |

#### Example: Attaching an invoker to the `click` and `keydown` events.

markupEventBindings: {
productListLinks: {
method: ["click", "keydown"]
args: ["{that}.selectProduct"] // This invoker will be passed the DOM Event as it's first argument
}
}

For a single event we can use just the string:

markupEventBindings: {
productListLinks: {
method: "click",
args: ["{that}.selectProduct"]
}
}

## Tests

To run the tests in this package, run `npm test`. In addition to the normal test output, a test coverage report will be
Expand Down
165 changes: 165 additions & 0 deletions src/js/common-event-bindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
/* global fluid */
(function () {
"use strict";

/**
* GPII Binder Markup Events Grade
* This grade allows binding typical HTML events such as
* mouse clicks and keypress events to selectors each time
* the markup is rendered for a component. (Meaning they will
* also apply if a component is rerendered after a model refresh
* or similar situation)
*
* It adds a new area to a grades options called `markupEventBindings`
* which allows binding `selectors` to jQuery events. Theoretically
* other event constructs could be supported in the future, but only
* jQuery events are implemented at the time of writing.
*
* Binding is initiated when the components `onMarkupRendered` event is
* fired. If you are using a grade derived from `gpii.handlebars.templateAware`
* this event will be automatically fired when the handlebars markup is
* rendered.
*
* Example usage of adding a click handler to a selector productListLinks.
* ```
* markupEventBindings: {
* productListLinks: {
* // type: jQuery <- Defaults to jQuery but could be configured ITF
* method: "click",
* args: ["{that}.selectProduct"]
* }
* }
* ```
*/
fluid.defaults("gpii.binder.bindMarkupEvents", {
mergePolicy: {
decorators: "noexpand"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this "decorators"?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In other words, is there a pattern we're emulating? Just curious.

},
events: {
onDomBind: null,
onDomUnbind: null,
onMarkupRendered: null
},
listeners: {
onMarkupRendered: "{that}.events.onDomBind.fire({that}, {that}.container)",
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These should all be namespaced. I'd also suggest using the "long form" to this shorter notation. That way someone can at least opt out of part of the contract in a derived grade by changing the "funcName" to fluid.identity or the like.

onDestroy: "{that}.events.onDomUnbind.fire({that}, {that}.container)",
onDomBind: "fluid.decoratorViewComponent.processDecorators({that}, {that}.options.markupEventBindings)"
}
});

/**
*
* Markup Event Binding
*
* @typedef {Object} MarkupEventBinding
* @property {String} type - Currently the only supported value is `jQuery`. This property
* can also be omitted, in which case it will default to `jQuery`.
* @property {String|Array} method - The DOM Event we are binding to such as `click`. If we
* want to listen for multiple events in the same binding this can be an array of event
* types such as `["click", "keypress"]`.
* @property {Array} args - A list of arguments to be passed to the event. This supports the
* usual range of Fluid IoC syntax. Typically, this will be an invoker on the component to be
* called when the event is triggered.
*/

/**
*
* Markup Event Bindings
*
* This is an object containing mappings of selector names to `MarkupEventBinding`
* declarations. Each key should be a name corresponding to a selector in the
* components `selectors` option block.
*
* @typedef {Object} MarkupEventBindings
*/

fluid.registerNamespace("fluid.decoratorViewComponent");

//
// The methods below might be generic enough to go straight to infusion
//

/**
*
* Expands string encoded arguments to the event invoker, filling
* in any compact string versions of infusion invokers.
*
* @param {gpii.binder.bindMarkupEvents} that - Any infusion component with sub-grade `bindMarkupEvents`.
* @param {String|Object} arg - A single argument being passed to the event invoker.
* @param {String} name - The name for the invoker.
* @return {Object} The expanded argument.
*/
fluid.expandCompoundArg = function (that, arg, name) {
var expanded = arg;
if (typeof(arg) === "string") {
if (arg.indexOf("(") !== -1) {
var invokerec = fluid.compactStringToRec(arg, "invoker");
// TODO: perhaps a a courtesy we could expose {node} or even {this}
expanded = fluid.makeInvoker(that, invokerec, name);
} else {
expanded = fluid.expandImmediate(arg, that);
}
}
return expanded;
};

/**
*
* Processes a single markup event binding decorator of event type jQuery.
* Currently the only type of event supported is jQuery events, so this does
* all the work.
*
* @param {MarkupEventBinding} dec - The single binding decorator being processed.
* @param {DOMNode} node - The node we are listening to for the specified events.
* @param {gpii.binder.bindMarkupEvents} that - Any infusion component with sub-grade `bindMarkupEvents`.
* @param {String} name - Name that will be used for the invoker created to handle
* this event.
* @return {jQuery[]} Array of jQuery objects which the events attached to them.
*/
fluid.processjQueryDecorator = function (dec, node, that, name) {
var args = fluid.makeArray(dec.args);
var expanded = fluid.transform(args, function (arg, index) {
return fluid.expandCompoundArg(that, arg, name + " argument " + index);
});
fluid.log("Got expanded value of ", expanded, " for jQuery decorator");
// Support for listing multiple methods in an array, or just a single string method
var methods = [dec.method];
var togo = [];
if (fluid.isArrayable(dec.method)) {
methods = dec.method;
}
fluid.each(methods, function (method) {
var func = node[method];
togo.push(func.apply(node, expanded));
});
return togo;
};

/**
*
* Function to process the markup binding decorators and create the events described
* by them. The markup needs to be rendered and settled before this can be called.
*
* @param {gpii.binder.bindMarkupEvents} that - Any component inheriting from `bindMarkupEvents`.
* @param {MarkupEventBindings} decorators - Markup Event Binding decorators on the component.
*/
fluid.decoratorViewComponent.processDecorators = function (that, decorators) {
fluid.each(decorators, function (val, key) {
var node = that.locate(key);
if (node.length > 0) {
var name = "Decorator for DOM node with selector " + key + " for component " + fluid.dumpThat(that);
// val can be an array to support multiple event handlers
var handlerDecs = fluid.isArrayable(val) ? val : [val];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why not just use fluid.makeArray here?

fluid.each(handlerDecs, function (nextVal) {
var decs = fluid.makeArray(nextVal);
fluid.each(decs, function (dec) {
// If no type is specified default to jQuery
if (!dec.type || dec.type === "jQuery") {
fluid.processjQueryDecorator(dec, node, that, name);
}
});
});
}
});
};
})();
4 changes: 3 additions & 1 deletion tests/static/all-tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,9 @@
"/tests-binder-select.html",
"/tests-binder-short.html",
"/tests-binder-textarea.html",
"/tests-binder-transforms.html"
"/tests-binder-transforms.html",
"/tests-binder-commonEventBindings.html",
"/tests-binder-commonEventBindingsOverride.html"
]
};

Expand Down
158 changes: 158 additions & 0 deletions tests/static/js/jqunit/jqUnit-binder-commonEventBindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
/* globals fluid, jqUnit */
(function () {
"use strict";
var gpii = fluid.registerNamespace("gpii");

// Component to test support for common event bindings
fluid.defaults("gpii.tests.binder.commonEventBindings", {
gradeNames: ["gpii.tests.binder.base", "gpii.binder.bindMarkupEvents"],
bindings: {
},
selectors: {
inputButton: "#input-button",
inputKeydown: "#input-button",
paragraphClick: ".paragraph-click",
multipleEventInputButton: "#input-multiple-events-button",
multipleEventHandlerInputButton: "#input-multiple-handlers-events-button"
},
markupEventBindings: {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also supply a test which shows that these handlers can be overridden cleanly

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a test where 1) Some are left as is, 2) some are overridden, and 3) some new one is introduced.

inputButton: {
method: "click",
args: ["{that}.handleInputClick"]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Supply further tests to validate the primitive kinds of "compact" IoC syntax we support which allows for args

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for this tip, I didn't know this was possibly actually, and will simplify my usage in the capture tool, PPT, and elsewhere

},
inputKeydown: {
method: "keydown",
args: ["{that}.handleInputClick"]
},
paragraphClick: {
method: "click",
args: ["gpii.tests.binder.commonEventBindings.handleParagraphClick({arguments}.0)"]
},
multipleEventInputButton: {
method: ["click", "keydown"],
args: ["{that}.handleInputClick"]
},
multipleEventHandlerInputButton: [
{
method: "click",
args: ["{that}.handleInputClick"]
},
{
method: "keydown",
args: ["{that}.handleInputKeydown"]
}
]
},
invokers: {
handleInputClick: {
funcName: "gpii.tests.binder.commonEventBindings.handleInputClick",
args: ["{arguments}.0"]
},
handleInputKeydown: {
funcName: "gpii.tests.binder.commonEventBindings.handleInputKeydown",
args: ["{arguments}.0"]
}
},
listeners: {
"onCreate.markupEventBindings": "{that}.events.onMarkupRendered.fire()"
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is the kind of thing I'd also write out, what if someone needs to do other work before firing the event? I guess they can just rewrite the whole string.

}
});

gpii.tests.binder.commonEventBindings.handleInputClick = function (event) {
jqUnit.assertEquals("We just clicked an input...", "INPUT", event.target.nodeName);
};

gpii.tests.binder.commonEventBindings.handleInputKeydown = function (event) {
jqUnit.assertEquals("We just keydowned an input...", "INPUT", event.target.nodeName);
};

gpii.tests.binder.commonEventBindings.handleParagraphClick = function (event) {
jqUnit.assertEquals("We just clicked a paragraph...", "P", event.target.nodeName);
};

fluid.defaults("gpii.tests.binder.commonEventBindings.caseHolder", {
gradeNames: ["gpii.tests.binder.caseHolder"],
rawModules: [{
name: "Testing support for common event bindings...",
tests: [
{
name: "Test markup event binding for click on an input button",
type: "test",
expect: 1,
sequence: [
{
func: "gpii.tests.binder.clickSelector",
args: ["#input-button"]
}
]
},
{
name: "Test markup event binding for keydown on an input button",
type: "test",
expect: 1,
sequence: [
{
func: "gpii.tests.binder.keydownSelector",
args: ["#input-button"]
}
]
},
{
name: "Test markup event binding for click on a paragraph with a class",
type: "test",
expect: 1,
sequence: [
{
func: "gpii.tests.binder.clickSelector",
args: [".paragraph-click"]
}
]
},
{
name: "Test markup event binding for both `click` and `keydown` on an input button",
type: "test",
expect: 2,
sequence: [
{
func: "gpii.tests.binder.clickSelector",
args: ["#input-multiple-events-button"]
},
{
func: "gpii.tests.binder.keydownSelector",
args: ["#input-multiple-events-button"]
}
]
},
{
name: "Test markup event binding for both `click` and `keydown` on an input button, each using a different handler",
type: "test",
expect: 2,
sequence: [
{
func: "gpii.tests.binder.clickSelector",
args: ["#input-multiple-handlers-events-button"]
},
{
func: "gpii.tests.binder.keydownSelector",
args: ["#input-multiple-handlers-events-button"]
}
]
}
]
}]
});

fluid.defaults("gpii.tests.binder.commonEventBindings.environment", {
gradeNames: ["gpii.tests.binder.environment"],
markupFixture: ".viewport-common-event-bindings",
binderGradeNames: ["gpii.tests.binder.commonEventBindings"],
moduleName: "Testing common event bindings",
components: {
commonEventBindingsTests: {
type: "gpii.tests.binder.commonEventBindings.caseHolder"
}
}
});

gpii.tests.binder.commonEventBindings.environment();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess it matters less on the browser side (I think?), but for consistency with the rest of the package, this should use fluid.test.runTests instead of calling the environment directly.

})();
Loading