├── 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 | [](https://travis-ci.org/buschtoens/ember-on-helper)
4 | [](http://badge.fury.io/js/ember-on-helper)
5 | [](http://badge.fury.io/js/ember-on-helper)
6 | [](https://emberobserver.com/addons/ember-on-helper)
7 | [](https://travis-ci.org/buschtoens/ember-on-helper)
8 | [](https://travis-ci.org/buschtoens/ember-on-helper)
9 | [](https://github.com/prettier/prettier)
10 | [](https://david-dm.org/buschtoens/ember-on-helper)
11 | [](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 |
--------------------------------------------------------------------------------