Skip to content

Commit

Permalink
[FEATURE] Improved closure based actions per RFC #50
Browse files Browse the repository at this point in the history
  • Loading branch information
mixonic committed May 9, 2015
1 parent 8fe0caa commit e19b9e2
Show file tree
Hide file tree
Showing 9 changed files with 626 additions and 131 deletions.
10 changes: 10 additions & 0 deletions FEATURES.md
Original file line number Diff line number Diff line change
Expand Up @@ -266,3 +266,13 @@ for a detailed explanation.
```
Addd in [#10461](https://github.com/emberjs/ember.js/pull/10461)
* `ember-routing-htmlbars-improved-actions`
Using the `(action` subexpression, allow for the creation of closure-wrapped
callbacks to pass into downstream components. The returned value of
the `(action` subexpression (or `submit={{action 'save'}}` style subexpression)
is a function. mut objects expose an `INVOKE` interface making them
compatible with action subexpressions.
Per RFC [#50](https://github.com/emberjs/rfcs/pull/50)
3 changes: 2 additions & 1 deletion features.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
"ember-application-visit": null,
"ember-views-component-block-info": null,
"ember-routing-core-outlet": null,
"ember-libraries-isregistered": null
"ember-libraries-isregistered": null,
"ember-routing-htmlbars-improved-actions": null
},
"debugStatements": [
"Ember.warn",
Expand Down
6 changes: 5 additions & 1 deletion packages/ember-htmlbars/lib/keywords/mut.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import merge from "ember-metal/merge";
import { symbol } from "ember-metal/utils";
import ProxyStream from "ember-metal/streams/proxy-stream";
import { MUTABLE_CELL } from "ember-views/compat/attrs-proxy";
import { INVOKE } from "ember-routing-htmlbars/keywords/closure-action";

export let MUTABLE_REFERENCE = symbol("MUTABLE_REFERENCE");

Expand Down Expand Up @@ -52,11 +53,14 @@ merge(MutStream.prototype, {
let val = {
value: source.value(),
update(val) {
source.sourceDep.setValue(val);
source.setValue(val);
}
};

val[MUTABLE_CELL] = true;
return val;
},
[INVOKE](val) {
this.setValue(val);
}
});
136 changes: 10 additions & 126 deletions packages/ember-routing-htmlbars/lib/keywords/action.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
@submodule ember-htmlbars
*/

import Ember from "ember-metal/core"; // assert
import { uuid } from "ember-metal/utils";
import run from "ember-metal/run_loop";
import { readUnwrappedModel } from "ember-views/streams/utils";
import { isSimpleClick } from "ember-views/system/utils";
import ActionManager from "ember-views/system/action_manager";
import { keyword } from "htmlbars-runtime/hooks";
import closureAction from "ember-routing-htmlbars/keywords/closure-action";

/**
The `{{action}}` helper provides a useful shortcut for registering an HTML
Expand Down Expand Up @@ -173,128 +169,16 @@ import ActionManager from "ember-views/system/action_manager";
@param {Object} [context]*
@param {Hash} options
*/
export default {
setupState: function(state, env, scope, params, hash) {
var getStream = env.hooks.get;
var read = env.hooks.getValue;

var actionName = read(params[0]);

Ember.assert("You specified a quoteless path to the {{action}} helper " +
"which did not resolve to an action name (a string). " +
"Perhaps you meant to use a quoted actionName? (e.g. {{action 'save'}}).",
typeof actionName === 'string');

var actionArgs = [];
for (var i = 1, l = params.length; i < l; i++) {
actionArgs.push(readUnwrappedModel(params[i]));
export default function(morph, env, scope, params, hash, template, inverse, visitor) {
if (Ember.FEATURES.isEnabled("ember-routing-htmlbars-improved-actions")) {
if (morph) {
keyword('@element_action', morph, env, scope, params, hash, template, inverse, visitor);
return true;
}

var target;
if (hash.target) {
if (typeof hash.target === 'string') {
target = read(getStream(env, scope, hash.target));
} else {
target = read(hash.target);
}
} else {
target = read(scope.locals.controller) || read(scope.self);
}

return { actionName, actionArgs, target };
},

isStable: function(state, env, scope, params, hash) {
return closureAction(morph, env, scope, params, hash, template, inverse, visitor);
} else {
keyword('@element_action', morph, env, scope, params, hash, template, inverse, visitor);
return true;
},

render: function(node, env, scope, params, hash, template, inverse, visitor) {
var actionId = ActionHelper.registerAction({
node: node,
eventName: hash.on || "click",
bubbles: hash.bubbles,
preventDefault: hash.preventDefault,
withKeyCode: hash.withKeyCode,
allowedKeys: hash.allowedKeys
});

node.cleanup = function() {
ActionHelper.unregisterAction(actionId);
};

env.dom.setAttribute(node.element, 'data-ember-action', actionId);
}
};

export var ActionHelper = {};

// registeredActions is re-exported for compatibility with older plugins
// that were using this undocumented API.
ActionHelper.registeredActions = ActionManager.registeredActions;

ActionHelper.registerAction = function({ node, eventName, preventDefault, bubbles, allowedKeys }) {
var actionId = uuid();

ActionManager.registeredActions[actionId] = {
eventName,
handler(event) {
if (!isAllowedEvent(event, allowedKeys)) {
return true;
}

if (preventDefault !== false) {
event.preventDefault();
}

if (bubbles === false) {
event.stopPropagation();
}

let { target, actionName, actionArgs } = node.state;

run(function runRegisteredAction() {
if (target.send) {
target.send.apply(target, [actionName, ...actionArgs]);
} else {
Ember.assert(
"The action '" + actionName + "' did not exist on " + target,
typeof target[actionName] === 'function'
);

target[actionName].apply(target, actionArgs);
}
});
}
};

return actionId;
};

ActionHelper.unregisterAction = function(actionId) {
delete ActionManager.registeredActions[actionId];
};

var MODIFIERS = ["alt", "shift", "meta", "ctrl"];
var POINTER_EVENT_TYPE_REGEX = /^click|mouse|touch/;

function isAllowedEvent(event, allowedKeys) {
if (typeof allowedKeys === "undefined") {
if (POINTER_EVENT_TYPE_REGEX.test(event.type)) {
return isSimpleClick(event);
} else {
allowedKeys = '';
}
}

if (allowedKeys.indexOf("any") >= 0) {
return true;
}

for (var i=0, l=MODIFIERS.length;i<l;i++) {
if (event[MODIFIERS[i] + "Key"] && allowedKeys.indexOf(MODIFIERS[i]) === -1) {
return false;
}
}

return true;
}
76 changes: 76 additions & 0 deletions packages/ember-routing-htmlbars/lib/keywords/closure-action.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import Stream from "ember-metal/streams/stream";
import { map } from "ember-metal/array";
import {
read,
readArray
} from "ember-metal/streams/utils";
import keys from 'ember-metal/keys';
import { symbol } from "ember-metal/utils";

export const INVOKE = symbol("INVOKE");

export default function closureAction(morph, env, scope, params, hash, template, inverse, visitor) {
return new Stream(function() {
map.call(params, this.addDependency, this);
map.call(keys(hash), (item) => {
this.addDependency(item);
});

var rawAction = params[0];
var actionArguments = readArray(params.slice(1, params.length));

var target, action, valuePath;
if (rawAction[INVOKE]) {
// on-change={{action (mut name)}}
target = rawAction;
action = rawAction[INVOKE];
} else {
// on-change={{action setName}}
// element-space actions look to "controller" then target. Here we only
// look to "target".
target = read(scope.self);
action = read(rawAction);
if (typeof action === 'string') {
// on-change={{action 'setName'}}
actionArguments.unshift(action);
if (hash.target) {
// on-change={{action 'setName' target=alternativeComponent}}
target = read(hash.target);
}
action = target.send;
}
}

if (hash.value) {
// <button on-keypress={{action (mut name) value="which"}}
// on-keypress is not even an Ember feature yet
valuePath = read(hash.value);
}

return createClosureAction(target, action, valuePath, actionArguments);
});
}

function createClosureAction(target, action, valuePath, actionArguments) {
if (actionArguments.length > 0) {
return function() {
var args = actionArguments;
if (arguments.length > 0) {
args = actionArguments.concat(Array.prototype.slice.apply(arguments));
}
if (valuePath && args.length > 0) {
args[0] = Ember.get(args[0], valuePath);
}
return action.apply(target, args);
};
} else {
return function() {
var args = arguments;
if (valuePath && args.length > 0) {
args = Array.prototype.slice.apply(args);
args[0] = Ember.get(args[0], valuePath);
}
return action.apply(target, args);
};
}
}
Loading

0 comments on commit e19b9e2

Please sign in to comment.