diff --git a/README.md b/README.md index b298313..e973e46 100644 --- a/README.md +++ b/README.md @@ -7,38 +7,20 @@ Demo: http://jsbin.com/jipani/edit?html,js,output ember install ember-route-action-helper ``` -The `route-action` helper allows you to bubble closure actions, which will delegate it to the currently active route hierarchy per the bubbling rules explained under `actions`. Like closure actions, `route-action` will also have a return value. +The `route-action` helper allows you to call actions from routes. Like closure actions, `route-action` will also have a return value. -However, returning `true` in an action will **not** preserve bubbling semantics. In case you would like that behavior, you should use ordinary string actions instead. +Route actions do not bubble. In case you would like that behavior, you should use ordinary string actions instead. ## Usage -For example, this route template tells the component to lookup the `updateFoo` action on the route when its internal `clicked` property is invoked, and curries the function call with 2 arguments. +For example, this route template tells the component to lookup the `updateFoo` action on the current route when its internal `clicked` property is invoked, and curries the function call with 2 arguments. ```hbs {{! foo/route.hbs }} {{foo-bar clicked=(route-action "updateFoo" "Hello" "world")}} ``` -If the action is not found on the current route, it is bubbled up: - -```js -// application/route.js -import Ember from 'ember'; - -const { Route, set } = Ember; - -export default Route.extend({ - actions: { - updateFoo(...args) { - // handle action - return 42; - } - } -}); -``` - -If no action is found after bubbling, an error will be raised. The `route-action` also has a return value: +If no action is found, an error will be raised. The `route-action` also has a return value: ```js // foo/component.js @@ -65,7 +47,7 @@ You may also use in conjunction with the `{{action}}` helper: ## Compatibility -This addon will work on Ember versions `1.13.x` and up only, due to use of the new `Helper` implementation. +This addon will work on Ember versions `2.x` and up only. ## Installation diff --git a/addon/-private/internals.js b/addon/-private/internals.js index 344cbbf..558284e 100644 --- a/addon/-private/internals.js +++ b/addon/-private/internals.js @@ -1,3 +1,4 @@ +// @ts-nocheck import Ember from 'ember'; let ClosureActionModule; diff --git a/addon/helpers/route-action.js b/addon/helpers/route-action.js index 93d3c3a..417ac14 100644 --- a/addon/helpers/route-action.js +++ b/addon/helpers/route-action.js @@ -2,38 +2,94 @@ import Ember from 'ember'; import { ACTION } from '../-private/internals'; const { - A: emberArray, Helper, assert, + canInvoke, computed, - typeOf, get, getOwner, run, + isPresent, + // @ts-ignore no type signature runInDebug } = Ember; +/** + * @typedef {Object} RouterInstance + * @property {Router} _routerMicrolib + * @property {Router} router + * @property {string} currentRouteName + */ +/** + * @typedef {Object} Router + * @property {Handler[]} currentHandlerInfos + * */ +/** + * @typedef {Object} Handler + * @property {string} name + * @property {Route} handler + * */ +/** + * @typedef {Object} Route + * @property {ActionsHash} actions + * @property {ActionsHash} _actions + * */ +/** + * @typedef {Object} ActionsHash + * @property {[propName: string]: function(): void} + */ +/** + * @typedef {Object} RouteAction + * @property {function(): void} action + * @property {Route} handler + * */ + +/** + * Get current handler infos from the router. + * + * @param {RouterInstance} router + * @returns {Handler[]} + */ function getCurrentHandlerInfos(router) { let routerLib = router._routerMicrolib || router.router; - return routerLib.currentHandlerInfos; } -function getRoutes(router) { - return emberArray(getCurrentHandlerInfos(router)) - .mapBy('handler') - .reverse(); +/** + * Get current handler instances from router. + * + * @param {RouterInstance} router + * @returns {Route[]} + */ +function getCurrentHandlers(router) { + /** @type {string} */ + let currentRouteName = get(router, 'currentRouteName'); + let currentRoot = currentRouteName.replace(/\.index$/gi, ''); + return getCurrentHandlerInfos(router) + .reduce((acc, h) => { + return h.name === currentRouteName || h.name === currentRoot + ? [h.handler, ...acc] + : acc; + }, []); } -function getRouteWithAction(router, actionName) { +/** + * Get current route handler and action. + * + * @param {RouterInstance} router + * @param {string} actionName + * @returns {RouteAction} + */ +function getCurrentRouteWithAction(router, actionName) { + /** @type {function(): void} */ let action; - let handler = emberArray(getRoutes(router)).find((route) => { + /** @type {Route} */ + let handler = getCurrentHandlers(router).find((route) => { + /** @type {ActionsHash} */ let actions = route.actions || route._actions; action = actions[actionName]; - - return typeOf(action) === 'function'; + return canInvoke(actions, actionName); }); - return { action, handler }; } @@ -42,17 +98,22 @@ export default Helper.extend({ return getOwner(this).lookup('router:main'); }).readOnly(), + /** + * @param {any} [actionName, ...params] + * @returns {function(...invocationArgs: any[]): void} + */ compute([actionName, ...params]) { + /** @type {RouterInstance} */ let router = get(this, 'router'); - assert('[ember-route-action-helper] Unable to lookup router', router); + assert('[ember-route-action-helper] Unable to lookup router', isPresent(router)); runInDebug(() => { - let { handler } = getRouteWithAction(router, actionName); - assert(`[ember-route-action-helper] Unable to find action ${actionName}`, handler); + let { handler } = getCurrentRouteWithAction(router, actionName); + assert(`[ember-route-action-helper] Unable to find action ${actionName}`, isPresent(handler)); }); let routeAction = function(...invocationArgs) { - let { action, handler } = getRouteWithAction(router, actionName); + let { action, handler } = getCurrentRouteWithAction(router, actionName); let args = params.concat(invocationArgs); return run.join(handler, action, ...args); }; diff --git a/config/ember-try.js b/config/ember-try.js index b6e351a..a070124 100644 --- a/config/ember-try.js +++ b/config/ember-try.js @@ -1,22 +1,6 @@ /* eslint-env node */ module.exports = { scenarios: [ - { - name: 'ember-1.13', - bower: { - dependencies: { - 'ember': '~1.13.0' - }, - resolutions: { - 'ember': '~1.13.0' - } - }, - npm: { - devDependencies: { - 'ember-source': null - } - } - }, { name: 'ember-lts-2.4', bower: { diff --git a/package.json b/package.json index f4d57cf..570d98b 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "scripts": { "build": "ember build", "start": "ember server", - "test": "ember try:each" + "test": "tsc && ember try:each" }, "repository": "https://github.com/DockYard/ember-route-action-helper", "engines": { @@ -21,6 +21,7 @@ ], "license": "MIT", "devDependencies": { + "@types/ember": "^2.7.41", "broccoli-asset-rev": "^2.4.5", "ember-ajax": "^3.0.0", "ember-cli": "2.13.0", @@ -40,7 +41,8 @@ "ember-load-initializers": "^1.0.0", "ember-resolver": "^4.0.0", "ember-source": "~2.13.0", - "loader.js": "^4.2.3" + "loader.js": "^4.2.3", + "typescript": "^2.3.2" }, "keywords": [ "ember-addon", diff --git a/tests/acceptance/main-test.js b/tests/acceptance/main-test.js index 8fc02a6..b1eb459 100644 --- a/tests/acceptance/main-test.js +++ b/tests/acceptance/main-test.js @@ -26,29 +26,44 @@ moduleForAcceptance('Acceptance | main', { } }); -test('it bubbles a route action', function(assert) { - visit('/thing'); +test('it has a return value', function(assert) { + visit('/math'); - andThen(() => assert.equal(currentURL(), '/thing')); - andThen(() => click('.foo-button')); - andThen(() => assert.equal(findWithAssert('.foo-value').text().trim(), 'Hello world Bob!')); + andThen(() => assert.equal(currentURL(), '/math')); + andThen(() => click('#math-1')); + andThen(() => assert.equal(findWithAssert('#math-value').text().trim(), '3')); + andThen(() => click('#math-2')); + andThen(() => assert.equal(findWithAssert('#math-value').text().trim(), '16')); + andThen(() => click('#math-3')); + andThen(() => assert.equal(findWithAssert('#math-value').text().trim(), '15')); + andThen(() => click('.confirm-value-button')); + andThen(() => assert.equal(findWithAssert('.confirm-value').text().trim(), 'My value is 25')); }); -test('it has a return value', function(assert) { - visit('/thing'); +test('it can be partially applied', function(assert) { + visit('/math'); - andThen(() => assert.equal(currentURL(), '/thing')); - andThen(() => click('.thing .max-button')); - andThen(() => assert.equal(findWithAssert('.thing .max-value').text().trim(), '20')); + andThen(() => click('.add-value-button')); + andThen(() => assert.equal(findWithAssert('.add-value').text().trim(), 'My value is 7')); +}); - // changing routes, 2 helpers invoked - andThen(() => visit('/thing/show')); - andThen(() => assert.equal(currentURL(), '/thing/show')); - andThen(() => click('.thing-show .max-button')); +test('it invokes action in the current route hierarchy', function(assert) { + visit('/math'); + andThen(() => click('#math-1')); + andThen(() => assert.equal(findWithAssert('#math-value').text().trim(), '3')); + visit('/math/add'); + andThen(() => click('#math-add-1')); + andThen(() => assert.equal(findWithAssert('#math-add-value').text().trim(), '[math/add] Value is: 3')); +}); - // ensure values are different - andThen(() => assert.equal(findWithAssert('.thing .max-value').text().trim(), '20')); - andThen(() => assert.equal(findWithAssert('.thing-show .max-value').text().trim(), '300')); +test('it handles .index routes', function(assert) { + visit('/hello'); + andThen(() => click('#hello-index-button')); + andThen(() => assert.equal(findWithAssert('#hello-index-value').text().trim(), 'Hello from hello.index')); + andThen(() => click('#hello-button-1')); + andThen(() => assert.equal(findWithAssert('#hello-value').text().trim(), '', 'should not fire because `hello.index` action takes precedence')); + andThen(() => click('#hello-button-2')); + andThen(() => assert.equal(findWithAssert('#hello-value').text().trim(), 'HELLO FROM HELLO')); }); test('it can be used without rewrapping with (action (route-action "foo"))', function() { @@ -68,12 +83,3 @@ skip('it should throw an error immediately if the route action is missing', func // }); // }); }); - -test('it invokes action in the current route hierarchy', function(assert) { - visit('/thing'); - click('.foo-button'); - andThen(() => assert.equal(findWithAssert('.foo-value').text().trim(), 'Hello world Bob!')); - visit('/thing/route-with-action'); - click('.foo-button'); - andThen(() => assert.equal(findWithAssert('.foo-value').text().trim(), 'Set via route-with-action: Hello world Bob!')); -}); diff --git a/tests/dummy/app/components/add-value.js b/tests/dummy/app/components/add-value.js new file mode 100644 index 0000000..1cc839c --- /dev/null +++ b/tests/dummy/app/components/add-value.js @@ -0,0 +1,13 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + baseValue: 4, + value: null, + actions: { + addValue(x) { + this.set('value', this.get('add')(x)); + } + } +}); diff --git a/tests/dummy/app/components/confirm-value.js b/tests/dummy/app/components/confirm-value.js new file mode 100644 index 0000000..69f3752 --- /dev/null +++ b/tests/dummy/app/components/confirm-value.js @@ -0,0 +1,12 @@ +import Ember from 'ember'; + +const { Component } = Ember; + +export default Component.extend({ + value: null, + actions: { + confirm() { + this.set('value', this.get('doAction')()); + } + } +}); diff --git a/tests/dummy/app/components/foo-bar.js b/tests/dummy/app/components/foo-bar.js deleted file mode 100644 index 3dfcdc6..0000000 --- a/tests/dummy/app/components/foo-bar.js +++ /dev/null @@ -1,13 +0,0 @@ -import Ember from 'ember'; - -const { Component, set } = Ember; - -export default Component.extend({ - actions: { - getMax(...numbers) { - // contrived example, but demonstrates that the closure action has a - // return value. - return set(this, 'max', this.attrs.getMax(...numbers)); - } - } -}); diff --git a/tests/dummy/app/router.js b/tests/dummy/app/router.js index 0ce6078..42e7da6 100644 --- a/tests/dummy/app/router.js +++ b/tests/dummy/app/router.js @@ -7,10 +7,12 @@ const Router = Ember.Router.extend({ }); Router.map(function() { - this.route('thing', function() { - this.route('show'); - this.route('route-with-action'); + this.route('math', function() { + this.route('add'); }); + this.route('hello', function() { + this.route('world'); + }) this.route('dynamic'); this.route('dynamic2'); }); diff --git a/tests/dummy/app/routes/application.js b/tests/dummy/app/routes/application.js deleted file mode 100644 index cf914fd..0000000 --- a/tests/dummy/app/routes/application.js +++ /dev/null @@ -1,15 +0,0 @@ -import Ember from 'ember'; - -const { Route, set } = Ember; - -export default Route.extend({ - actions: { - updateFoo(...args) { - return set(this, 'controller.foo', args.join(' ')); - }, - - getMax(...numbers) { - return Math.max.apply([], numbers); - } - } -}); diff --git a/tests/dummy/app/routes/hello.js b/tests/dummy/app/routes/hello.js new file mode 100644 index 0000000..fcfb35a --- /dev/null +++ b/tests/dummy/app/routes/hello.js @@ -0,0 +1,14 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + actions: { + greet() { + return this.set('controller.value', 'Hello from hello'); + }, + yell() { + return this.set('controller.value', 'HELLO FROM HELLO'); + } + } +}); diff --git a/tests/dummy/app/routes/hello/index.js b/tests/dummy/app/routes/hello/index.js new file mode 100644 index 0000000..253ea17 --- /dev/null +++ b/tests/dummy/app/routes/hello/index.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + actions: { + greet() { + return this.set('controller.value', 'Hello from hello.index'); + } + } +}); diff --git a/tests/dummy/app/routes/math.js b/tests/dummy/app/routes/math.js new file mode 100644 index 0000000..6ba8f28 --- /dev/null +++ b/tests/dummy/app/routes/math.js @@ -0,0 +1,17 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + actions: { + add(x, y) { + return this.set('controller.value', x + y); + }, + square(x) { + return this.set('controller.value', x * x); + }, + triple(x) { + return this.set('controller.value', x * 3); + } + } +}); diff --git a/tests/dummy/app/routes/math/add.js b/tests/dummy/app/routes/math/add.js new file mode 100644 index 0000000..5cdf45c --- /dev/null +++ b/tests/dummy/app/routes/math/add.js @@ -0,0 +1,11 @@ +import Ember from 'ember'; + +const { Route } = Ember; + +export default Route.extend({ + actions: { + add(x, y) { + return this.set('controller.value', `[math/add] Value is: ${x + y}`); + } + } +}); diff --git a/tests/dummy/app/routes/thing/route-with-action.js b/tests/dummy/app/routes/thing/route-with-action.js deleted file mode 100644 index 8bf8a56..0000000 --- a/tests/dummy/app/routes/thing/route-with-action.js +++ /dev/null @@ -1,12 +0,0 @@ -import Ember from 'ember'; - -const { Route, set } = Ember; - -export default Route.extend({ - actions: { - updateFoo(...args) { - let applicationController = this.controllerFor('application'); - return set(applicationController, 'foo', 'Set via route-with-action: ' + args.join(' ')); - } - } -}); diff --git a/tests/dummy/app/templates/application.hbs b/tests/dummy/app/templates/application.hbs deleted file mode 100644 index 1f93abb..0000000 --- a/tests/dummy/app/templates/application.hbs +++ /dev/null @@ -1,3 +0,0 @@ -

{{foo}}

- -{{outlet}} diff --git a/tests/dummy/app/templates/components/add-value.hbs b/tests/dummy/app/templates/components/add-value.hbs new file mode 100644 index 0000000..c1d1512 --- /dev/null +++ b/tests/dummy/app/templates/components/add-value.hbs @@ -0,0 +1,4 @@ + +
+ My value is {{value}} +
\ No newline at end of file diff --git a/tests/dummy/app/templates/components/confirm-value.hbs b/tests/dummy/app/templates/components/confirm-value.hbs new file mode 100644 index 0000000..c7d7610 --- /dev/null +++ b/tests/dummy/app/templates/components/confirm-value.hbs @@ -0,0 +1,4 @@ + +
+ My value is {{value}} +
\ No newline at end of file diff --git a/tests/dummy/app/templates/components/foo-bar.hbs b/tests/dummy/app/templates/components/foo-bar.hbs deleted file mode 100644 index e91d590..0000000 --- a/tests/dummy/app/templates/components/foo-bar.hbs +++ /dev/null @@ -1,6 +0,0 @@ -
{{max}}
- -{{#if clicked}} - -{{/if}} - \ No newline at end of file diff --git a/tests/dummy/app/templates/hello.hbs b/tests/dummy/app/templates/hello.hbs new file mode 100644 index 0000000..6fba619 --- /dev/null +++ b/tests/dummy/app/templates/hello.hbs @@ -0,0 +1,9 @@ + + +
+ {{value}} +
+ +
+ {{outlet}} +
\ No newline at end of file diff --git a/tests/dummy/app/templates/hello/index.hbs b/tests/dummy/app/templates/hello/index.hbs new file mode 100644 index 0000000..5feba27 --- /dev/null +++ b/tests/dummy/app/templates/hello/index.hbs @@ -0,0 +1,4 @@ + +
+ {{value}} +
\ No newline at end of file diff --git a/tests/dummy/app/templates/math.hbs b/tests/dummy/app/templates/math.hbs new file mode 100644 index 0000000..3161b7b --- /dev/null +++ b/tests/dummy/app/templates/math.hbs @@ -0,0 +1,10 @@ +
{{value}}
+ + + + + +{{confirm-value doAction=(route-action "square" 5)}} +{{add-value add=(route-action "add" 3)}} + +{{outlet}} \ No newline at end of file diff --git a/tests/dummy/app/templates/math/add.hbs b/tests/dummy/app/templates/math/add.hbs new file mode 100644 index 0000000..4a58178 --- /dev/null +++ b/tests/dummy/app/templates/math/add.hbs @@ -0,0 +1,3 @@ +
{{value}}
+ + \ No newline at end of file diff --git a/tests/dummy/app/templates/thing.hbs b/tests/dummy/app/templates/thing.hbs deleted file mode 100644 index ff8c96c..0000000 --- a/tests/dummy/app/templates/thing.hbs +++ /dev/null @@ -1,9 +0,0 @@ -
- {{foo-bar - class="thing" - clicked=(route-action "updateFoo" "Hello" "world") - getMax=(route-action "getMax" 1 5 10) - }} -
- -{{outlet}} \ No newline at end of file diff --git a/tests/dummy/app/templates/thing/show.hbs b/tests/dummy/app/templates/thing/show.hbs deleted file mode 100644 index 005d659..0000000 --- a/tests/dummy/app/templates/thing/show.hbs +++ /dev/null @@ -1,3 +0,0 @@ -

Thing / Show

- -{{foo-bar class="thing-show" getMax=(route-action "getMax" 100 200 300)}} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..24aa5b4 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "es2017", + "noEmit": true, + "allowJs": true, + "checkJs": true, + "types": [ + "ember" + ] + }, + "include": [ + "addon/**/*" + ] +} diff --git a/yarn.lock b/yarn.lock index efb8ca1..806a538 100644 --- a/yarn.lock +++ b/yarn.lock @@ -86,6 +86,21 @@ dependencies: "@glimmer/util" "^0.22.0" +"@types/ember@^2.7.41": + version "2.7.41" + resolved "https://registry.yarnpkg.com/@types/ember/-/ember-2.7.41.tgz#6155bbc82fc817dbcdd9a237b6319dfce6a16c36" + dependencies: + "@types/handlebars" "*" + "@types/jquery" "*" + +"@types/handlebars@*": + version "4.0.32" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.32.tgz#637e8d945a9354aab47df7125005490fe9f8e592" + +"@types/jquery@*": + version "2.0.43" + resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.43.tgz#e30d971d56dce22bf0d62e02bd4b28a7ffdba076" + abbrev@1: version "1.0.9" resolved "https://registry.yarnpkg.com/abbrev/-/abbrev-1.0.9.tgz#91b4792588a7738c25f35dd6f63752a2f8776135" @@ -1087,7 +1102,7 @@ broccoli-config-replace@^1.1.2: debug "^2.2.0" fs-extra "^0.24.0" -broccoli-filter@^1.0.1, broccoli-filter@^1.2.2, broccoli-filter@^1.2.3: +broccoli-filter@^1.2.2, broccoli-filter@^1.2.3: version "1.2.4" resolved "https://registry.yarnpkg.com/broccoli-filter/-/broccoli-filter-1.2.4.tgz#409afb94b9a3a6da9fac8134e91e205f40cc7330" dependencies: @@ -1759,7 +1774,7 @@ ember-cli-app-version@^2.0.0: ember-cli-htmlbars "^1.0.0" git-repo-version "0.4.1" -ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6, ember-cli-babel@^5.1.7: +ember-cli-babel@^5.1.5, ember-cli-babel@^5.1.6: version "5.2.1" resolved "https://registry.yarnpkg.com/ember-cli-babel/-/ember-cli-babel-5.2.1.tgz#14a1a7b3ae9e9f1284f7bcdb142eb53bd0b1b5bd" dependencies: @@ -2085,13 +2100,6 @@ ember-load-initializers@^1.0.0: dependencies: ember-cli-babel "^6.0.0-beta.7" -ember-qunit-assert-helpers@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/ember-qunit-assert-helpers/-/ember-qunit-assert-helpers-0.1.3.tgz#6ba2acf63a3c45c6f6764bc1b5cffd42942df678" - dependencies: - broccoli-filter "^1.0.1" - ember-cli-babel "^5.1.7" - ember-qunit@^2.0.0-beta.1: version "2.1.2" resolved "https://registry.yarnpkg.com/ember-qunit/-/ember-qunit-2.1.2.tgz#ede8e56f098206c1d0834a592acefa0c47dc9ad7" @@ -4826,6 +4834,10 @@ typedarray@~0.0.5: version "0.0.6" resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" +typescript@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.3.2.tgz#f0f045e196f69a72f06b25fd3bd39d01c3ce9984" + uc.micro@^1.0.0, uc.micro@^1.0.1, uc.micro@^1.0.3: version "1.0.3" resolved "https://registry.yarnpkg.com/uc.micro/-/uc.micro-1.0.3.tgz#7ed50d5e0f9a9fb0a573379259f2a77458d50192"