Skip to content

Commit

Permalink
Merge pull request #952 from NullVoxPopuli/modifiers
Browse files Browse the repository at this point in the history
Modifiers
  • Loading branch information
NullVoxPopuli authored Jul 20, 2023
2 parents 9213df6 + aa2c453 commit 4b1f708
Show file tree
Hide file tree
Showing 12 changed files with 1,893 additions and 635 deletions.
61 changes: 61 additions & 0 deletions .changeset/polite-tips-bow.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
---
"ember-resources": minor
---

Introduce resources as modifiers.
This brings alignment with Starbeam's plans for modifiers as a universal primitive.

In ember-resources, using modifiers as resources looks like this:

```js
import { resource } from 'ember-resources';
import { modifier } from 'ember-resources/modifier';

const wiggle = modifier((element, arg1, arg2, namedArgs) => {
return resource(({ on }) => {
let animation = element.animate([
{ transform: `translateX(${arg1}px)` },
{ transform: `translateX(-${arg2}px)` },
], {
duration: 100,
iterations: Infinity,
});

on.cleanup(() => animation.cancel());
});
});

<template>
<div {{wiggle 2 5 named="hello"}}>hello</div>
</template>
```

The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.

This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.

<details><summary>in Starbeam</summary>

```js
import { resource } from '@starbeam/universal';

function wiggle(element, arg1, arg2, namedArgs) {
return resource(({ on }) => {
let animation = element.animate([
{ transform: `translateX(${arg1}px)` },
{ transform: `translateX(-${arg2}px)` },
], {
duration: 100,
iterations: Infinity,
});

on.cleanup(() => animation.cancel());
});
}

<template>
<div {{wiggle 2 5 named="hello"}}>hello</div>
</template>
```

</details>
8 changes: 6 additions & 2 deletions ember-resources/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"./core/function-based": "./dist/core/function-based/index.js",
"./link": "./dist/link.js",
"./service": "./dist/service.js",
"./modifier": "./dist/modifier/index.js",
"./util": "./dist/util/index.js",
"./util/cell": "./dist/util/cell.js",
"./util/keep-latest": "./dist/util/keep-latest.js",
Expand All @@ -40,6 +41,9 @@
"service": [
"dist/service.d.ts"
],
"modifier": [
"dist/modifier/index.d.ts"
],
"util": [
"dist/util/index.d.ts"
],
Expand Down Expand Up @@ -102,7 +106,7 @@
"dependencies": {
"@babel/runtime": "^7.17.8",
"@embroider/addon-shim": "^1.2.0",
"@embroider/macros": "^1.2.0",
"@embroider/macros": "^1.12.3",
"ember-async-data": "^1.0.1"
},
"peerDependencies": {
Expand Down Expand Up @@ -134,7 +138,7 @@
"@babel/plugin-transform-typescript": "^7.18.4",
"@babel/preset-typescript": "^7.17.12",
"@ember/test-waiters": "^3.0.0",
"@embroider/addon-dev": "^3.0.0",
"@embroider/addon-dev": "^3.1.2",
"@glimmer/component": "^1.1.2",
"@glimmer/tracking": "^1.1.2",
"@glint/template": "^1.0.2",
Expand Down
18 changes: 0 additions & 18 deletions ember-resources/rollup.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,6 @@ export default defineConfig({
// addon. Anything not listed here may get optimized away.
addon.publicEntrypoints(['**/*.ts']),

// These are the modules that should get reexported into the traditional
// "app" tree. Things in here should also be in publicEntrypoints above, but
// not everything in publicEntrypoints necessarily needs to go here.
// addon.appReexports([]),

// This babel config should *not* apply presets or compile away ES modules.
// It exists only to provide development niceties for you, like automatic
// template colocation.
//
// By default, this will load the actual babel config from the file
// babel.config.json.
ts({
// can be changed to swc or other transpilers later
// but we need the ember plugins converted first
Expand Down Expand Up @@ -69,13 +58,6 @@ export default defineConfig({
// package names.
addon.dependencies(),

// Ensure that standalone .hbs files are properly integrated as Javascript.
// addon.hbs(),

// addons are allowed to contain imports of .css files, which we want rollup
// to leave alone and keep in the published output.
// addon.keepAssets(['**/*.css']),

// Start with a clean output directory before building
addon.clean(),

Expand Down
22 changes: 0 additions & 22 deletions ember-resources/src/core/function-based/immediate-invocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,6 @@ class ResourceInvokerManager {
getDestroyable({ cache }: State) {
return cache;
}

// createHelper(fn: AnyFunction, args: Arguments): State {
// return { fn, args };
// }

// getValue({ fn, args }: State): unknown {
// if (Object.keys(args.named).length > 0) {
// let argsForFn: FnArgs<Arguments> = [...args.positional, args.named];

// return fn(...argsForFn);
// }

// return fn(...args.positional);
// }

// getDebugName(fn: AnyFunction): string {
// if (fn.name) {
// return `(helper function ${fn.name})`;
// }

// return '(anonymous helper function)';
// }
}

/**
Expand Down
6 changes: 6 additions & 0 deletions ember-resources/src/core/types/signature-args.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,12 @@ export type ArgsFor<S> =
}
: { Named: EmptyObject; Positional: [] };

export type ElementFor<S> = 'Element' extends keyof S
? S['Element'] extends Element
? S['Element']
: Element
: Element;

/**
* Converts a variety of types to the expanded arguments type
* that aligns with the 'Args' portion of the 'Signature' types
Expand Down
227 changes: 227 additions & 0 deletions ember-resources/src/modifier/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,227 @@
import { assert } from '@ember/debug';
// @ts-expect-error
import { setModifierManager } from '@ember/modifier';

import { resourceFactory } from '../index';
import FunctionBasedModifierManager from './manager';

import type { resource } from '../index';
import type { ArgsFor, ElementFor, EmptyObject } from '[core-types]';
import type { ModifierLike } from '@glint/template';

// Provide a singleton manager.
const MANAGER = new FunctionBasedModifierManager();

type PositionalArgs<S> = S extends { Args?: object } ? ArgsFor<S['Args']>['Positional'] : [];
type NamedArgs<S> = S extends { Args?: object }
? ArgsFor<S['Args']>['Named'] extends object
? ArgsFor<S['Args']>['Named']
: EmptyObject
: EmptyObject;

type ArgsForFn<S> = S extends { Args?: object }
? ArgsFor<S['Args']>['Named'] extends EmptyObject
? [...PositionalArgs<S>]
: [...PositionalArgs<S>, NamedArgs<S>]
: [];

/**
* A resource-based API for building modifiers.
*
* You can attach this to an element, and use a `resource` to manage
* the state, add event listeners, remove event listeners on cleanup, etc.
*
* Using resources for modifiers provides a clear and concise API with
* easy to read concerns.
*
*
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
*
* ```js
* import { resource } from 'ember-resources';
* import { modifier } from 'ember-resources/modifier';
*
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
* return resource(({ on }) => {
* let animation = element.animate([
* { transform: `translateX(${arg1}px)` },
* { transform: `translateX(-${arg2}px)` },
* ], {
* duration: 100,
* iterations: Infinity,
* });
*
* on.cleanup(() => animation.cancel());
* });
* });
*
* <template>
* <div {{wiggle 2 5 named="hello"}}>hello</div>
* </template>
* ```
*
*/
export function modifier<El extends Element, Args extends unknown[] = unknown[]>(
fn: (element: El, ...args: Args) => void
): ModifierLike<{
Element: El;
Args: {
Named: EmptyObject;
Positional: Args;
};
}>;

/**
* A resource-based API for building modifiers.
*
* You can attach this to an element, and use a `resource` to manage
* the state, add event listeners, remove event listeners on cleanup, etc.
*
* Using resources for modifiers provides a clear and concise API with
* easy to read concerns.
*
*
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
*
* ```js
* import { resource } from 'ember-resources';
* import { modifier } from 'ember-resources/modifier';
*
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
* return resource(({ on }) => {
* let animation = element.animate([
* { transform: `translateX(${arg1}px)` },
* { transform: `translateX(-${arg2}px)` },
* ], {
* duration: 100,
* iterations: Infinity,
* });
*
* on.cleanup(() => animation.cancel());
* });
* });
*
* <template>
* <div {{wiggle 2 5 named="hello"}}>hello</div>
* </template>
* ```
*
*/
export function modifier<S extends { Element?: Element }>(
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
): ModifierLike<S>;
/**
* A resource-based API for building modifiers.
*
* You can attach this to an element, and use a `resource` to manage
* the state, add event listeners, remove event listeners on cleanup, etc.
*
* Using resources for modifiers provides a clear and concise API with
* easy to read concerns.
*
*
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
*
* ```js
* import { resource } from 'ember-resources';
* import { modifier } from 'ember-resources/modifier';
*
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
* return resource(({ on }) => {
* let animation = element.animate([
* { transform: `translateX(${arg1}px)` },
* { transform: `translateX(-${arg2}px)` },
* ], {
* duration: 100,
* iterations: Infinity,
* });
*
* on.cleanup(() => animation.cancel());
* });
* });
*
* <template>
* <div {{wiggle 2 5 named="hello"}}>hello</div>
* </template>
* ```
*
*/
export function modifier<S extends { Args?: object }>(
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
): ModifierLike<S>;
/**
* A resource-based API for building modifiers.
*
* You can attach this to an element, and use a `resource` to manage
* the state, add event listeners, remove event listeners on cleanup, etc.
*
* Using resources for modifiers provides a clear and concise API with
* easy to read concerns.
*
*
* The signature for the modifier here is _different_ from `ember-modifier`, where positional args and named args are grouped together into an array and object respectively.
* This signature for ember-resource's `modifier` follows the [plain function invocation](https://blog.emberjs.com/plain-old-functions-as-helpers/) signature.
*
* ```js
* import { resource } from 'ember-resources';
* import { modifier } from 'ember-resources/modifier';
*
* const wiggle = modifier((element, arg1, arg2, namedArgs) => {
* return resource(({ on }) => {
* let animation = element.animate([
* { transform: `translateX(${arg1}px)` },
* { transform: `translateX(-${arg2}px)` },
* ], {
* duration: 100,
* iterations: Infinity,
* });
*
* on.cleanup(() => animation.cancel());
* });
* });
*
* <template>
* <div {{wiggle 2 5 named="hello"}}>hello</div>
* </template>
* ```
*
*/
export function modifier<S extends { Element?: Element; Args?: object }>(
fn: (element: ElementFor<S>, ...args: ArgsForFn<S>) => ReturnType<typeof resource>
): ModifierLike<S>;

export function modifier(fn: (element: Element, ...args: unknown[]) => void): ModifierLike<{
Element: Element;
Args: {
Named: {};
Positional: [];
};
}> {
assert(`modifier() must be invoked with a function`, typeof fn === 'function');
setModifierManager(() => MANAGER, fn);
resourceFactory(fn);

return fn as unknown as ModifierLike<{
Element: Element;
Args: {
Named: {};
Positional: [];
};
}>;
}

/**
* @internal
*/
export type FunctionBasedModifierDefinition<S> = (
element: ElementFor<S>,
positional: PositionalArgs<S>,
named: NamedArgs<S>
) => void;
Loading

0 comments on commit 4b1f708

Please sign in to comment.