├── tests ├── unit │ ├── .gitkeep │ └── utils │ │ └── event-listener-test.js ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ └── .gitkeep │ │ ├── templates │ │ │ ├── components │ │ │ │ └── .gitkeep │ │ │ └── application.hbs │ │ ├── resolver.js │ │ ├── router.js │ │ ├── app.js │ │ └── index.html │ ├── config │ │ ├── optional-features.json │ │ ├── .eslintrc.js │ │ ├── targets.js │ │ └── environment.js │ └── public │ │ └── robots.txt ├── .eslintrc.js ├── test-helper.js ├── index.html └── integration │ └── helpers │ ├── on-window-test.js │ ├── on-document-test.js │ └── on-test.js ├── .watchmanconfig ├── app ├── helpers │ ├── on.js │ ├── on-document.js │ └── on-window.js └── .eslintrc.js ├── .eslintrc.js ├── .template-lintrc.js ├── index.js ├── addon ├── .eslintrc.js ├── helpers │ ├── on-window.js │ ├── on-document.js │ └── on.js └── utils │ └── event-listener.js ├── config ├── environment.js └── ember-try.js ├── .prettierrc ├── .ember-cli ├── .eslintignore ├── ember-cli-build.js ├── .dependabot └── config.yml ├── .editorconfig ├── .gitignore ├── .npmignore ├── CONTRIBUTING.md ├── appveyor.yml ├── testem.js ├── LICENSE.md ├── .travis.yml ├── package.json └── README.md /tests/unit/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /app/helpers/on.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-on-helper/helpers/on'; 2 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "jquery-integration": false 3 | } 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@clark/node' 4 | }; 5 | -------------------------------------------------------------------------------- /app/helpers/on-document.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-on-helper/helpers/on-document'; 2 | -------------------------------------------------------------------------------- /app/helpers/on-window.js: -------------------------------------------------------------------------------- 1 | export { default } from 'ember-on-helper/helpers/on-window'; 2 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | 3 | {{outlet}} -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'recommended' 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name 5 | }; 6 | -------------------------------------------------------------------------------- /tests/dummy/app/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember-resolver'; 2 | 3 | export default Resolver; 4 | -------------------------------------------------------------------------------- /tests/dummy/config/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | extends: '@clark/node' 4 | }; 5 | -------------------------------------------------------------------------------- /addon/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | extends: '@clark/ember' 6 | }; 7 | -------------------------------------------------------------------------------- /app/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | extends: '@clark/ember' 6 | }; 7 | -------------------------------------------------------------------------------- /tests/.eslintrc.js: -------------------------------------------------------------------------------- 1 | /* eslint-env node */ 2 | 3 | module.exports = { 4 | root: true, 5 | extends: '@clark/ember' 6 | }; 7 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return {}; 5 | }; 6 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "overrides": [ 4 | { 5 | "files": "*.hbs", 6 | "options": { 7 | "singleQuote": false 8 | } 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /addon/helpers/on-window.js: -------------------------------------------------------------------------------- 1 | import OnHelper from './on'; 2 | 3 | export default OnHelper.extend({ 4 | compute(positional, named) { 5 | return this._super([window, ...positional], named); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /addon/helpers/on-document.js: -------------------------------------------------------------------------------- 1 | import OnHelper from './on'; 2 | 3 | export default OnHelper.extend({ 4 | compute(positional, named) { 5 | return this._super([document, ...positional], named); 6 | } 7 | }); 8 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | const Router = EmberRouter.extend({ 5 | location: config.locationType, 6 | rootURL: config.rootURL 7 | }); 8 | 9 | Router.map(function() {}); 10 | 11 | export default Router; 12 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | # unconventional js 2 | /blueprints/*/files/ 3 | /vendor/ 4 | 5 | # compiled output 6 | /dist/ 7 | /tmp/ 8 | 9 | # dependencies 10 | /bower_components/ 11 | /node_modules/ 12 | 13 | # misc 14 | /coverage/ 15 | !.* 16 | 17 | # ember-try 18 | /.node_modules.ember-try/ 19 | /bower.json.ember-try 20 | /package.json.ember-try 21 | -------------------------------------------------------------------------------- /ember-cli-build.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 4 | 5 | module.exports = function(defaults) { 6 | const app = new EmberAddon(defaults, { 7 | 'ember-cli-babel': { 8 | includePolyfill: Boolean(process.env.IE) 9 | } 10 | }); 11 | 12 | app.import({ test: 'vendor/ember/ember-template-compiler.js' }); 13 | 14 | return app.toTree(); 15 | }; 16 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | version: 1 2 | update_configs: 3 | - package_manager: 'javascript' 4 | directory: '/' 5 | update_schedule: 'live' 6 | version_requirement_updates: increase_versions 7 | automerged_updates: 8 | - match: 9 | dependency_type: 'development' 10 | update_type: 'all' 11 | - match: 12 | dependency_type: 'production' 13 | update_type: 'all' 14 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from './resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | const App = Application.extend({ 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix, 9 | Resolver 10 | }); 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | 14 | export default App; 15 | -------------------------------------------------------------------------------- /tests/dummy/config/targets.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const browsers = [ 4 | 'last 1 Chrome versions', 5 | 'last 1 Firefox versions', 6 | 'last 1 Safari versions' 7 | ]; 8 | 9 | const isIE = !!process.env.IE; 10 | const isCI = !!process.env.CI; 11 | const isProduction = process.env.EMBER_ENV === 'production'; 12 | 13 | if (isIE || isCI || isProduction) { 14 | browsers.push('ie 11'); 15 | } 16 | 17 | module.exports = { 18 | browsers 19 | }; 20 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.hbs] 17 | insert_final_newline = false 18 | 19 | [*.{diff,md}] 20 | trim_trailing_whitespace = false 21 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist/ 5 | /tmp/ 6 | 7 | # dependencies 8 | /bower_components/ 9 | /node_modules/ 10 | 11 | # misc 12 | /.env* 13 | /.pnp* 14 | /.sass-cache 15 | /connect.lock 16 | /coverage/ 17 | /libpeerconnection.log 18 | /npm-debug.log* 19 | /testem.log 20 | /yarn-error.log 21 | 22 | # ember-try 23 | /.node_modules.ember-try/ 24 | /bower.json.ember-try 25 | /package.json.ember-try 26 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | # compiled output 2 | /dist/ 3 | /tmp/ 4 | 5 | # dependencies 6 | /bower_components/ 7 | 8 | # misc 9 | /.bowerrc 10 | /.editorconfig 11 | /.ember-cli 12 | /.env* 13 | /.eslintignore 14 | /.eslintrc.js 15 | /.gitignore 16 | /.template-lintrc.js 17 | /.travis.yml 18 | /.watchmanconfig 19 | /bower.json 20 | /config/ember-try.js 21 | /CONTRIBUTING.md 22 | /ember-cli-build.js 23 | /testem.js 24 | /tests/ 25 | /yarn.lock 26 | .gitkeep 27 | 28 | # ember-try 29 | /.node_modules.ember-try/ 30 | /bower.json.ember-try 31 | /package.json.ember-try 32 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-on-helper` 7 | * `yarn install` 8 | 9 | ## Linting 10 | 11 | * `yarn lint:hbs` 12 | * `yarn lint:js` 13 | * `yarn lint:js --fix` 14 | 15 | ## Running tests 16 | 17 | * `ember test` – Runs the test suite on the current Ember version 18 | * `ember test --server` – Runs the test suite in "watch mode" 19 | * `ember try:each` – Runs the test suite against multiple Ember versions 20 | 21 | ## Running the dummy application 22 | 23 | * `ember serve` 24 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 25 | 26 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | # http://www.appveyor.com/docs/appveyor-yml 2 | 3 | image: Visual Studio 2017 4 | 5 | environment: 6 | CI: true 7 | IE: true 8 | 9 | cache: 10 | - '%LOCALAPPDATA%\Yarn' 11 | 12 | # Install scripts. (runs after repo cloning) 13 | install: 14 | # Get the latest stable version of nodejs 15 | - ps: Install-Product node 8 16 | - npm version 17 | - npm config set spin false 18 | - appveyor-retry npm install -g yarn 19 | - yarn --version 20 | - appveyor-retry yarn install --frozen-lockfile --non-interactive 21 | 22 | # Post-install test scripts. 23 | test_script: 24 | - cmd: yarn test 25 | 26 | # Don't actually build. 27 | build: off 28 | 29 | # Set build version format here instead of in the admin panel. 30 | version: "{build}" 31 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: ['Chrome', process.env.IE && 'IE'].filter(Boolean), 5 | // eslint-disable-next-line unicorn/prevent-abbreviations 6 | launch_in_dev: ['Chrome'], 7 | browser_args: { 8 | Chrome: { 9 | ci: [ 10 | // --no-sandbox is needed when running Chrome inside a container 11 | process.env.CI ? '--no-sandbox' : null, 12 | '--headless', 13 | '--disable-gpu', 14 | '--disable-dev-shm-usage', 15 | '--disable-software-rasterizer', 16 | '--mute-audio', 17 | '--remote-debugging-port=0', 18 | '--window-size=1440,900' 19 | ].filter(Boolean) 20 | } 21 | } 22 | }; 23 | -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | 12 | 13 | 14 | 15 | {{content-for "head-footer"}} 16 | 17 | 18 | {{content-for "body"}} 19 | 20 | 21 | 22 | 23 | {{content-for "body-footer"}} 24 | 25 | 26 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import Application from '../app'; 2 | import config from '../config/environment'; 3 | import { setApplication } from '@ember/test-helpers'; 4 | import { start } from 'ember-qunit'; 5 | import QUnit from 'qunit'; 6 | import { __counts } from 'ember-on-helper/helpers/on'; 7 | 8 | QUnit.testStart(() => { 9 | QUnit.config.current.testEnvironment._startingCounts = __counts(); 10 | }); 11 | 12 | QUnit.assert.counts = function( 13 | expected, 14 | message = `counters have incremented by ${JSON.stringify(expected)}` 15 | ) { 16 | const current = __counts(); 17 | 18 | this.deepEqual( 19 | current, 20 | { 21 | adds: 22 | expected.adds + 23 | QUnit.config.current.testEnvironment._startingCounts.adds, 24 | removes: 25 | expected.removes + 26 | QUnit.config.current.testEnvironment._startingCounts.removes 27 | }, 28 | message 29 | ); 30 | }; 31 | 32 | setApplication(Application.create(config.APP)); 33 | 34 | start(); 35 | -------------------------------------------------------------------------------- /tests/unit/utils/event-listener-test.js: -------------------------------------------------------------------------------- 1 | import { SUPPORTS_EVENT_OPTIONS } from 'ember-on-helper/utils/event-listener'; 2 | import { module, test } from 'qunit'; 3 | 4 | module('Unit | Utility | event-listener', function() { 5 | test('SUPPORTS_EVENT_OPTIONS has the right value', function(assert) { 6 | const { userAgent } = navigator; 7 | if (userAgent.includes('Chrome/')) { 8 | assert.ok( 9 | SUPPORTS_EVENT_OPTIONS, 10 | 'Google Chrome has support for event options' 11 | ); 12 | } else if (userAgent.includes('Firefox/')) { 13 | assert.ok( 14 | SUPPORTS_EVENT_OPTIONS, 15 | 'Firefox has support for event options' 16 | ); 17 | } else if (userAgent.includes('Trident/')) { 18 | assert.notOk( 19 | SUPPORTS_EVENT_OPTIONS, 20 | 'Internet Explorer 11 has no support for event options' 21 | ); 22 | } else { 23 | throw new Error(`Could not detect browser from: ${userAgent}`); 24 | } 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2019 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for "head"}} 11 | {{content-for "test-head"}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for "head-footer"}} 18 | {{content-for "test-head-footer"}} 19 | 20 | 21 | {{content-for "body"}} 22 | {{content-for "test-body"}} 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for "body-footer"}} 31 | {{content-for "test-body-footer"}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/integration/helpers/on-window-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click, resetOnerror } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Helper | on-window', function(hooks) { 7 | setupRenderingTest(hooks); 8 | hooks.afterEach(() => resetOnerror()); 9 | 10 | test('it basically works', async function(assert) { 11 | assert.expect(6); 12 | 13 | this.someMethod = function(event) { 14 | assert.ok(this instanceof Window, 'this context is the window'); 15 | assert.ok( 16 | event instanceof MouseEvent, 17 | 'first argument is a `MouseEvent`' 18 | ); 19 | assert.strictEqual( 20 | event.target.tagName, 21 | 'BUTTON', 22 | 'correct element tagName' 23 | ); 24 | assert.dom(event.target).hasAttribute('data-foo', 'test-element'); 25 | }; 26 | 27 | await render(hbs` 28 | {{on-window "click" this.someMethod}} 29 | 30 | `); 31 | 32 | assert.counts({ adds: 1, removes: 0 }); 33 | 34 | await click('button'); 35 | 36 | assert.counts({ adds: 1, removes: 0 }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/integration/helpers/on-document-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, click, resetOnerror } from '@ember/test-helpers'; 4 | import hbs from 'htmlbars-inline-precompile'; 5 | 6 | module('Integration | Helper | on-document', function(hooks) { 7 | setupRenderingTest(hooks); 8 | hooks.afterEach(() => resetOnerror()); 9 | 10 | test('it basically works', async function(assert) { 11 | assert.expect(6); 12 | 13 | this.someMethod = function(event) { 14 | assert.ok(this instanceof HTMLDocument, 'this context is the document'); 15 | assert.ok( 16 | event instanceof MouseEvent, 17 | 'first argument is a `MouseEvent`' 18 | ); 19 | assert.strictEqual( 20 | event.target.tagName, 21 | 'BUTTON', 22 | 'correct element tagName' 23 | ); 24 | assert.dom(event.target).hasAttribute('data-foo', 'test-element'); 25 | }; 26 | 27 | await render(hbs` 28 | {{on-document "click" this.someMethod}} 29 | 30 | `); 31 | 32 | assert.counts({ adds: 1, removes: 0 }); 33 | 34 | await click('button'); 35 | 36 | assert.counts({ adds: 1, removes: 0 }); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | const ENV = { 5 | modulePrefix: 'dummy', 6 | environment, 7 | rootURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | FEATURES: { 11 | // Here you can enable experimental features on an ember canary build 12 | // e.g. 'with-controller': true 13 | }, 14 | EXTEND_PROTOTYPES: { 15 | // Prevent Ember Data from overriding Date.parse. 16 | Date: false 17 | } 18 | }, 19 | 20 | APP: { 21 | // Here you can pass flags/options to your application instance 22 | // when it is created 23 | }, 24 | 25 | EMBER_EVENT_HELPERS_INSTALLED: Boolean( 26 | process.env.EMBER_EVENT_HELPERS_INSTALLED 27 | ) 28 | }; 29 | 30 | if (environment === 'development') { 31 | // ENV.APP.LOG_RESOLVER = true; 32 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 33 | // ENV.APP.LOG_TRANSITIONS = true; 34 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 35 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 36 | } 37 | 38 | if (environment === 'test') { 39 | // Testem prefers this... 40 | ENV.locationType = 'none'; 41 | 42 | // keep test console output quieter 43 | ENV.APP.LOG_ACTIVE_GENERATION = false; 44 | ENV.APP.LOG_VIEW_LOOKUPS = false; 45 | 46 | ENV.APP.rootElement = '#ember-testing'; 47 | ENV.APP.autoboot = false; 48 | } 49 | 50 | if (environment === 'production') { 51 | // here you can enable a production-specific feature 52 | } 53 | 54 | return ENV; 55 | }; 56 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | # we recommend testing addons with the same minimum supported node version as Ember CLI 5 | # so that your addon works for all apps 6 | - "8" 7 | 8 | sudo: false 9 | dist: trusty 10 | 11 | addons: 12 | chrome: stable 13 | 14 | cache: 15 | yarn: true 16 | 17 | env: 18 | global: 19 | # See https://git.io/vdao3 for details. 20 | - JOBS=1 21 | 22 | branches: 23 | only: 24 | - master 25 | # npm version tags 26 | - /^v\d+\.\d+\.\d+/ 27 | 28 | jobs: 29 | fail_fast: true 30 | allow_failures: 31 | - env: EMBER_TRY_SCENARIO=ember-canary 32 | 33 | include: 34 | # runs linting and tests with current locked deps 35 | 36 | - stage: "Tests" 37 | name: "Tests" 38 | install: 39 | - yarn install --non-interactive 40 | script: 41 | - yarn lint:hbs 42 | - yarn lint:js 43 | - yarn test 44 | 45 | - name: "Floating Dependencies" 46 | script: 47 | - yarn test 48 | 49 | # we recommend new addons test the current and previous LTS 50 | # as well as latest stable release (bonus points to beta/canary) 51 | - stage: "Additional Tests" 52 | env: EMBER_TRY_SCENARIO=ember-2.13 53 | - env: EMBER_TRY_SCENARIO=ember-lts-2.18 54 | - env: EMBER_TRY_SCENARIO=ember-lts-3.4 55 | - env: EMBER_TRY_SCENARIO=ember-release 56 | - env: EMBER_TRY_SCENARIO=ember-beta 57 | - env: EMBER_TRY_SCENARIO=ember-canary 58 | - env: EMBER_TRY_SCENARIO=ember-default-with-jquery 59 | 60 | before_install: 61 | - curl -o- -L https://yarnpkg.com/install.sh | bash 62 | - export PATH=$HOME/.yarn/bin:$PATH 63 | 64 | install: 65 | - yarn install --no-lockfile --non-interactive 66 | 67 | script: 68 | - node_modules/.bin/ember try:one $EMBER_TRY_SCENARIO 69 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-on-helper", 3 | "version": "0.1.0", 4 | "description": "{{on eventTarget eventName someAction}} template helper", 5 | "keywords": [ 6 | "ember-addon", 7 | "ember-helper", 8 | "ember-template-helper", 9 | "on", 10 | "event", 11 | "listener", 12 | "addEventListener" 13 | ], 14 | "repository": "buschtoens/ember-on-helper", 15 | "license": "MIT", 16 | "author": "Jan Buschtöns (https://jan.buschtoens.me)", 17 | "directories": { 18 | "doc": "doc", 19 | "test": "tests" 20 | }, 21 | "scripts": { 22 | "build": "ember build", 23 | "lint:hbs": "ember-template-lint .", 24 | "lint:js": "eslint .", 25 | "start": "ember serve", 26 | "test": "ember test", 27 | "test:all": "ember try:each" 28 | }, 29 | "dependencies": { 30 | "ember-cli-babel": "^7.13.2" 31 | }, 32 | "devDependencies": { 33 | "@clark/eslint-config-ember": "^1.6.0", 34 | "@clark/eslint-config-node": "^1.18.0", 35 | "@ember/optional-features": "^1.3.0", 36 | "broccoli-asset-rev": "^3.0.0", 37 | "ember-cli": "~3.15.1", 38 | "ember-cli-dependency-checker": "^3.2.0", 39 | "ember-cli-htmlbars": "^4.2.2", 40 | "ember-cli-htmlbars-inline-precompile": "^3.0.1", 41 | "ember-cli-inject-live-reload": "^2.0.2", 42 | "ember-cli-sri": "^2.1.1", 43 | "ember-cli-uglify": "^3.0.0", 44 | "ember-compatibility-helpers": "^1.2.0", 45 | "ember-disable-prototype-extensions": "^1.1.3", 46 | "ember-export-application-global": "^2.0.1", 47 | "ember-load-initializers": "^2.1.1", 48 | "ember-maybe-import-regenerator": "^0.1.6", 49 | "ember-qunit": "^4.6.0", 50 | "ember-qunit-assert-helpers": "^0.2.2", 51 | "ember-resolver": "^7.0.0", 52 | "ember-source": "~3.15.0", 53 | "ember-source-channel-url": "^2.0.1", 54 | "ember-template-lint": "^1.13.0", 55 | "ember-try": "^1.4.0", 56 | "eslint": "^6.8.0", 57 | "loader.js": "^4.7.0", 58 | "qunit-dom": "^0.9.2" 59 | }, 60 | "engines": { 61 | "node": "8.* || >= 10.*" 62 | }, 63 | "ember-addon": { 64 | "configPath": "tests/dummy/config" 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = function() { 6 | return Promise.all([ 7 | getChannelURL('release'), 8 | getChannelURL('beta'), 9 | getChannelURL('canary') 10 | ]).then(urls => { 11 | return { 12 | useYarn: true, 13 | scenarios: [ 14 | { 15 | name: 'ember-2.13', 16 | env: { 17 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 18 | 'jquery-integration': true 19 | }) 20 | }, 21 | npm: { 22 | devDependencies: { 23 | '@ember/jquery': '^0.5.1', 24 | 'ember-source': '~2.13.0' 25 | } 26 | } 27 | }, 28 | { 29 | name: 'ember-lts-2.18', 30 | env: { 31 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 32 | 'jquery-integration': true 33 | }) 34 | }, 35 | npm: { 36 | devDependencies: { 37 | '@ember/jquery': '^0.5.1', 38 | 'ember-source': '~2.18.0' 39 | } 40 | } 41 | }, 42 | { 43 | name: 'ember-lts-3.4', 44 | npm: { 45 | devDependencies: { 46 | 'ember-source': '~3.4.0' 47 | } 48 | } 49 | }, 50 | { 51 | name: 'ember-release', 52 | npm: { 53 | devDependencies: { 54 | 'ember-source': urls[0] 55 | } 56 | } 57 | }, 58 | { 59 | name: 'ember-beta', 60 | npm: { 61 | devDependencies: { 62 | 'ember-source': urls[1] 63 | } 64 | } 65 | }, 66 | { 67 | name: 'ember-canary', 68 | npm: { 69 | devDependencies: { 70 | 'ember-source': urls[2] 71 | } 72 | } 73 | }, 74 | // The default `.travis.yml` runs this scenario via `yarn test`, 75 | // not via `ember try`. It's still included here so that running 76 | // `ember try:each` manually or from a customized CI config will run it 77 | // along with all the other scenarios. 78 | { 79 | name: 'ember-default', 80 | npm: { 81 | devDependencies: {} 82 | } 83 | }, 84 | { 85 | name: 'ember-default-with-jquery', 86 | env: { 87 | EMBER_OPTIONAL_FEATURES: JSON.stringify({ 88 | 'jquery-integration': true 89 | }) 90 | }, 91 | npm: { 92 | devDependencies: { 93 | '@ember/jquery': '^0.5.1' 94 | } 95 | } 96 | } 97 | ] 98 | }; 99 | }); 100 | }; 101 | -------------------------------------------------------------------------------- /addon/helpers/on.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: "off" */ 2 | 3 | import Helper from '@ember/component/helper'; 4 | import { addEventListener, removeEventListener } from '../utils/event-listener'; 5 | import { assert } from '@ember/debug'; 6 | import { DEBUG } from '@glimmer/env'; 7 | 8 | /** 9 | * These are private API and only used for testing instrumentation. 10 | */ 11 | let adds = 0; 12 | let removes = 0; 13 | export function __counts() { 14 | return { adds, removes }; 15 | } 16 | 17 | const assertValidEventOptions = 18 | DEBUG && 19 | (() => { 20 | const ALLOWED_EVENT_OPTIONS = ['capture', 'once', 'passive']; 21 | const joinOptions = options => options.map(o => `'${o}'`).join(', '); 22 | 23 | return function(eventOptions, eventName) { 24 | const invalidOptions = Object.keys(eventOptions).filter( 25 | o => !ALLOWED_EVENT_OPTIONS.includes(o) 26 | ); 27 | 28 | assert( 29 | `ember-on-helper: Provided invalid event options (${joinOptions( 30 | invalidOptions 31 | )}) to '${eventName}' event listener. Only these options are valid: ${joinOptions( 32 | ALLOWED_EVENT_OPTIONS 33 | )}`, 34 | invalidOptions.length === 0 35 | ); 36 | }; 37 | })(); 38 | 39 | function setupListener(eventTarget, eventName, callback, eventOptions) { 40 | if (DEBUG) assertValidEventOptions(eventOptions, eventName); 41 | assert( 42 | `ember-on-helper: '${eventTarget}' is not a valid event target. It has to be an Element or an object that conforms to the EventTarget interface.`, 43 | eventTarget && 44 | typeof eventTarget.addEventListener === 'function' && 45 | typeof eventTarget.removeEventListener === 'function' 46 | ); 47 | assert( 48 | `ember-on-helper: '${eventName}' is not a valid event name. It has to be a string with a minimum length of 1 character.`, 49 | typeof eventName === 'string' && eventName.length > 1 50 | ); 51 | assert( 52 | `ember-on-helper: '${callback}' is not a valid callback. Provide a function.`, 53 | typeof callback === 'function' 54 | ); 55 | 56 | adds++; 57 | addEventListener(eventTarget, eventName, callback, eventOptions); 58 | 59 | return callback; 60 | } 61 | 62 | function destroyListener(eventTarget, eventName, callback, eventOptions) { 63 | if (eventTarget && eventName && callback) { 64 | removes++; 65 | removeEventListener(eventTarget, eventName, callback, eventOptions); 66 | } 67 | } 68 | 69 | export default Helper.extend({ 70 | eventTarget: null, 71 | eventName: undefined, 72 | callback: undefined, 73 | eventOptions: undefined, 74 | 75 | compute([eventTarget, eventName, callback], eventOptions) { 76 | destroyListener( 77 | this.eventTarget, 78 | this.eventName, 79 | this.callback, 80 | this.eventOptions 81 | ); 82 | 83 | this.eventTarget = eventTarget; 84 | 85 | this.callback = setupListener( 86 | this.eventTarget, 87 | eventName, 88 | callback, 89 | eventOptions 90 | ); 91 | 92 | this.eventName = eventName; 93 | this.eventOptions = eventOptions; 94 | }, 95 | 96 | willDestroy() { 97 | this._super(); 98 | 99 | destroyListener( 100 | this.eventTarget, 101 | this.eventName, 102 | this.callback, 103 | this.eventOptions 104 | ); 105 | } 106 | }); 107 | -------------------------------------------------------------------------------- /addon/utils/event-listener.js: -------------------------------------------------------------------------------- 1 | /* eslint no-param-reassign: "off" */ 2 | 3 | import { assert } from '@ember/debug'; 4 | import { DEBUG } from '@glimmer/env'; 5 | 6 | /** 7 | * Internet Explorer 11 does not support `once` and also does not support 8 | * passing `eventOptions`. In some situations it then throws a weird script 9 | * error, like: 10 | * 11 | * ``` 12 | * Could not complete the operation due to error 80020101 13 | * ``` 14 | * 15 | * This flag determines, whether `{ once: true }` and thus also event options in 16 | * general are supported. 17 | */ 18 | export const SUPPORTS_EVENT_OPTIONS = (() => { 19 | try { 20 | const div = document.createElement('div'); 21 | let counter = 0; 22 | div.addEventListener('click', () => counter++, { once: true }); 23 | 24 | let event; 25 | if (typeof Event === 'function') { 26 | event = new Event('click'); 27 | } else { 28 | event = document.createEvent('Event'); 29 | event.initEvent('click', true, true); 30 | } 31 | 32 | div.dispatchEvent(event); 33 | div.dispatchEvent(event); 34 | 35 | return counter === 1; 36 | } catch (error) { 37 | return false; 38 | } 39 | })(); 40 | 41 | /** 42 | * Registers an event for an `element` that is called exactly once and then 43 | * unregistered again. This is effectively a polyfill for `{ once: true }`. 44 | * 45 | * It also accepts a fourth optional argument `useCapture`, that will be passed 46 | * through to `addEventListener`. 47 | * 48 | * @param {Element} element 49 | * @param {string} eventName 50 | * @param {Function} callback 51 | * @param {boolean} [useCapture=false] 52 | */ 53 | export function addEventListenerOnce( 54 | element, 55 | eventName, 56 | callback, 57 | useCapture = false 58 | ) { 59 | function listener() { 60 | element.removeEventListener(eventName, listener, useCapture); 61 | callback(); 62 | } 63 | element.addEventListener(eventName, listener, useCapture); 64 | } 65 | 66 | /** 67 | * Safely invokes `addEventListener` for IE11 and also polyfills the 68 | * `{ once: true }` and `{ capture: true }` options. 69 | * 70 | * All other options are discarded for IE11. Currently this is only `passive`. 71 | * 72 | * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 73 | * 74 | * @param {Element} element 75 | * @param {string} eventName 76 | * @param {Function} callback 77 | * @param {object} [eventOptions] 78 | */ 79 | export function addEventListener(element, eventName, callback, eventOptions) { 80 | const _callback = 81 | DEBUG && eventOptions && eventOptions.passive 82 | ? function(event) { 83 | event.preventDefault = () => { 84 | assert( 85 | `ember-on-helper: You marked this listener as 'passive', meaning that you must not call 'event.preventDefault()'.` 86 | ); 87 | }; 88 | return callback.call(this, event); 89 | } 90 | : callback; 91 | 92 | if (SUPPORTS_EVENT_OPTIONS) { 93 | element.addEventListener(eventName, _callback, eventOptions); 94 | } else if (eventOptions && eventOptions.once) { 95 | addEventListenerOnce( 96 | element, 97 | eventName, 98 | _callback, 99 | Boolean(eventOptions.capture) 100 | ); 101 | } else { 102 | element.addEventListener( 103 | eventName, 104 | _callback, 105 | Boolean(eventOptions && eventOptions.capture) 106 | ); 107 | } 108 | } 109 | 110 | /** 111 | * Since the same `capture` event option that was used to add the event listener 112 | * needs to be used when removing the listener, it needs to be polyfilled as 113 | * `useCapture` for IE11. 114 | * 115 | * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/removeEventListener 116 | * 117 | * @param {Element} element 118 | * @param {string} eventName 119 | * @param {Function} callback 120 | * @param {object} [eventOptions] 121 | */ 122 | export function removeEventListener( 123 | element, 124 | eventName, 125 | callback, 126 | eventOptions 127 | ) { 128 | if (SUPPORTS_EVENT_OPTIONS) { 129 | element.removeEventListener(eventName, callback, eventOptions); 130 | } else { 131 | element.removeEventListener( 132 | eventName, 133 | callback, 134 | Boolean(eventOptions && eventOptions.capture) 135 | ); 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ember-on-helper 2 | 3 | [![Build Status](https://travis-ci.org/buschtoens/ember-on-helper.svg)](https://travis-ci.org/buschtoens/ember-on-helper) 4 | [![npm version](https://badge.fury.io/js/ember-on-helper.svg)](http://badge.fury.io/js/ember-on-helper) 5 | [![Download Total](https://img.shields.io/npm/dt/ember-on-helper.svg)](http://badge.fury.io/js/ember-on-helper) 6 | [![Ember Observer Score](https://emberobserver.com/badges/ember-on-helper.svg)](https://emberobserver.com/addons/ember-on-helper) 7 | [![Ember Versions](https://img.shields.io/badge/Ember.js%20Versions-%5E2.18%20%7C%7C%20%5E3.0-brightgreen.svg)](https://travis-ci.org/buschtoens/ember-on-helper) 8 | [![ember-cli Versions](https://img.shields.io/badge/ember--cli%20Versions-%5E2.13%20%7C%7C%20%5E3.0-brightgreen.svg)](https://travis-ci.org/buschtoens/ember-on-helper) 9 | [![code style: prettier](https://img.shields.io/badge/code_style-prettier-ff69b4.svg)](https://github.com/prettier/prettier) 10 | [![dependencies](https://img.shields.io/david/buschtoens/ember-on-helper.svg)](https://david-dm.org/buschtoens/ember-on-helper) 11 | [![devDependencies](https://img.shields.io/david/dev/buschtoens/ember-on-helper.svg)](https://david-dm.org/buschtoens/ember-on-helper) 12 | 13 | An `{{on}}` template helper complimentary to the 14 | [RFC #471 "`{{on}}` modifier"](https://github.com/emberjs/rfcs/blob/master/text/0471-on-modifier.md). 15 | 16 | ## Installation 17 | 18 | ``` 19 | ember install ember-on-helper 20 | ``` 21 | 22 | #### Compatibility 23 | 24 | - Ember.js v2.18 or above 25 | - ember-cli v2.13 or above 26 | 27 | ## But why? 28 | 29 | You would use the `{{on}}` _modifier_ to register event listeners on elements 30 | that are in the realm of your current template. But sometimes you need to 31 | register event listeners on elements or even on generic `EventTarget`s that are 32 | outside of the control of your template, e.g. `document` or `window`. 33 | 34 | > ⚠️👉 **WARNING:** Do not overuse this helper. If you want to bind to an 35 | > element that _is_ controlled by Glimmer, but maybe just not by the current 36 | > template, _do not_ reach for a manual `document.querySelector()`. Instead, 37 | > think about your current template and state setup and try to use a true "Data 38 | > Down, Actions Up" pattern or use a shared `Service` as a message bus. 39 | 40 | ## Usage 41 | 42 | Pretty much exactly the same as the `{{on}}` modifier, except for that the 43 | `{{on}}` helper expects one more positional parameter upfront: the `evenTarget`. 44 | 45 | ```hbs 46 | {{on eventTarget eventName eventListener}} 47 | ``` 48 | 49 | As with the `{{on}}` modifier, you can also pass optional event options as named 50 | parameters: 51 | 52 | ```hbs 53 | {{on eventTarget eventName eventListener capture=bool once=bool passive=bool}} 54 | ``` 55 | 56 | ### Simple Example 57 | 58 | ```hbs 59 | Click anywhere in the browser window, fam. 60 | 61 | {{on this.document "click" this.onDocumentClick}} 62 | ``` 63 | 64 | ```ts 65 | import Component from '@glimmer/component'; 66 | import { action } from '@ember/object'; 67 | 68 | export default class TomstersWitnessComponent extends Component { 69 | document = document; 70 | 71 | @action 72 | onDocumentClick(event: MouseEvent) { 73 | console.log( 74 | 'Do you have a minute to talk about our Lord and Savior, Ember.js?' 75 | ); 76 | } 77 | } 78 | ``` 79 | 80 | This is essentially equivalent to: 81 | 82 | ```ts 83 | didInsertElement() { 84 | super.didInsertElement(); 85 | 86 | document.addEventListener('click', this.onDocumentClick); 87 | } 88 | ``` 89 | 90 | In addition to the above `{{on}}` will properly tear down the event listener, 91 | when the helper is removed from the DOM. It will also re-register the event 92 | listener, if any of the passed parameters change. 93 | 94 | The [`@action` decorator][@action] is used to bind the `onDocumentClick` method 95 | to the component instance. This is not strictly required here, since we do not 96 | access `this`, but in order to not break with established patterns, we do it 97 | anyway. 98 | 99 | [@action]: https://github.com/emberjs/rfcs/blob/master/text/0408-decorators.md#method-binding 100 | 101 | ### Listening to Events on `window` or `document` 102 | 103 | You will often want to use the `{{on}}` helper to listen to events which are 104 | emitted on `window` or `document`. Because providing access to these globals in 105 | the template as shown in **[Simple Example](#simple-example)** is quite 106 | cumbersome, `{{on}}` brings two friends to the party: 107 | 108 | - `{{on-document eventName eventListener}}` 109 | - `{{on-window eventName eventListener}}` 110 | 111 | They work exactly the same way as `{{on}}` and also accept event options. 112 | 113 | ### Listening to Multiple Events 114 | 115 | You can use the `{{on}}` helper multiple times in the same template and for the 116 | same event target, even for the same event. 117 | 118 | ```hbs 119 | {{on this.someElement "click" this.onClick}} 120 | {{on this.someElement "click" this.anotherOnClick}} 121 | {{on this.someElement "mousemove" this.onMouseMove}} 122 | ``` 123 | 124 | ### Event Options 125 | 126 | All named parameters will be passed through to 127 | [`addEventListener`][addeventlistener] as the third parameter, the options hash. 128 | 129 | [addeventlistener]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener 130 | 131 | ```hbs 132 | {{on-document "scroll" this.onScroll passive=true}} 133 | ``` 134 | 135 | This is essentially equivalent to: 136 | 137 | ```ts 138 | didInsertElement() { 139 | super.didInsertElement(); 140 | 141 | document.addEventListener('scroll', this.onScroll, { passive: true }); 142 | } 143 | ``` 144 | 145 | #### `once` 146 | 147 | To fire an event listener only once, you can pass the [`once` option][addeventlistener-parameters]: 148 | 149 | ```hbs 150 | {{on-window "click" this.clickOnlyTheFirstTime once=true}} 151 | {{on-window "click" this.clickEveryTime}} 152 | ``` 153 | 154 | `clickOnlyTheFirstTime` will only be fired the first time the page is clicked. 155 | `clickEveryTime` is fired every time the page is clicked, including the first 156 | time. 157 | 158 | [addeventlistener-parameters]: https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/addEventListener#Parameters 159 | 160 | #### `capture` 161 | 162 | To listen for an event during the capture phase already, use the [`capture` option][addeventlistener-parameters]: 163 | 164 | ```hbs 165 | {{on-document "click" this.triggeredFirst capture=true}} 166 | 167 | 170 | ``` 171 | 172 | #### `passive` 173 | 174 | If `true`, you promise to not call `event.preventDefault()`. This allows the 175 | browser to optimize the processing of this event and not block the UI thread. 176 | This prevent scroll jank. 177 | 178 | If you still call `event.preventDefault()`, an assertion will be raised. 179 | 180 | ```hbs 181 | {{on-document "scroll" this.trackScrollPosition passive=true}}> 182 | ``` 183 | 184 | #### Internet Explorer 11 Support 185 | 186 | Internet Explorer 11 has a buggy and incomplete implementation of 187 | `addEventListener`: It does not accept an 188 | [`options`][addeventlistener-parameters] parameter and _sometimes_ even throws 189 | a cryptic error when passing options. 190 | 191 | This is why this addon ships a tiny [ponyfill][ponyfill] for `addEventLisener` 192 | that is used internally to emulate the `once`, `capture` and `passive` option. 193 | This means that all currently known [`options`][addeventlistener-parameters] are 194 | polyfilled, so that you can rely on them in your logic. 195 | 196 | [ponyfill]: https://github.com/sindresorhus/ponyfill 197 | 198 | ### Currying / Partial Application 199 | 200 | If you want to curry the function call / partially apply arguments, you can do 201 | so using the [`{{fn}}` helper][fn-helper]: 202 | 203 | [fn-helper]: https://github.com/emberjs/rfcs/blob/master/text/0470-fn-helper.md 204 | 205 | ```hbs 206 | {{#each this.videos as |video|}} 207 | {{on video.element "play" (fn this.onPlay video)}} 208 | {{on video.element "pause" (fn this.onPause video)}} 209 | {{/each}} 210 | ``` 211 | 212 | ```ts 213 | import Component from '@ember/component'; 214 | import { action } from '@ember-decorators/object'; 215 | 216 | interface Video { 217 | element: HTMLVideoElement; 218 | title: string; 219 | } 220 | 221 | export default class UserListComponent extends Component { 222 | videos: Video[]; 223 | 224 | @action 225 | onPlay(video: Video, event: MouseEvent) { 226 | console.log(`Started playing '${video.title}'.`); 227 | } 228 | 229 | @action 230 | onPlay(video: Video, event: MouseEvent) { 231 | console.log(`Paused '${video.title}'.`); 232 | } 233 | } 234 | ``` 235 | 236 | ### `preventDefault` / `stopPropagation` / `stopImmediatePropagation` 237 | 238 | The old [`{{action}}` modifier][action-event-propagation] used to allow easily 239 | calling `event.preventDefault()` like so: 240 | 241 | ```hbs 242 | Click me 243 | ``` 244 | 245 | [action-event-propagation]: https://www.emberjs.com/api/ember/release/classes/Ember.Templates.helpers/methods/action?anchor=action#event-propagation 246 | 247 | You also could easily call `event.stopPropagation()` to avoid bubbling like so: 248 | 249 | ```hbs 250 | Click me 251 | ``` 252 | 253 | You can still do this using [`ember-event-helpers`][ember-event-helpers]: 254 | 255 | [ember-event-helpers]: https://github.com/buschtoens/ember-event-helpers 256 | 257 | ```hbs 258 | Click me 259 | ``` 260 | 261 | ```hbs 262 | Click me 263 | ``` 264 | 265 | ## Attribution 266 | 267 | This addon is a straight copy of [`ember-on-modifier`][ember-on-modifier], the 268 | polyfill for the `{{on}}` modifier. 269 | 270 | [ember-on-modifier]: https://github.com/buschtoens/ember-on-modifier 271 | -------------------------------------------------------------------------------- /tests/integration/helpers/on-test.js: -------------------------------------------------------------------------------- 1 | import { module, test, skip } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { 4 | render, 5 | click, 6 | settled, 7 | setupOnerror, 8 | resetOnerror 9 | } from '@ember/test-helpers'; 10 | import hbs from 'htmlbars-inline-precompile'; 11 | import { set } from '@ember/object'; 12 | import { run } from '@ember/runloop'; 13 | import { gte } from 'ember-compatibility-helpers'; 14 | import { compileTemplate } from '@ember/template-compilation'; 15 | 16 | module('Integration | Helper | on', function(hooks) { 17 | setupRenderingTest(hooks); 18 | hooks.afterEach(() => resetOnerror()); 19 | hooks.beforeEach(function() { 20 | this.testElement = document.createElement('button'); 21 | this.testElement.dataset.foo = 'test-element'; 22 | 23 | this.testParentElement = document.createElement('div'); 24 | this.testParentElement.append(this.testElement); 25 | }); 26 | 27 | test('it basically works', async function(assert) { 28 | assert.expect(6); 29 | 30 | this.someMethod = function(event) { 31 | assert.ok( 32 | this instanceof HTMLButtonElement && 33 | this.dataset.foo === 'test-element', 34 | 'this context is the element' 35 | ); 36 | assert.ok( 37 | event instanceof MouseEvent, 38 | 'first argument is a `MouseEvent`' 39 | ); 40 | assert.strictEqual( 41 | event.target.tagName, 42 | 'BUTTON', 43 | 'correct element tagName' 44 | ); 45 | assert.dom(event.target).hasAttribute('data-foo', 'test-element'); 46 | }; 47 | 48 | await render(hbs`{{on this.testElement "click" this.someMethod}}`); 49 | 50 | assert.counts({ adds: 1, removes: 0 }); 51 | 52 | await click(this.testElement); 53 | 54 | assert.counts({ adds: 1, removes: 0 }); 55 | }); 56 | 57 | test('it can accept the `once` option', async function(assert) { 58 | assert.expect(3); 59 | 60 | let n = 0; 61 | this.someMethod = () => n++; 62 | 63 | await render( 64 | hbs`{{on this.testElement "click" this.someMethod once=true}}` 65 | ); 66 | 67 | assert.counts({ adds: 1, removes: 0 }); 68 | 69 | await click(this.testElement); 70 | await click(this.testElement); 71 | 72 | assert.counts({ adds: 1, removes: 0 }); 73 | 74 | assert.strictEqual(n, 1, 'callback has only been called once'); 75 | }); 76 | 77 | test('unrelated property changes do not break the `once` option', async function(assert) { 78 | assert.expect(5); 79 | 80 | let n = 0; 81 | this.someMethod = () => n++; 82 | this.someProperty = 0; 83 | 84 | await render( 85 | hbs`{{this.someProperty}}{{on this.testElement "click" this.someMethod once=true}}{{this.someProperty}}` 86 | ); 87 | 88 | assert.counts({ adds: 1, removes: 0 }); 89 | 90 | await click(this.testElement); 91 | await click(this.testElement); 92 | 93 | assert.counts({ adds: 1, removes: 0 }); 94 | 95 | assert.strictEqual(n, 1, 'callback has only been called once'); 96 | 97 | run(() => set(this, 'someProperty', 1)); 98 | await settled(); 99 | assert.counts({ adds: 1, removes: 0 }); 100 | 101 | await click(this.testElement); 102 | assert.strictEqual(n, 1, 'callback has only been called once'); 103 | }); 104 | 105 | test('unrelated property changes do not cause the listener to re-register', async function(assert) { 106 | assert.expect(2); 107 | 108 | this.someMethod = () => {}; 109 | this.someProperty = 0; 110 | 111 | await render( 112 | hbs`{{this.someProperty}}{{on this.testElement "click" this.someMethod}}{{this.someProperty}}` 113 | ); 114 | assert.counts({ adds: 1, removes: 0 }); 115 | 116 | run(() => set(this, 'someProperty', 1)); 117 | await settled(); 118 | assert.counts({ adds: 1, removes: 0 }); 119 | }); 120 | 121 | test('it can accept the `capture` option', async function(assert) { 122 | assert.expect(5); 123 | 124 | this.outerListener = () => assert.step('outer'); 125 | this.innerListener = () => assert.step('inner'); 126 | 127 | await render(hbs` 128 | {{on this.testParentElement "click" this.outerListener capture=true}} 129 | {{on this.testElement "click" this.innerListener}} 130 | `); 131 | 132 | assert.counts({ adds: 2, removes: 0 }); 133 | 134 | await click(this.testElement); 135 | 136 | assert.counts({ adds: 2, removes: 0 }); 137 | 138 | assert.verifySteps( 139 | ['outer', 'inner'], 140 | 'outer capture listener was called first' 141 | ); 142 | }); 143 | 144 | test('it can accept the `once` & `capture` option combined', async function(assert) { 145 | assert.expect(6); 146 | 147 | this.outerListener = () => assert.step('outer'); 148 | this.innerListener = () => assert.step('inner'); 149 | 150 | await render(hbs` 151 | {{on this.testParentElement "click" this.outerListener once=true capture=true}} 152 | {{on this.testElement "click" this.innerListener}} 153 | `); 154 | 155 | assert.counts({ adds: 2, removes: 0 }); 156 | 157 | await click(this.testElement); 158 | await click(this.testElement); 159 | 160 | assert.counts({ adds: 2, removes: 0 }); 161 | 162 | assert.verifySteps( 163 | ['outer', 'inner', 'inner'], 164 | 'outer capture listener was called first and was then unregistered' 165 | ); 166 | }); 167 | 168 | test('it raises an assertion when calling `event.preventDefault()` on a `passive` event', async function(assert) { 169 | assert.expect(3); 170 | 171 | this.handler = event => { 172 | assert.expectAssertion( 173 | () => event.preventDefault(), 174 | `ember-on-helper: You marked this listener as 'passive', meaning that you must not call 'event.preventDefault()'.` 175 | ); 176 | }; 177 | 178 | await render( 179 | hbs`{{on this.testElement "click" this.handler passive=true}}` 180 | ); 181 | 182 | assert.counts({ adds: 1, removes: 0 }); 183 | 184 | await click(this.testElement); 185 | 186 | assert.counts({ adds: 1, removes: 0 }); 187 | }); 188 | 189 | (gte('3.0.0') // I have no clue how to catch the error in Ember 2.13 190 | ? test 191 | : skip)('it raises an assertion if an invalid event option is passed in', async function(assert) { 192 | assert.expect(2); 193 | 194 | setupOnerror(function(error) { 195 | assert.strictEqual( 196 | error.message, 197 | "Assertion Failed: ember-on-helper: Provided invalid event options ('nope', 'foo') to 'click' event listener. Only these options are valid: 'capture', 'once', 'passive'", 198 | 'error is thrown' 199 | ); 200 | }); 201 | 202 | await render( 203 | hbs`{{on this.testElement "click" this.someMethod nope=true foo=false}}` 204 | ); 205 | 206 | assert.counts({ adds: 0, removes: 0 }); 207 | }); 208 | 209 | (gte('3.0.0') // I have no clue how to catch the error in Ember 2.13 210 | ? test 211 | : skip)('it raises an assertion if an invalid event name or callback is passed in', async function(assert) { 212 | // There is a bug in Glimmer when rendering helpers that throw an error 213 | setupOnerror( 214 | error => error.message.includes('lastNode') || assert.step(error.message) 215 | ); 216 | 217 | const testExpression = async expression => { 218 | await render( 219 | compileTemplate(` 220 | {{#if this.runTest}} 221 | ${expression} 222 | {{/if}} 223 | `) 224 | ); 225 | // If this was true initially, Glimmer would fail and could not recover 226 | // from it. 227 | run(() => set(this, 'runTest', true)); 228 | await settled(); 229 | run(() => set(this, 'runTest', false)); 230 | }; 231 | 232 | await testExpression(`{{on this.testElement "click" 10}}`); 233 | await testExpression(`{{on this.testElement "click"}}`); 234 | await testExpression(`{{on this.testElement "" undefined}}`); 235 | await testExpression(`{{on this.testElement 10 undefined}}`); 236 | await testExpression(`{{on this.testElement}}`); 237 | await testExpression(`{{on null 10 undefined}}`); 238 | await testExpression(`{{on}}`); 239 | 240 | assert.counts({ adds: 0, removes: 0 }); 241 | 242 | assert.verifySteps([ 243 | "Assertion Failed: ember-on-helper: '10' is not a valid callback. Provide a function.", 244 | "Assertion Failed: ember-on-helper: 'undefined' is not a valid callback. Provide a function.", 245 | "Assertion Failed: ember-on-helper: '' is not a valid event name. It has to be a string with a minimum length of 1 character.", 246 | "Assertion Failed: ember-on-helper: '10' is not a valid event name. It has to be a string with a minimum length of 1 character.", 247 | "Assertion Failed: ember-on-helper: 'undefined' is not a valid event name. It has to be a string with a minimum length of 1 character.", 248 | "Assertion Failed: ember-on-helper: 'null' is not a valid event target. It has to be an Element or an object that conforms to the EventTarget interface.", 249 | "Assertion Failed: ember-on-helper: 'undefined' is not a valid event target. It has to be an Element or an object that conforms to the EventTarget interface." 250 | ]); 251 | }); 252 | 253 | (gte('3.0.0') // I have no clue how to catch the error in Ember 2.13 254 | ? test 255 | : skip)('it recovers after updating to incorrect parameters', async function(assert) { 256 | assert.expect(9); 257 | 258 | const errors = []; 259 | setupOnerror(error => errors.push(error)); 260 | 261 | let n = 0; 262 | this.someMethod = () => n++; 263 | 264 | await render(hbs`{{on this.testElement "click" this.someMethod}}`); 265 | assert.counts({ adds: 1, removes: 0 }); 266 | 267 | await click(this.testElement); 268 | assert.strictEqual(n, 1); 269 | assert.counts({ adds: 1, removes: 0 }); 270 | 271 | run(() => set(this, 'someMethod', undefined)); 272 | await settled(); 273 | assert.counts({ adds: 1, removes: 1 }); 274 | 275 | await click(this.testElement); 276 | assert.strictEqual(n, 1); 277 | assert.counts({ adds: 1, removes: 1 }); 278 | 279 | run(() => set(this, 'someMethod', () => n++)); 280 | await settled(); 281 | assert.counts({ adds: 2, removes: 2 }); 282 | 283 | await click(this.testElement); 284 | assert.strictEqual(n, 2); 285 | assert.counts({ adds: 2, removes: 2 }); 286 | }); 287 | 288 | test('it is re-registered, when the callback changes', async function(assert) { 289 | assert.expect(6); 290 | 291 | let a = 0; 292 | this.someMethod = () => a++; 293 | 294 | await render(hbs`{{on this.testElement "click" this.someMethod}}`); 295 | assert.counts({ adds: 1, removes: 0 }); 296 | 297 | await click(this.testElement); 298 | assert.counts({ adds: 1, removes: 0 }); 299 | 300 | let b = 0; 301 | run(() => set(this, 'someMethod', () => b++)); 302 | await settled(); 303 | assert.counts({ adds: 2, removes: 1 }); 304 | 305 | await click(this.testElement); 306 | assert.counts({ adds: 2, removes: 1 }); 307 | 308 | assert.strictEqual(a, 1); 309 | assert.strictEqual(b, 1); 310 | }); 311 | 312 | test('it is re-registered, when the callback changes and `capture` is used', async function(assert) { 313 | assert.expect(9); 314 | 315 | let a = 0; 316 | this.someMethod = () => a++; 317 | this.capture = true; 318 | 319 | await render( 320 | hbs`{{on this.testElement "click" this.someMethod capture=this.capture}}` 321 | ); 322 | assert.counts({ adds: 1, removes: 0 }); 323 | 324 | await click(this.testElement); 325 | assert.counts({ adds: 1, removes: 0 }); 326 | 327 | let b = 0; 328 | run(() => set(this, 'someMethod', () => b++)); 329 | await settled(); 330 | assert.counts({ adds: 2, removes: 1 }); 331 | 332 | await click(this.testElement); 333 | assert.counts({ adds: 2, removes: 1 }); 334 | 335 | let c = 0; 336 | run(() => { 337 | set(this, 'someMethod', () => c++); 338 | set(this, 'capture', false); 339 | }); 340 | await settled(); 341 | assert.counts({ adds: 3, removes: 2 }); 342 | 343 | await click(this.testElement); 344 | assert.counts({ adds: 3, removes: 2 }); 345 | 346 | assert.strictEqual(a, 1); 347 | assert.strictEqual(b, 1); 348 | assert.strictEqual(c, 1); 349 | }); 350 | }); 351 | --------------------------------------------------------------------------------