├── 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 | 
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 |
--------------------------------------------------------------------------------