Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
95 changes: 95 additions & 0 deletions src/js/common-event-bindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
/* 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)"
}
});

fluid.registerNamespace("fluid.decoratorViewComponent");

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

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.expandOptions(arg, that);
}
}
return expanded;
};

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");
var func = node[dec.method];
return func.apply(node, expanded);
};

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);
var decs = fluid.makeArray(val);
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);
}
});
}
});
};
})();
3 changes: 2 additions & 1 deletion tests/static/all-tests.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,8 @@
"/tests-binder-select.html",
"/tests-binder-short.html",
"/tests-binder-textarea.html",
"/tests-binder-transforms.html"
"/tests-binder-transforms.html",
"/tests-binder-commonEventBindings.html"
]
};

Expand Down
92 changes: 92 additions & 0 deletions tests/static/js/jqunit/jqUnit-binder-commonEventBindings.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/* 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",
paragraphClick: ".parapgraph-click"
},
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

},
paragraphClick: {
method: "click",
args: ["{that}.handleParagraphClick"]
}
},
invokers: {
handleInputClick: {
funcName: "gpii.tests.binder.commonEventBindings.handleInputClick",
args: ["{arguments}.0"]
},
handleParagraphClick: {
funcName: "gpii.tests.binder.commonEventBindings.handleParagraphClick",
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.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 click on a paragraph with a class",
type: "test",
expect: 1,
sequence: [
{
func: "gpii.tests.binder.clickSelector",
args: [".parapgraph-click"]
}
]
}
]
}]
});

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.

})();
58 changes: 58 additions & 0 deletions tests/static/tests-binder-commonEventBindings.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<html>
<head>
<title>Unit Tests for Binder Module Common Event Bindings button support...</title>

<!-- Bring in jQuery and other dependencies -->
<script type="text/javascript" src="../../node_modules/infusion/src/lib/jquery/core/js/jquery.js"></script>

<!-- Bring in fluid dependencies -->
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/Fluid.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/FluidDocument.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/FluidDOMUtilities.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/FluidIoC.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/DataBinding.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/FluidView.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/ModelTransformation.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/core/js/ModelTransformationTransforms.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/enhancement/js/ContextAwareness.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/src/framework/enhancement/js/ProgressiveEnhancement.js"></script>

<script type="text/javascript" src="../../node_modules/infusion/tests/test-core/utils/js/IoCTestUtils.js"></script>

<!-- QUnit/jqUnit dependencies -->
<script type="text/javascript" src="../../node_modules/infusion/tests/lib/qunit/js/qunit.js"></script>
<script src="/testem.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/tests/test-core/jqUnit/js/jqUnit.js"></script>
<script type="text/javascript" src="../../node_modules/infusion/tests/test-core/jqUnit/js/jqUnit-browser.js"></script>

<!-- Our test fixtures -->
<script type="text/javascript" src="./js/tests-fixtures.js"></script>

<!-- Bring in client library being tested -->
<script type="text/javascript" src="/coverage/client/coverageSender.js"></script>
<script type="text/javascript" src="../../src/js/binder.js"></script>
<script type="text/javascript" src="../../src/js/common-event-bindings.js"></script>

<link rel="stylesheet" href="../../node_modules/node-jqunit/node_modules/infusion/tests/lib/qunit/css/qunit.css"/>
</head>
<body>
<h1 id="qunit-header">"Common Event Bindings" Component Tests</h1>
<h2 id="qunit-banner"></h2>
<div id="qunit-testrunner-toolbar"></div>
<h2 id="qunit-userAgent"></h2>
<ol id="qunit-tests"></ol>

<!-- Test HTML, we have to hide this ourselves. QUnit's styles put it offscreen, which doesn't work for us. -->
<div class="viewport-common-event-bindings" style="display:none">
<form>
<h3>Common Event Bindings...</h3>
<input type="button" id="input-button" value="First Input Button" />

<p class="parapgraph-click">This is a paragraph.</p>
</form>
</div>

<!-- our QUnit tests -->
<script type="text/javascript" src="./js/jqunit/jqunit-binder-commonEventBindings.js"></script>
</body>
</html>