├── app ├── .gitkeep └── initializers │ └── usable-function-manager.js ├── vendor └── .gitkeep ├── tests ├── helpers │ └── .gitkeep ├── unit │ └── .gitkeep ├── dummy │ ├── app │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── models │ │ │ └── .gitkeep │ │ ├── routes │ │ │ └── .gitkeep │ │ ├── styles │ │ │ └── app.css │ │ ├── components │ │ │ ├── counter.hbs │ │ │ └── counter.js │ │ ├── router.js │ │ ├── app.js │ │ ├── templates │ │ │ └── application.hbs │ │ ├── controllers │ │ │ └── application.js │ │ └── index.html │ ├── public │ │ └── robots.txt │ └── config │ │ ├── optional-features.json │ │ ├── targets.js │ │ └── environment.js ├── test-helper.js ├── index.html └── integration │ ├── use-test.js │ ├── functions-test.js │ ├── resources-test.js │ └── modifiers-test.js ├── .vscode └── settings.json ├── .watchmanconfig ├── .template-lintrc.js ├── index.js ├── config ├── environment.js └── ember-try.js ├── addon ├── initializers │ └── usable-function-manager.js ├── -private │ ├── functions.js │ ├── resources.js │ └── modifiers.js └── index.js ├── .ember-cli ├── .eslintignore ├── .dependabot └── config.yml ├── .editorconfig ├── .gitignore ├── .npmignore ├── .github └── workflows │ ├── types.yml │ ├── lint.yml │ └── ci.yml ├── testem.js ├── CONTRIBUTING.md ├── CHANGELOG.md ├── LICENSE.md ├── .eslintrc.js ├── ember-cli-build.js ├── RELEASE.md ├── package.json └── README.md /app/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tests/helpers/.gitkeep: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.enable": true 3 | } -------------------------------------------------------------------------------- /tests/dummy/app/components/counter.hbs: -------------------------------------------------------------------------------- 1 | {{this.count}} 2 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp", "dist"] 3 | } 4 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /.template-lintrc.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | extends: 'octane' 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | name: require('./package').name 5 | }; 6 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /app/initializers/usable-function-manager.js: -------------------------------------------------------------------------------- 1 | export { default, initialize } from 'ember-could-get-used-to-this/initializers/usable-function-manager'; 2 | 3 | -------------------------------------------------------------------------------- /tests/dummy/config/optional-features.json: -------------------------------------------------------------------------------- 1 | { 2 | "application-template-wrapper": false, 3 | "default-async-observers": true, 4 | "jquery-integration": false, 5 | "template-only-glimmer-components": true 6 | } 7 | -------------------------------------------------------------------------------- /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 | module.exports = { 10 | browsers 11 | }; 12 | -------------------------------------------------------------------------------- /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 | 6 | setApplication(Application.create(config.APP)); 7 | 8 | start(); 9 | -------------------------------------------------------------------------------- /addon/initializers/usable-function-manager.js: -------------------------------------------------------------------------------- 1 | import 'ember-could-get-used-to-this/-private/functions'; 2 | 3 | export function initialize(/* application */) { 4 | // application.inject('route', 'foo', 'service:foo'); 5 | } 6 | 7 | export default { 8 | initialize 9 | }; 10 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import EmberRouter from '@ember/routing/router'; 2 | import config from './config/environment'; 3 | 4 | export default class Router extends EmberRouter { 5 | location = config.locationType; 6 | rootURL = config.rootURL; 7 | } 8 | 9 | Router.map(function() { 10 | }); 11 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.dependabot/config.yml: -------------------------------------------------------------------------------- 1 | # Create Pull-Requests for NPM updates automatically 2 | # https://dependabot.com/docs/config-file/ 3 | 4 | version: 1 5 | update_configs: 6 | # Keep package.json (& lockfiles) up to date. 7 | # 8 | # Security updates will be created immediately, 9 | # other updates monthly, to cut down on repo noise. 10 | - package_manager: "javascript" 11 | directory: "/" 12 | update_schedule: "monthly" 13 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Application from '@ember/application'; 2 | import Resolver from 'ember-resolver'; 3 | import loadInitializers from 'ember-load-initializers'; 4 | import config from './config/environment'; 5 | 6 | export default class App extends Application { 7 | modulePrefix = config.modulePrefix; 8 | podModulePrefix = config.podModulePrefix; 9 | Resolver = Resolver; 10 | } 11 | 12 | loadInitializers(App, config.modulePrefix); 13 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |

Welcome to Ember

2 | 3 |
4 | 7 |
8 | 9 |
10 | 11 | 12 |
13 | 14 |
15 | {{#if this.showCount}} 16 | 17 | {{/if}} 18 |
19 | -------------------------------------------------------------------------------- /.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 | /.git/ 16 | /.gitignore 17 | /.template-lintrc.js 18 | /.travis.yml 19 | /.watchmanconfig 20 | /bower.json 21 | /config/ember-try.js 22 | /CONTRIBUTING.md 23 | /ember-cli-build.js 24 | /testem.js 25 | /tests/ 26 | /yarn.lock 27 | .gitkeep 28 | 29 | # ember-try 30 | /.node_modules.ember-try/ 31 | /bower.json.ember-try 32 | /package.json.ember-try 33 | -------------------------------------------------------------------------------- /.github/workflows/types.yml: -------------------------------------------------------------------------------- 1 | name: Types 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # filtering branches here prevents duplicate builds from pull_request and push 7 | branches: 8 | - main 9 | - master 10 | 11 | env: 12 | CI: true 13 | 14 | jobs: 15 | types: 16 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 17 | name: Type Checking 18 | runs-on: ubuntu-latest 19 | 20 | # steps: 21 | # - uses: actions/checkout@v2 22 | # - uses: volta-cli/action@v1 23 | 24 | # - run: yarn install --frozen-lockfile 25 | 26 | # - name: Type Checking 27 | # run: yarn tsc --build 28 | -------------------------------------------------------------------------------- /testem.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | test_page: 'tests/index.html?hidepassed', 3 | disable_watching: true, 4 | launch_in_ci: [ 5 | 'Chrome' 6 | ], 7 | launch_in_dev: [ 8 | 'Chrome' 9 | ], 10 | browser_args: { 11 | Chrome: { 12 | ci: [ 13 | // --no-sandbox is needed when running Chrome inside a container 14 | process.env.CI ? '--no-sandbox' : null, 15 | '--headless', 16 | '--disable-dev-shm-usage', 17 | '--disable-software-rasterizer', 18 | '--mute-audio', 19 | '--remote-debugging-port=0', 20 | '--window-size=1440,900' 21 | ].filter(Boolean) 22 | } 23 | } 24 | }; 25 | -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Controller from '@ember/controller'; 2 | import { action } from '@ember/object'; 3 | import { tracked } from '@glimmer/tracking'; 4 | 5 | export default class ApplicationController extends Controller { 6 | @tracked interval = 1000; 7 | @tracked showCount = true; 8 | 9 | @action 10 | toggleShowCount() { 11 | this.showCount = !this.showCount; 12 | } 13 | 14 | @action 15 | increaseInterval() { 16 | this.interval = this.interval + 100; 17 | } 18 | 19 | @action 20 | decreaseInterval() { 21 | if (this.interval !== 100) { 22 | this.interval = this.interval - 100; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How To Contribute 2 | 3 | ## Installation 4 | 5 | * `git clone ` 6 | * `cd ember-could-get-used-to-this` 7 | * `yarn install` 8 | 9 | ## Linting 10 | 11 | * `yarn lint` 12 | * `yarn lint --fix` 13 | 14 | ## Running tests 15 | 16 | * `ember test` – Runs the test suite on the current Ember version 17 | * `ember test --server` – Runs the test suite in "watch mode" 18 | * `ember try:each` – Runs the test suite against multiple Ember versions 19 | 20 | ## Running the dummy application 21 | 22 | * `ember serve` 23 | * Visit the dummy application at [http://localhost:4200](http://localhost:4200). 24 | 25 | For more information on using ember-cli, visit [https://ember-cli.com/](https://ember-cli.com/). 26 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## v1.0.1 (2020-10-29) 2 | 3 | #### :bug: Bug Fix 4 | * [#17](https://github.com/pzuraq/ember-could-get-used-to-this/pull/17) [BUGFIX] Fix the package repository ([@pzuraq](https://github.com/pzuraq)) 5 | * [#16](https://github.com/pzuraq/ember-could-get-used-to-this/pull/16) [BUGFIX] Detangles lifecycle and value ([@pzuraq](https://github.com/pzuraq)) 6 | 7 | #### Committers: 1 8 | - Chris Garrett ([@pzuraq](https://github.com/pzuraq)) 9 | 10 | 11 | ## v1.0.0 (2020-10-29) 12 | 13 | #### :rocket: Enhancement 14 | * [#10](https://github.com/pzuraq/ember-could-get-used-to-this/pull/10) [FEAT] Initial Implementation ([@pzuraq](https://github.com/pzuraq)) 15 | 16 | #### Committers: 1 17 | - Chris Garrett ([@pzuraq](https://github.com/pzuraq)) 18 | 19 | -------------------------------------------------------------------------------- /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/dummy/app/components/counter.js: -------------------------------------------------------------------------------- 1 | import Component from '@glimmer/component'; 2 | import { tracked } from '@glimmer/tracking'; 3 | import { use, Resource } from 'ember-could-get-used-to-this'; 4 | 5 | class Counter extends Resource { 6 | @tracked count = 0; 7 | 8 | intervalId = null; 9 | 10 | get value() { 11 | return this.count; 12 | } 13 | 14 | setup() { 15 | this.intervalId = setInterval(() => this.count++, this.args.positional[0]); 16 | } 17 | 18 | update() { 19 | clearInterval(this.intervalId); 20 | this.intervalId = setInterval(() => this.count++, this.args.positional[0]); 21 | } 22 | 23 | teardown() { 24 | clearInterval(this.intervalId); 25 | } 26 | } 27 | 28 | export default class CounterWrapper extends Component { 29 | @use count = new Counter(() => [this.args.interval]); 30 | } 31 | -------------------------------------------------------------------------------- /addon/-private/functions.js: -------------------------------------------------------------------------------- 1 | import { 2 | setHelperManager, 3 | capabilities as helperCapabilities, 4 | } from '@ember/helper'; 5 | import { assert } from '@ember/debug'; 6 | 7 | class FunctionalHelperManager { 8 | capabilities = helperCapabilities('3.23', { 9 | hasValue: true, 10 | }); 11 | 12 | createHelper(fn, args) { 13 | return { fn, args }; 14 | } 15 | 16 | getValue({ fn, args }) { 17 | assert( 18 | `Functional helpers cannot receive hash parameters. \`${this.getDebugName(fn)}\` received ${Object.keys(args.named)}`, 19 | Object.keys(args.named).length === 0 20 | ); 21 | 22 | return fn(...args.positional); 23 | } 24 | 25 | getDebugName(fn) { 26 | return fn.name || '(anonymous function)'; 27 | } 28 | } 29 | 30 | const FUNCTIONAL_HELPER_MANAGER = new FunctionalHelperManager(); 31 | 32 | setHelperManager(() => FUNCTIONAL_HELPER_MANAGER, Function.prototype); 33 | -------------------------------------------------------------------------------- /addon/index.js: -------------------------------------------------------------------------------- 1 | import { invokeHelper } from '@ember/helper'; 2 | import { getValue } from '@glimmer/tracking/primitives/cache'; 3 | 4 | export { modifier, Modifier } from './-private/modifiers'; 5 | export { Resource } from './-private/resources'; 6 | 7 | export function use(prototype, key, desc) { 8 | let resources = new WeakMap(); 9 | let { initializer } = desc; 10 | 11 | return { 12 | get() { 13 | let resource = resources.get(this); 14 | 15 | if (!resource) { 16 | let { definition, args } = initializer.call(this); 17 | 18 | resource = invokeHelper(this, definition, () => { 19 | let reified = args(); 20 | 21 | if (Array.isArray(reified)) { 22 | return { positional: reified }; 23 | } 24 | 25 | return reified; 26 | }); 27 | resources.set(this, resource); 28 | } 29 | 30 | return getValue(resource); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2020 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/use-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { tracked } from 'tracked-built-ins'; 3 | 4 | import { use, Resource } from 'ember-could-get-used-to-this'; 5 | 6 | module('@use', () => { 7 | test('it works', async function (assert) { 8 | class TestResource extends Resource { 9 | @tracked firstArg; 10 | 11 | setup() { 12 | this.firstArg = this.args.positional[0]; 13 | } 14 | } 15 | 16 | class MyClass { 17 | @use test = new TestResource(() => ['hello']) 18 | } 19 | 20 | let instance = new MyClass(); 21 | 22 | assert.equal(instance.test.firstArg, 'hello'); 23 | }); 24 | 25 | test('resources update if args update', async function (assert) { 26 | class TestResource extends Resource { 27 | @tracked firstArg; 28 | 29 | setup() { 30 | this.firstArg = this.args.positional[0]; 31 | } 32 | } 33 | 34 | class MyClass { 35 | @tracked text = 'hello' 36 | 37 | @use test = new TestResource(() => [this.text]) 38 | } 39 | 40 | let instance = new MyClass(); 41 | 42 | assert.equal(instance.test.firstArg, 'hello'); 43 | 44 | instance.text = 'world'; 45 | 46 | assert.equal(instance.test.firstArg, 'world'); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const getChannelURL = require('ember-source-channel-url'); 4 | 5 | module.exports = async function () { 6 | return { 7 | useYarn: true, 8 | scenarios: [ 9 | { 10 | name: 'ember-lts-3.24', 11 | npm: { 12 | devDependencies: { 13 | 'ember-source': await getChannelURL('release'), 14 | }, 15 | }, 16 | }, 17 | { 18 | name: 'ember-release', 19 | npm: { 20 | devDependencies: { 21 | 'ember-source': await getChannelURL('release'), 22 | }, 23 | }, 24 | }, 25 | { 26 | name: 'ember-beta', 27 | npm: { 28 | devDependencies: { 29 | 'ember-source': await getChannelURL('beta'), 30 | }, 31 | }, 32 | }, 33 | { 34 | name: 'ember-canary', 35 | npm: { 36 | devDependencies: { 37 | 'ember-source': await getChannelURL('canary'), 38 | }, 39 | }, 40 | }, 41 | { 42 | name: 'embroider', 43 | npm: { 44 | devDependencies: { 45 | '@embroider/core': '*', 46 | '@embroider/webpack': '*', 47 | '@embroider/compat': '*', 48 | }, 49 | }, 50 | }, 51 | ], 52 | }; 53 | }; 54 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | parser: 'babel-eslint', 4 | parserOptions: { 5 | ecmaVersion: 2018, 6 | sourceType: 'module', 7 | ecmaFeatures: { 8 | legacyDecorators: true 9 | } 10 | }, 11 | plugins: [ 12 | 'ember' 13 | ], 14 | extends: [ 15 | 'eslint:recommended', 16 | 'plugin:ember/recommended' 17 | ], 18 | env: { 19 | browser: true 20 | }, 21 | rules: { 22 | 'ember/no-jquery': 'error', 23 | 'quotes': ['error', 'single'], 24 | }, 25 | overrides: [ 26 | // node files 27 | { 28 | files: [ 29 | '.eslintrc.js', 30 | '.template-lintrc.js', 31 | 'ember-cli-build.js', 32 | 'index.js', 33 | 'testem.js', 34 | 'blueprints/*/index.js', 35 | 'config/**/*.js', 36 | 'tests/dummy/config/**/*.js' 37 | ], 38 | excludedFiles: [ 39 | 'addon/**', 40 | 'addon-test-support/**', 41 | 'app/**', 42 | 'tests/dummy/app/**' 43 | ], 44 | parserOptions: { 45 | sourceType: 'script' 46 | }, 47 | env: { 48 | browser: false, 49 | node: true 50 | }, 51 | plugins: ['node'], 52 | rules: Object.assign({}, require('eslint-plugin-node').configs.recommended.rules, { 53 | // add your custom rules and overrides for node files here 54 | }) 55 | } 56 | ] 57 | }; 58 | -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(environment) { 4 | let 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. EMBER_NATIVE_DECORATOR_SUPPORT: 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 | 26 | if (environment === 'development') { 27 | // ENV.APP.LOG_RESOLVER = true; 28 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 29 | // ENV.APP.LOG_TRANSITIONS = true; 30 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 31 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 32 | } 33 | 34 | if (environment === 'test') { 35 | // Testem prefers this... 36 | ENV.locationType = 'none'; 37 | 38 | // keep test console output quieter 39 | ENV.APP.LOG_ACTIVE_GENERATION = false; 40 | ENV.APP.LOG_VIEW_LOOKUPS = false; 41 | 42 | ENV.APP.rootElement = '#ember-testing'; 43 | ENV.APP.autoboot = false; 44 | } 45 | 46 | if (environment === 'production') { 47 | // here you can enable a production-specific feature 48 | } 49 | 50 | return ENV; 51 | }; 52 | -------------------------------------------------------------------------------- /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 | let app = new EmberAddon(defaults, { 7 | // Add options here 8 | }); 9 | 10 | /* 11 | This build file specifies the options for the dummy test app of this 12 | addon, located in `/tests/dummy` 13 | This build file does *not* influence how the addon or the app using it 14 | behave. You most likely want to be modifying `./index.js` or app's build file 15 | */ 16 | 17 | if ('@embroider/webpack' in app.dependencies()) { 18 | const { Webpack } = require('@embroider/webpack'); // eslint-disable-line 19 | return require('@embroider/compat') // eslint-disable-line 20 | .compatBuild(app, Webpack, { 21 | staticAddonTestSupportTrees: true, 22 | staticAddonTrees: true, 23 | // temporarily disabled to allow tests to dynamically register helpers 24 | staticHelpers: false, 25 | staticComponents: true, 26 | packageRules: [ 27 | { 28 | // Components used during testing, 29 | // these are dynamically registered during the tests 30 | package: 'dummy', 31 | components: { 32 | '{{add}}': { safeToIgnore: true }, 33 | '{{count}}': { safeToIgnore: true }, 34 | '{{set-text}}': { safeToIgnore: true }, 35 | }, 36 | }, 37 | ], 38 | }); 39 | } 40 | 41 | return app.toTree(); 42 | }; 43 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # filtering branches here prevents duplicate builds from pull_request and push 7 | branches: 8 | - main 9 | - master 10 | 11 | env: 12 | CI: true 13 | 14 | jobs: 15 | source: 16 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 17 | name: Source 18 | runs-on: ubuntu-latest 19 | 20 | steps: 21 | - uses: actions/checkout@v2 22 | - uses: volta-cli/action@v1 23 | 24 | - run: yarn install --frozen-lockfile 25 | 26 | - name: ESLint 27 | run: yarn lint:js 28 | 29 | # - name: Templates 30 | # run: yarn lint:hbs 31 | 32 | 33 | # docs: 34 | # if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 35 | # name: Docs 36 | # runs-on: ubuntu-latest 37 | 38 | # steps: 39 | # - uses: actions/checkout@v2 40 | # - uses: volta-cli/action@v1 41 | 42 | # - run: yarn install 43 | # - run: yarn lint:docs 44 | 45 | 46 | # docs-js-code: 47 | # if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 48 | # name: Docs (JS Code Samples) 49 | # runs-on: ubuntu-latest 50 | 51 | # steps: 52 | # - uses: actions/checkout@v2 53 | # - uses: volta-cli/action@v1 54 | 55 | # - run: yarn install 56 | # - run: yarn lint:docs-js 57 | 58 | # commits: 59 | # if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 60 | # name: Commit Messages 61 | # runs-on: ubuntu-latest 62 | 63 | # steps: 64 | # - uses: actions/checkout@v2 65 | # with: 66 | # fetch-depth: 0 67 | 68 | # - uses: volta-cli/action@v1 69 | # - uses: wagoid/commitlint-github-action@v3.0.1 70 | -------------------------------------------------------------------------------- /RELEASE.md: -------------------------------------------------------------------------------- 1 | # Release Process 2 | 3 | Releases are mostly automated using 4 | [release-it](https://github.com/release-it/release-it/) and 5 | [lerna-changelog](https://github.com/lerna/lerna-changelog/). 6 | 7 | ## Preparation 8 | 9 | Since the majority of the actual release process is automated, the primary 10 | remaining task prior to releasing is confirming that all pull requests that 11 | have been merged since the last release have been labeled with the appropriate 12 | `lerna-changelog` labels and the titles have been updated to ensure they 13 | represent something that would make sense to our users. Some great information 14 | on why this is important can be found at 15 | [keepachangelog.com](https://keepachangelog.com/en/1.0.0/), but the overall 16 | guiding principle here is that changelogs are for humans, not machines. 17 | 18 | When reviewing merged PR's the labels to be used are: 19 | 20 | * breaking - Used when the PR is considered a breaking change. 21 | * enhancement - Used when the PR adds a new feature or enhancement. 22 | * bug - Used when the PR fixes a bug included in a previous release. 23 | * documentation - Used when the PR adds or updates documentation. 24 | * internal - Used for internal changes that still require a mention in the 25 | changelog/release notes. 26 | 27 | ## Release 28 | 29 | Once the prep work is completed, the actual release is straight forward: 30 | 31 | * First, ensure that you have installed your projects dependencies: 32 | 33 | ```sh 34 | yarn install 35 | ``` 36 | 37 | * Second, ensure that you have obtained a 38 | [GitHub personal access token][generate-token] with the `repo` scope (no 39 | other permissions are needed). Make sure the token is available as the 40 | `GITHUB_AUTH` environment variable. 41 | 42 | For instance: 43 | 44 | ```bash 45 | export GITHUB_AUTH=abc123def456 46 | ``` 47 | 48 | [generate-token]: https://github.com/settings/tokens/new?scopes=repo&description=GITHUB_AUTH+env+variable 49 | 50 | * And last (but not least 😁) do your release. 51 | 52 | ```sh 53 | npx release-it 54 | ``` 55 | 56 | [release-it](https://github.com/release-it/release-it/) manages the actual 57 | release process. It will prompt you to to choose the version number after which 58 | you will have the chance to hand tweak the changelog to be used (for the 59 | `CHANGELOG.md` and GitHub release), then `release-it` continues on to tagging, 60 | pushing the tag and commits, etc. 61 | -------------------------------------------------------------------------------- /addon/-private/resources.js: -------------------------------------------------------------------------------- 1 | import { 2 | setHelperManager, 3 | capabilities as helperCapabilities, 4 | } from '@ember/helper'; 5 | import { createCache, getValue } from '@glimmer/tracking/primitives/cache'; 6 | import { setOwner } from '@ember/application'; 7 | import { destroy, registerDestructor, associateDestroyableChild } from '@ember/destroyable'; 8 | 9 | export class Resource { 10 | constructor(ownerOrThunk, args) { 11 | if (typeof ownerOrThunk === 'function') { 12 | return { definition: this.constructor, args: ownerOrThunk }; 13 | } 14 | 15 | setOwner(this, ownerOrThunk); 16 | this.args = args; 17 | } 18 | 19 | setup() {} 20 | } 21 | 22 | class ResourceManager { 23 | capabilities = helperCapabilities('3.23', { 24 | hasValue: true, 25 | hasDestroyable: true, 26 | }); 27 | 28 | constructor(owner) { 29 | this.owner = owner; 30 | } 31 | 32 | createHelper(Class, args) { 33 | let { update, teardown } = Class.prototype; 34 | 35 | let hasUpdate = typeof update === 'function'; 36 | let hasTeardown = typeof teardown === 'function'; 37 | 38 | let owner = this.owner; 39 | 40 | let instance; 41 | let cache; 42 | 43 | if (hasUpdate) { 44 | cache = createCache(() => { 45 | if (instance === undefined) { 46 | instance = setupInstance(cache, Class, owner, args, hasTeardown); 47 | } else { 48 | instance.update(); 49 | } 50 | 51 | return instance; 52 | }); 53 | } else { 54 | cache = createCache(() => { 55 | if (instance !== undefined) { 56 | destroy(instance); 57 | } 58 | 59 | instance = setupInstance(cache, Class, owner, args, hasTeardown); 60 | 61 | return instance; 62 | }); 63 | } 64 | 65 | return cache; 66 | } 67 | 68 | getValue(cache) { 69 | let instance = getValue(cache); 70 | 71 | return instance; 72 | } 73 | 74 | getDestroyable(cache) { 75 | return cache; 76 | } 77 | 78 | getDebugName(fn) { 79 | return fn.name || '(anonymous function)'; 80 | } 81 | } 82 | 83 | function setupInstance(cache, Class, owner, args, hasTeardown) { 84 | let instance = new Class(owner, args); 85 | associateDestroyableChild(cache, instance); 86 | instance.setup(); 87 | 88 | if (hasTeardown) { 89 | registerDestructor(instance, () => instance.teardown()); 90 | } 91 | 92 | return instance; 93 | } 94 | 95 | setHelperManager((owner) => new ResourceManager(owner), Resource); 96 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-could-get-used-to-this", 3 | "version": "1.0.1", 4 | "description": "The default blueprint for ember-cli addons.", 5 | "keywords": [ 6 | "ember-addon" 7 | ], 8 | "repository": { 9 | "type": "git", 10 | "url": "git@github.com:pzuraq/ember-could-get-used-to-this.git" 11 | }, 12 | "license": "MIT", 13 | "author": "", 14 | "directories": { 15 | "doc": "doc", 16 | "test": "tests" 17 | }, 18 | "scripts": { 19 | "build": "ember build", 20 | "lint": "yarn lint:js", 21 | "lint:js": "eslint .", 22 | "start": "ember serve", 23 | "test": "ember test", 24 | "test:all": "ember try:each" 25 | }, 26 | "dependencies": { 27 | "ember-cli-babel": "^7.25.0", 28 | "ember-cli-htmlbars": "^4.2.0" 29 | }, 30 | "devDependencies": { 31 | "@ember/optional-features": "^1.1.0", 32 | "@glimmer/component": "^1.0.0", 33 | "@types/ember": "^3.16.1", 34 | "babel-eslint": "^10.0.3", 35 | "broccoli-asset-rev": "^3.0.0", 36 | "ember-auto-import": "^1.5.3", 37 | "ember-cli": "~3.15.1", 38 | "ember-cli-dependency-checker": "^3.2.0", 39 | "ember-cli-inject-live-reload": "^2.0.1", 40 | "ember-cli-sri": "^2.1.1", 41 | "ember-cli-uglify": "^3.0.0", 42 | "ember-disable-prototype-extensions": "^1.1.3", 43 | "ember-export-application-global": "^2.0.1", 44 | "ember-load-initializers": "^2.1.1", 45 | "ember-maybe-import-regenerator": "^0.1.6", 46 | "ember-qunit": "^4.6.0", 47 | "ember-resolver": "^7.0.0", 48 | "ember-source": "~3.23.0-beta.2", 49 | "ember-source-channel-url": "^2.0.1", 50 | "ember-try": "^1.4.0", 51 | "eslint": "^7.12.1", 52 | "eslint-plugin-ember": "^7.7.1", 53 | "eslint-plugin-node": "^10.0.0", 54 | "loader.js": "^4.7.0", 55 | "qunit-dom": "^0.9.2", 56 | "release-it": "^14.2.0", 57 | "release-it-lerna-changelog": "^3.0.0", 58 | "tracked-built-ins": "^1.0.2" 59 | }, 60 | "engines": { 61 | "node": "8.* || >= 10.*" 62 | }, 63 | "publishConfig": { 64 | "registry": "https://registry.npmjs.org" 65 | }, 66 | "ember": { 67 | "edition": "octane" 68 | }, 69 | "ember-addon": { 70 | "configPath": "tests/dummy/config" 71 | }, 72 | "release-it": { 73 | "plugins": { 74 | "release-it-lerna-changelog": { 75 | "infile": "CHANGELOG.md", 76 | "launchEditor": true 77 | } 78 | }, 79 | "git": { 80 | "tagName": "v${version}" 81 | }, 82 | "github": { 83 | "release": true, 84 | "tokenRef": "GITHUB_AUTH" 85 | } 86 | }, 87 | "volta": { 88 | "node": "14.16.0", 89 | "yarn": "1.22.10" 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /tests/integration/functions-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, settled, setupOnerror } from '@ember/test-helpers'; 4 | import { hbs } from 'ember-cli-htmlbars'; 5 | import { tracked } from 'tracked-built-ins'; 6 | 7 | module('Integration | functions', (hooks) => { 8 | setupRenderingTest(hooks); 9 | 10 | test('functions can be used as helpers', async function(assert) { 11 | this.owner.register('helper:add', (a, b) => a + b); 12 | 13 | await render(hbs`{{add 1 2}}`); 14 | 15 | assert.equal(this.element.textContent.trim(), '3'); 16 | }); 17 | 18 | test('functional helpers update if args update', async function(assert) { 19 | this.owner.register('helper:add', (a, b) => a + b); 20 | 21 | this.first = 1; 22 | this.second = 2; 23 | 24 | await render(hbs`{{add this.first this.second}}`); 25 | 26 | assert.equal(this.element.textContent.trim(), '3'); 27 | 28 | this.set('first', 2); 29 | this.set('second', 3); 30 | 31 | await settled(); 32 | 33 | assert.equal(this.element.textContent.trim(), '5'); 34 | }); 35 | 36 | test('functional helpers update if tracked state used within updates', async function(assert) { 37 | this.owner.register('helper:add', ({ a, b }) => a + b); 38 | 39 | this.value = tracked({ a: 1, b: 2 }); 40 | 41 | await render(hbs`{{add this.value}}`); 42 | 43 | assert.equal(this.element.textContent.trim(), '3'); 44 | 45 | this.value.a = 2; 46 | this.value.b = 3; 47 | 48 | await settled(); 49 | 50 | assert.equal(this.element.textContent.trim(), '5'); 51 | }); 52 | 53 | test('functional helpers cache correctly', async function(assert) { 54 | let count = 0; 55 | this.owner.register('helper:count', () => ++count); 56 | 57 | this.first = 1; 58 | this.second = 2; 59 | 60 | await render(hbs`{{count this.first}} {{this.second}}`); 61 | 62 | assert.equal(this.element.textContent.trim(), '1 2'); 63 | assert.equal(count, 1, 'calculated once'); 64 | 65 | this.set('second', 3); 66 | 67 | await settled(); 68 | assert.equal(this.element.textContent.trim(), '1 3'); 69 | assert.equal(count, 1, 'returned cached value'); 70 | 71 | this.set('first', 2); 72 | 73 | assert.equal(this.element.textContent.trim(), '2 3'); 74 | assert.equal(count, 2, 'cached value updated'); 75 | }); 76 | 77 | test('functional helpers throw an error if passed hash args', async function(assert) { 78 | let add = (a, b) => a + b; 79 | this.owner.register('helper:add', add); 80 | 81 | setupOnerror((e) => { 82 | assert.equal(e.message, 'Assertion Failed: Functional helpers cannot receive hash parameters. `add` received first,second'); 83 | }); 84 | 85 | await render(hbs`{{add first=1 second=2}}`); 86 | }); 87 | }); 88 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | pull_request: 5 | push: 6 | # filtering branches here prevents duplicate builds from pull_request and push 7 | branches: 8 | - main 9 | - master 10 | schedule: 11 | # Check on floating deps weekly 12 | - cron: '0 3 * * 0' # every Sunday at 3am 13 | 14 | env: 15 | CI: true 16 | 17 | jobs: 18 | tests: 19 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 20 | name: Base Tests 21 | timeout-minutes: 5 22 | runs-on: ubuntu-latest 23 | strategy: 24 | matrix: 25 | node: 26 | - "12" 27 | - "14" 28 | steps: 29 | - uses: actions/checkout@v2 30 | - uses: volta-cli/action@v1 31 | with: 32 | node-version: ${{ matrix.node }} 33 | 34 | - run: yarn install --frozen-lockfile 35 | 36 | - name: Test with ${{ matrix.node }} 37 | run: yarn test 38 | 39 | floating-dependencies: 40 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 41 | name: Floating Dependencies 42 | timeout-minutes: 5 43 | runs-on: ubuntu-latest 44 | strategy: 45 | matrix: 46 | node: 47 | - "12" 48 | - "14" 49 | 50 | steps: 51 | - uses: actions/checkout@v2 52 | - uses: volta-cli/action@v1 53 | with: 54 | node-version: ${{ matrix.node }} 55 | 56 | - run: yarn install --no-lockfile 57 | 58 | - name: Test with Node ${{ matrix.node }} 59 | run: yarn test 60 | 61 | try-scenarios: 62 | if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" 63 | name: "Compatibility" 64 | timeout-minutes: 5 65 | runs-on: ubuntu-latest 66 | 67 | strategy: 68 | fail-fast: true 69 | matrix: 70 | ember-try-scenario: 71 | - ember-lts-3.24 72 | - ember-release 73 | - ember-beta 74 | - ember-canary 75 | - embroider 76 | steps: 77 | - uses: actions/checkout@v2 78 | - uses: volta-cli/action@v1 79 | with: 80 | node-version: 12.x 81 | - name: install dependencies 82 | run: yarn install --frozen-lockfile 83 | - name: test 84 | run: node_modules/.bin/ember try:one ${{ matrix.ember-try-scenario }} --skip-cleanup 85 | 86 | 87 | # publish: 88 | # name: Release 89 | # runs-on: ubuntu-latest 90 | # if: github.ref == 'refs/heads/main' 91 | # needs: [tests, floating-dependencies, try-scenarios] 92 | 93 | # steps: 94 | # - uses: actions/checkout@v2 95 | # with: 96 | # persist-credentials: false 97 | # - uses: volta-cli/action@v1 98 | # - run: yarn install 99 | 100 | # - name: Release 101 | # env: 102 | # GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 103 | # NPM_TOKEN: ${{ secrets.NPM_TOKEN }} 104 | # run: yarn semantic-release 105 | -------------------------------------------------------------------------------- /addon/-private/modifiers.js: -------------------------------------------------------------------------------- 1 | import { 2 | setModifierManager, 3 | capabilities as modifierCapabilities, 4 | } from '@ember/modifier'; 5 | import { destroy, registerDestructor } from '@ember/destroyable'; 6 | import { setOwner } from '@ember/application'; 7 | 8 | class FunctionalModifierManager { 9 | capabilities = modifierCapabilities('3.22'); 10 | 11 | createModifier(fn, args) { 12 | return { fn, args, element: undefined, destructor: undefined }; 13 | } 14 | 15 | installModifier(state, element) { 16 | state.element = element; 17 | this.setupModifier(state); 18 | } 19 | 20 | updateModifier(state) { 21 | this.destroyModifier(state); 22 | this.setupModifier(state); 23 | } 24 | 25 | setupModifier(state) { 26 | let { fn, args, element } = state; 27 | 28 | state.destructor = fn(element, args.positional, args.named); 29 | } 30 | 31 | destroyModifier(state) { 32 | if (typeof state.destructor === 'function') { 33 | state.destructor(); 34 | } 35 | } 36 | 37 | getDebugName(fn) { 38 | return fn.name || '(anonymous function)'; 39 | } 40 | } 41 | 42 | const FUNCTIONAL_MODIFIER_MANAGER = new FunctionalModifierManager(); 43 | const FUNCTIONAL_MODIFIER_MANAGER_FACTORY = () => FUNCTIONAL_MODIFIER_MANAGER; 44 | 45 | export function modifier(fn) { 46 | return setModifierManager(FUNCTIONAL_MODIFIER_MANAGER_FACTORY, fn); 47 | } 48 | 49 | //////////// 50 | 51 | export class Modifier { 52 | constructor(owner, args) { 53 | setOwner(this, owner); 54 | this.args = args; 55 | } 56 | 57 | setup() {} 58 | } 59 | 60 | class ClassModifierManager { 61 | capabilities = modifierCapabilities('3.22'); 62 | 63 | constructor(owner) { 64 | this.owner = owner; 65 | } 66 | 67 | createModifier(Class, args) { 68 | let instance = new Class(this.owner, args); 69 | 70 | return { 71 | Class, 72 | instance, 73 | args, 74 | element: undefined, 75 | }; 76 | } 77 | 78 | installModifier(state, element) { 79 | state.element = element; 80 | this.setupModifier(state); 81 | } 82 | 83 | updateModifier(state) { 84 | if (typeof state.instance.update === 'function') { 85 | state.instance.update(); 86 | } else { 87 | this.destroyModifier(state); 88 | 89 | let { Class, args } = state; 90 | 91 | state.instance = new Class(this.owner, args); 92 | 93 | this.setupModifier(state); 94 | } 95 | } 96 | 97 | setupModifier({ instance, element }) { 98 | instance.element = element; 99 | instance.setup(); 100 | 101 | if (typeof instance.teardown === 'function') { 102 | registerDestructor(instance, () => instance.teardown()); 103 | } 104 | } 105 | 106 | destroyModifier(state) { 107 | destroy(state.instance); 108 | } 109 | 110 | getDebugName(Class) { 111 | return Class.name || '(anonymous class)'; 112 | } 113 | } 114 | 115 | setModifierManager((owner) => new ClassModifierManager(owner), Modifier); 116 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ember-could-get-used-to-this 2 | ============================================================================== 3 | 4 | ![I Could Get Used To This](https://i.giphy.com/media/Q5LcPLQxjB1ZOm7Ozs/giphy.webp) 5 | 6 | Ember Could Get Used To This is an opinionated take on the future direction of 7 | non-component template constructs in Ember. See [this blog post](https://www.pzuraq.com/introducing-use/) 8 | for more details! 9 | 10 | 11 | Compatibility 12 | ------------------------------------------------------------------------------ 13 | 14 | * Ember.js 3.23 or above 15 | * Ember CLI v2.13 or above 16 | * Node.js v8 or above 17 | 18 | 19 | Installation 20 | ------------------------------------------------------------------------------ 21 | 22 | ``` 23 | ember install ember-could-get-used-to-this 24 | ``` 25 | 26 | 27 | Usage 28 | ------------------------------------------------------------------------------ 29 | 30 | ### Functions 31 | 32 | **Good news!** As of Ember 4.5, this feature is now built into Ember, and includes both positional and named arguments. For more details, read the [Plain Old Functions as Helper blog post](https://blog.emberjs.com/plain-old-functions-as-helpers/). 33 | 34 | You can export plain functions from inside `app/helpers` and use them as helpers. This only support positional arguments: 35 | 36 | ```js 37 | // app/helpers/add-numbers.js 38 | export default function addNumbers(number1, number2) { 39 | return number1 + number2; 40 | } 41 | ``` 42 | 43 | ```hbs 44 | {{! Usage in template: outputs 13 }} 45 | {{add-numbers 10 3}} 46 | ``` 47 | 48 | ### Modifiers 49 | 50 | You can define your own modifiers. You can do so using either a class-based or a functional style. 51 | 52 | Modifiers can be used like this: 53 | 54 | ```hbs 55 | 56 | ``` 57 | 58 | #### Functional modifiers 59 | 60 | ```js 61 | import { modifier } from 'ember-could-get-used-to-this'; 62 | 63 | export default modifier(function on(element, [eventName, handler]) => { 64 | element.addEventListener(eventName, handler); 65 | 66 | return () => { 67 | element.removeEventListener(eventName, handler); 68 | } 69 | }); 70 | ``` 71 | 72 | #### Class-based modifiers 73 | 74 | ```js 75 | // app/modifiers/on.js 76 | import { Modifier } from 'ember-could-get-used-to-this'; 77 | 78 | export default class On extends Modifier { 79 | event = null; 80 | handler = null; 81 | 82 | setup() { 83 | let [event, handler] = this.args.positional; 84 | 85 | this.event = event; 86 | this.handler = handler; 87 | 88 | this.element.addEventListener(event, handler); 89 | } 90 | 91 | teardown() { 92 | let { event, handler } = this; 93 | 94 | this.element.removeEventListener(event, handler); 95 | } 96 | } 97 | ``` 98 | 99 | ### Resources 100 | 101 | Resources are, as of now, also defined in the `app/helpers` directory. They can be either used directly in your templates, or by a JavaScript class. 102 | 103 | ```js 104 | // app/helpers/counter.js 105 | import { tracked } from '@glimmer/tracking'; 106 | import { Resource } from 'ember-could-get-used-to-this'; 107 | 108 | class Counter extends Resource { 109 | @tracked count = 0; 110 | 111 | intervalId = null; 112 | 113 | get value() { 114 | return this.count; 115 | } 116 | 117 | setup() { 118 | this.intervalId = setInterval(() => this.count++, this.args.positional[0]); 119 | } 120 | 121 | update() { 122 | clearInterval(this.intervalId); 123 | this.intervalId = setInterval(() => this.count++, this.args.positional[0]); 124 | } 125 | 126 | teardown() { 127 | clearInterval(this.intervalId); 128 | } 129 | } 130 | ``` 131 | 132 | This example resource can be used from a template like this: 133 | 134 | ```hbs 135 | {{#let (counter @interval) as |count|}} 136 | {{count}} 137 | {{/let}} 138 | ``` 139 | 140 | Or in a JS class: 141 | 142 | ```js 143 | // app/components/counter-wrapper.js 144 | import Component from '@glimmer/component'; 145 | import { use } from 'ember-could-get-used-to-this'; 146 | import Counter from 'my-app/helpers/counter'; 147 | 148 | export default class CounterWrapper extends Component { 149 | @use count = new Counter(() => [this.args.interval]); 150 | } 151 | ``` 152 | 153 | ```hbs 154 | {{! app/components/counter-wrapper.hbs }} 155 | {{this.count}} 156 | ``` 157 | 158 | If you provide an `update` function in your resource, this will be called every time an argument changes. Else, the resource will be torn down and re-created each time an argument changes. 159 | 160 | You can also provide named arguments to a resource, which are available via `this.args.named`. 161 | 162 | 163 | Contributing 164 | ------------------------------------------------------------------------------ 165 | 166 | See the [Contributing](CONTRIBUTING.md) guide for details. 167 | 168 | 169 | License 170 | ------------------------------------------------------------------------------ 171 | 172 | This project is licensed under the [MIT License](LICENSE.md). 173 | -------------------------------------------------------------------------------- /tests/integration/resources-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, settled } from '@ember/test-helpers'; 4 | import Service, { inject as service } from '@ember/service'; 5 | import { hbs } from 'ember-cli-htmlbars'; 6 | import { tracked } from 'tracked-built-ins'; 7 | 8 | import { Resource } from 'ember-could-get-used-to-this'; 9 | 10 | module('resources', (hooks) => { 11 | setupRenderingTest(hooks); 12 | 13 | test('basic resource API', async function (assert) { 14 | this.owner.register( 15 | 'helper:test-resource', 16 | class extends Resource { 17 | @tracked value; 18 | 19 | setup() { 20 | this.value = this.args.positional[0]; 21 | } 22 | } 23 | ); 24 | 25 | await render(hbs` 26 | {{#let (test-resource 'hello') as |test|}} 27 | {{test.value}} 28 | {{/let}} 29 | `); 30 | 31 | assert.equal(this.element.textContent.trim(), 'hello'); 32 | }); 33 | 34 | test('resources update if args update', async function (assert) { 35 | this.owner.register( 36 | 'helper:test-resource', 37 | class extends Resource { 38 | @tracked value; 39 | 40 | setup() { 41 | this.value = this.args.positional[0]; 42 | } 43 | } 44 | ); 45 | 46 | this.text = 'hello'; 47 | 48 | await render(hbs` 49 | {{#let (test-resource this.text) as |test|}} 50 | {{test.value}} 51 | {{/let}} 52 | `); 53 | 54 | assert.equal(this.element.textContent.trim(), 'hello'); 55 | 56 | this.set('text', 'world'); 57 | 58 | await settled(); 59 | 60 | assert.equal(this.element.textContent.trim(), 'world'); 61 | }); 62 | 63 | test('resources update if tracked state used within updates', async function (assert) { 64 | this.owner.register( 65 | 'helper:test-resource', 66 | class extends Resource { 67 | @tracked value; 68 | 69 | setup() { 70 | this.value = this.args.positional[0].text; 71 | } 72 | } 73 | ); 74 | 75 | this.value = tracked({ text: 'hello' }); 76 | 77 | await render(hbs` 78 | {{#let (test-resource this.value) as |test|}} 79 | {{test.value}} 80 | {{/let}} 81 | `); 82 | 83 | assert.equal(this.element.textContent.trim(), 'hello'); 84 | 85 | this.value.text = 'world'; 86 | 87 | await settled(); 88 | 89 | assert.equal(this.element.textContent.trim(), 'world'); 90 | }); 91 | 92 | test('resources can teardown', async function (assert) { 93 | let active = 0; 94 | 95 | this.owner.register( 96 | 'helper:test-resource', 97 | class extends Resource { 98 | @tracked value; 99 | 100 | setup() { 101 | active++; 102 | this.value = this.args.positional[0]; 103 | } 104 | 105 | teardown() { 106 | active--; 107 | } 108 | } 109 | ); 110 | 111 | this.text = 'hello'; 112 | 113 | await render(hbs` 114 | {{#if this.show}} 115 | {{#let (test-resource this.text) as |test|}} 116 | {{test.value}} 117 | {{/let}} 118 | {{/if}} 119 | `); 120 | 121 | assert.equal(this.element.textContent.trim(), ''); 122 | assert.equal(active, 0, 'no active resources yet'); 123 | 124 | this.set('show', true); 125 | await settled(); 126 | 127 | assert.equal(this.element.textContent.trim(), 'hello'); 128 | assert.equal(active, 1, 'one active resource'); 129 | 130 | this.set('text', 'world'); 131 | await settled(); 132 | 133 | assert.equal(this.element.textContent.trim(), 'world'); 134 | assert.equal(active, 1, 'one active resource'); 135 | 136 | this.set('show', false); 137 | await settled(); 138 | 139 | assert.equal(this.element.textContent.trim(), ''); 140 | assert.equal(active, 0, 'resources deactivated'); 141 | }); 142 | 143 | test('resources are destroyed and recreated after each change if no update is present', async function (assert) { 144 | let resources = new Set(); 145 | 146 | this.owner.register( 147 | 'helper:test-resource', 148 | class extends Resource { 149 | @tracked value; 150 | 151 | setup() { 152 | resources.add(this); 153 | this.value = this.args.positional[0]; 154 | } 155 | } 156 | ); 157 | 158 | this.text = 'hello'; 159 | 160 | await render(hbs` 161 | {{#let (test-resource this.text) as |test|}} 162 | {{test.value}} 163 | {{/let}} 164 | `); 165 | 166 | assert.equal(this.element.textContent.trim(), 'hello'); 167 | assert.equal(resources.size, 1, 'one resource class created'); 168 | 169 | this.set('text', 'world'); 170 | await settled(); 171 | 172 | assert.equal(this.element.textContent.trim(), 'world'); 173 | assert.equal(resources.size, 2, 'two resource classes created'); 174 | }); 175 | 176 | test('resources can be passed named args', async function (assert) { 177 | this.owner.register( 178 | 'helper:test-resource', 179 | class extends Resource { 180 | @tracked value; 181 | 182 | setup() { 183 | this.value = this.args.named.text; 184 | } 185 | } 186 | ); 187 | 188 | await render(hbs` 189 | {{#let (test-resource text='hello') as |test|}} 190 | {{test.value}} 191 | {{/let}} 192 | `); 193 | 194 | assert.equal(this.element.textContent.trim(), 'hello'); 195 | }); 196 | 197 | test('resources can define an update hook', async function (assert) { 198 | let resources = new Set(); 199 | 200 | this.owner.register( 201 | 'helper:test-resource', 202 | class extends Resource { 203 | @tracked value; 204 | 205 | setup() { 206 | resources.add(this); 207 | this.value = this.args.positional[0]; 208 | } 209 | 210 | update() { 211 | this.value = this.args.positional[0]; 212 | } 213 | } 214 | ); 215 | 216 | this.text = 'hello'; 217 | 218 | await render(hbs` 219 | {{#let (test-resource this.text) as |test|}} 220 | {{test.value}} 221 | {{/let}} 222 | `); 223 | 224 | assert.equal(this.element.textContent.trim(), 'hello'); 225 | assert.equal(resources.size, 1, 'one resource class created'); 226 | 227 | this.set('text', 'world'); 228 | await settled(); 229 | 230 | assert.equal(this.element.textContent.trim(), 'world'); 231 | assert.equal(resources.size, 1, 'same resource class used to update'); 232 | }); 233 | 234 | test('resources can inject services', async function (assert) { 235 | let serviceInstance; 236 | 237 | this.owner.register( 238 | 'service:text', 239 | class extends Service { 240 | constructor() { 241 | super(...arguments); 242 | serviceInstance = this; 243 | } 244 | 245 | @tracked text = 'hello'; 246 | } 247 | ); 248 | 249 | this.owner.register( 250 | 'helper:test-resource', 251 | class extends Resource { 252 | @service text; 253 | } 254 | ); 255 | 256 | this.text = 'hello'; 257 | 258 | await render(hbs` 259 | {{#let (test-resource this.text) as |test|}} 260 | {{test.text.text}} 261 | {{/let}} 262 | `); 263 | 264 | assert.equal(this.element.textContent.trim(), 'hello'); 265 | 266 | serviceInstance.text = 'world'; 267 | await settled(); 268 | 269 | assert.equal(this.element.textContent.trim(), 'world'); 270 | }); 271 | 272 | test('value and lifecycle hooks are not entangled', async function (assert) { 273 | let resolve; 274 | 275 | class LoadData extends Resource { 276 | @tracked isLoading = true; 277 | 278 | setup() { 279 | assert.step('setup'); 280 | this.loadData(); 281 | } 282 | 283 | async loadData() { 284 | await new Promise((r) => { 285 | resolve = r; 286 | }); 287 | 288 | this.isLoading = false; 289 | } 290 | } 291 | 292 | this.owner.register('helper:load-data', LoadData); 293 | 294 | await render(hbs` 295 | {{#let (load-data) as |data|}} 296 | {{data.isLoading}} 297 | {{/let}} 298 | `) 299 | 300 | assert.equal(this.element.textContent.trim(), 'true', 'correct value returned'); 301 | assert.verifySteps(['setup'], 'setup was run'); 302 | 303 | resolve(); 304 | await settled(); 305 | 306 | assert.equal(this.element.textContent.trim(), 'false', 'correct value returned'); 307 | assert.verifySteps([], 'setup was not run again'); 308 | }); 309 | }); 310 | -------------------------------------------------------------------------------- /tests/integration/modifiers-test.js: -------------------------------------------------------------------------------- 1 | import { module, test } from 'qunit'; 2 | import { setupRenderingTest } from 'ember-qunit'; 3 | import { render, settled } from '@ember/test-helpers'; 4 | import Service, { inject as service } from '@ember/service'; 5 | import { hbs } from 'ember-cli-htmlbars'; 6 | import { tracked } from 'tracked-built-ins'; 7 | 8 | import { modifier, Modifier } from 'ember-could-get-used-to-this'; 9 | 10 | module('functional modifiers', (hooks) => { 11 | setupRenderingTest(hooks); 12 | 13 | test('functions can be used as modifiers', async function (assert) { 14 | this.owner.register( 15 | 'modifier:set-text', 16 | modifier((element, [text]) => (element.innerText = text)) 17 | ); 18 | 19 | await render(hbs``); 20 | 21 | assert.equal(this.element.textContent.trim(), 'hello'); 22 | }); 23 | 24 | test('functional modifiers update if args update', async function (assert) { 25 | this.owner.register( 26 | 'modifier:set-text', 27 | modifier((element, [text]) => (element.innerText = text)) 28 | ); 29 | 30 | this.text = 'hello'; 31 | 32 | await render(hbs``); 33 | 34 | assert.equal(this.element.textContent.trim(), 'hello'); 35 | 36 | this.set('text', 'world'); 37 | 38 | await settled(); 39 | 40 | assert.equal(this.element.textContent.trim(), 'world'); 41 | }); 42 | 43 | test('functional modifiers update if tracked state used within updates', async function (assert) { 44 | this.owner.register( 45 | 'modifier:set-text', 46 | modifier((element, [{ text }]) => (element.innerText = text)) 47 | ); 48 | 49 | this.value = tracked({ text: 'hello' }); 50 | 51 | await render(hbs``); 52 | 53 | assert.equal(this.element.textContent.trim(), 'hello'); 54 | 55 | this.value.text = 'world'; 56 | 57 | await settled(); 58 | 59 | assert.equal(this.element.textContent.trim(), 'world'); 60 | }); 61 | 62 | test('functional modifiers can return destructor', async function (assert) { 63 | let active = 0; 64 | 65 | this.owner.register( 66 | 'modifier:set-text', 67 | modifier((element, [text]) => { 68 | active++; 69 | element.innerText = text; 70 | 71 | return () => active--; 72 | }) 73 | ); 74 | 75 | this.text = 'hello'; 76 | 77 | await render( 78 | hbs`{{#if this.show}}{{/if}}` 79 | ); 80 | 81 | assert.equal(this.element.textContent.trim(), ''); 82 | assert.equal(active, 0, 'no active modifiers yet'); 83 | 84 | this.set('show', true); 85 | await settled(); 86 | 87 | assert.equal(this.element.textContent.trim(), 'hello'); 88 | assert.equal(active, 1, 'one active modifier'); 89 | 90 | this.set('text', 'world'); 91 | await settled(); 92 | 93 | assert.equal(this.element.textContent.trim(), 'world'); 94 | assert.equal(active, 1, 'one active modifier'); 95 | 96 | this.set('show', false); 97 | await settled(); 98 | 99 | assert.equal(this.element.textContent.trim(), ''); 100 | assert.equal(active, 0, 'modifiers deactivated'); 101 | }); 102 | 103 | test('functional modifiers can be passed named args', async function (assert) { 104 | let setText = modifier( 105 | (element, positional, { text }) => (element.innerText = text) 106 | ); 107 | this.owner.register('modifier:set-text', setText); 108 | 109 | await render(hbs``); 110 | 111 | assert.equal(this.element.textContent.trim(), 'hello'); 112 | }); 113 | }); 114 | 115 | module('class modifiers', (hooks) => { 116 | setupRenderingTest(hooks); 117 | 118 | test('classes can be used as modifiers', async function (assert) { 119 | this.owner.register( 120 | 'modifier:set-text', 121 | class extends Modifier { 122 | setup() { 123 | this.element.innerText = this.args.positional[0]; 124 | } 125 | } 126 | ); 127 | 128 | await render(hbs``); 129 | 130 | assert.equal(this.element.textContent.trim(), 'hello'); 131 | }); 132 | 133 | test('class modifiers update if args update', async function (assert) { 134 | this.owner.register( 135 | 'modifier:set-text', 136 | class extends Modifier { 137 | setup() { 138 | this.element.innerText = this.args.positional[0]; 139 | } 140 | } 141 | ); 142 | 143 | this.text = 'hello'; 144 | 145 | await render(hbs``); 146 | 147 | assert.equal(this.element.textContent.trim(), 'hello'); 148 | 149 | this.set('text', 'world'); 150 | 151 | await settled(); 152 | 153 | assert.equal(this.element.textContent.trim(), 'world'); 154 | }); 155 | 156 | test('class modifiers update if tracked state used within updates', async function (assert) { 157 | this.owner.register( 158 | 'modifier:set-text', 159 | class extends Modifier { 160 | setup() { 161 | this.element.innerText = this.args.positional[0].text; 162 | } 163 | } 164 | ); 165 | 166 | this.value = tracked({ text: 'hello' }); 167 | 168 | await render(hbs``); 169 | 170 | assert.equal(this.element.textContent.trim(), 'hello'); 171 | 172 | this.value.text = 'world'; 173 | 174 | await settled(); 175 | 176 | assert.equal(this.element.textContent.trim(), 'world'); 177 | }); 178 | 179 | test('class modifiers can teardown', async function (assert) { 180 | let active = 0; 181 | 182 | this.owner.register( 183 | 'modifier:set-text', 184 | class extends Modifier { 185 | setup() { 186 | active++; 187 | this.element.innerText = this.args.positional[0]; 188 | } 189 | 190 | teardown() { 191 | active--; 192 | } 193 | } 194 | ); 195 | 196 | this.text = 'hello'; 197 | 198 | await render( 199 | hbs`{{#if this.show}}{{/if}}` 200 | ); 201 | 202 | assert.equal(this.element.textContent.trim(), ''); 203 | assert.equal(active, 0, 'no active modifiers yet'); 204 | 205 | this.set('show', true); 206 | await settled(); 207 | 208 | assert.equal(this.element.textContent.trim(), 'hello'); 209 | assert.equal(active, 1, 'one active modifier'); 210 | 211 | this.set('text', 'world'); 212 | await settled(); 213 | 214 | assert.equal(this.element.textContent.trim(), 'world'); 215 | assert.equal(active, 1, 'one active modifier'); 216 | 217 | this.set('show', false); 218 | await settled(); 219 | 220 | assert.equal(this.element.textContent.trim(), ''); 221 | assert.equal(active, 0, 'modifiers deactivated'); 222 | }); 223 | 224 | test('class modifiers are destroyed and recreated after each change if no update is present', async function (assert) { 225 | let modifiers = new Set(); 226 | 227 | this.owner.register( 228 | 'modifier:set-text', 229 | class extends Modifier { 230 | setup() { 231 | modifiers.add(this); 232 | this.element.innerText = this.args.positional[0]; 233 | } 234 | } 235 | ); 236 | 237 | this.text = 'hello'; 238 | 239 | await render(hbs``); 240 | 241 | assert.equal(this.element.textContent.trim(), 'hello'); 242 | assert.equal(modifiers.size, 1, 'one modifier class created'); 243 | 244 | this.set('text', 'world'); 245 | await settled(); 246 | 247 | assert.equal(this.element.textContent.trim(), 'world'); 248 | assert.equal(modifiers.size, 2, 'two modifier classes created'); 249 | }); 250 | 251 | test('class modifiers can be passed named args', async function (assert) { 252 | this.owner.register( 253 | 'modifier:set-text', 254 | class extends Modifier { 255 | setup() { 256 | this.element.innerText = this.args.named.text; 257 | } 258 | } 259 | ); 260 | 261 | await render(hbs``); 262 | 263 | assert.equal(this.element.textContent.trim(), 'hello'); 264 | }); 265 | 266 | test('class modifiers can define an update hook', async function (assert) { 267 | let modifiers = new Set(); 268 | 269 | this.owner.register( 270 | 'modifier:set-text', 271 | class extends Modifier { 272 | setup() { 273 | modifiers.add(this); 274 | this.element.innerText = this.args.positional[0]; 275 | } 276 | 277 | update() { 278 | this.element.innerText = this.args.positional[0]; 279 | } 280 | } 281 | ); 282 | 283 | this.text = 'hello'; 284 | 285 | await render(hbs``); 286 | 287 | assert.equal(this.element.textContent.trim(), 'hello'); 288 | assert.equal(modifiers.size, 1, 'one modifier class created'); 289 | 290 | this.set('text', 'world'); 291 | await settled(); 292 | 293 | assert.equal(this.element.textContent.trim(), 'world'); 294 | assert.equal(modifiers.size, 1, 'same modifier class used to update'); 295 | }); 296 | 297 | test('class modifiers can inject services', async function (assert) { 298 | let serviceInstance; 299 | 300 | this.owner.register( 301 | 'service:text', 302 | class extends Service { 303 | constructor() { 304 | super(...arguments); 305 | serviceInstance = this; 306 | } 307 | 308 | @tracked text = 'hello';; 309 | } 310 | ) 311 | 312 | this.owner.register( 313 | 'modifier:set-text', 314 | class extends Modifier { 315 | @service text; 316 | 317 | setup() { 318 | this.element.innerText = this.text.text; 319 | } 320 | } 321 | ); 322 | 323 | this.text = 'hello'; 324 | 325 | await render(hbs``); 326 | 327 | assert.equal(this.element.textContent.trim(), 'hello'); 328 | 329 | serviceInstance.text = 'world'; 330 | await settled(); 331 | 332 | assert.equal(this.element.textContent.trim(), 'world'); 333 | }); 334 | }); 335 | --------------------------------------------------------------------------------