Skip to content
This repository has been archived by the owner on Oct 29, 2024. It is now read-only.

Commit

Permalink
[FEAT] Modifier Managers
Browse files Browse the repository at this point in the history
Adds modifier managers that match Ember's modifier manager API. They can
be defined by importing `setModifierManager` from `@glimmer/core`:

```ts
import {
  modifierCapabilities,
  setModifierManager,
} from '@glimmer/core';
```

Also allows users to use simple functions as modifiers:

```js
function scroll(element, position) {
  element.scrollY = position;

  return () => element.scrollY = 0;
}
```

The function receives the element as the first argument, and any
positional arguments after that. It can return a destructor, which will
be called whenever the modifier is destroyed, or before it is updated
and the modifier is run again.

Also adds a `TrackedObject` implementation for tests, which makes life
for testing a bit easier.
  • Loading branch information
Chris Garrett committed Mar 27, 2020
1 parent 3c4efc5 commit 44e0323
Show file tree
Hide file tree
Showing 20 changed files with 774 additions and 144 deletions.
6 changes: 5 additions & 1 deletion .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,11 @@ module.exports = {
},
// source Js
{
files: ['src/**/*.js'],
files: ['**/src/**/*.js', '**/test/**/*.js'],
env: {
es2020: true,
browser: true,
},
parserOptions: {
sourceType: 'module',
},
Expand Down
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,6 @@
"**/bower_components": true,
"dist": true
},
"typescript.tsdk": "node_modules/typescript/lib"
"typescript.tsdk": "node_modules/typescript/lib",
"eslint.enable": true
}
7 changes: 7 additions & 0 deletions packages/@glimmer/core/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,13 @@ export {

export { Args as CapturedArgs } from './src/interfaces';

export {
ModifierManager,
ModifierDefinition,
capabilities as modifierCapabilities,
Capabilities as ModifierCapabilities,
} from './src/managers/modifier';

export {
HelperManager,
HelperDefinition,
Expand Down
4 changes: 2 additions & 2 deletions packages/@glimmer/core/src/managers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function getManagerInstanceForOwner<D extends ManagerDelegate>(
///////////

export function setModifierManager<StateBucket>(
factory: ManagerFactory<ModifierManager<unknown>>,
factory: ManagerFactory<ModifierManager<StateBucket>>,
definition: ModifierDefinition<StateBucket>
): {} {
return setManager({ factory, type: 'modifier' }, definition);
Expand All @@ -102,7 +102,7 @@ export function setModifierManager<StateBucket>(
export function getModifierManager<StateBucket = unknown>(
owner: object,
definition: ModifierDefinition<StateBucket>
): ModifierManager<unknown> | undefined {
): ModifierManager<StateBucket> | undefined {
const wrapper = getManager(definition);

if (wrapper !== undefined && wrapper.type === 'modifier') {
Expand Down
168 changes: 161 additions & 7 deletions packages/@glimmer/core/src/managers/modifier.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,20 @@
import { assert } from '@glimmer/util';
import { DEBUG } from '@glimmer/env';
import {
ModifierManager as VMModifierManager,
VMArguments,
CapturedArguments,
Destroyable,
DynamicScope,
} from '@glimmer/interfaces';
import { Tag, createUpdatableTag, track, untrack, combine, update } from '@glimmer/validator';
import { assert, debugToString } from '@glimmer/util';
import { SimpleElement } from '@simple-dom/interface';
import { Args } from '../interfaces';
import debugRenderMessage from '../utils/debug';
import { argsProxyFor } from './util';
import { getModifierManager } from '.';
import { OWNER_KEY } from '../owner';
import { VMModifierDefinitionWithHandle } from '../render-component/vm-definitions';

///////////

Expand All @@ -10,7 +24,7 @@ export interface Capabilities {

export type OptionalCapabilities = Partial<Capabilities>;

export type ManagerAPIVersion = '3.4' | '3.13';
export type ManagerAPIVersion = '3.13';

export function capabilities(
managerAPI: ManagerAPIVersion,
Expand All @@ -25,12 +39,152 @@ export function capabilities(

///////////

export interface ModifierManager<ModifierInstance> {
export interface ModifierManager<ModifierStateBucket> {
capabilities: Capabilities;
createModifier(definition: unknown, args: Args): ModifierInstance;
installModifier(instance: ModifierInstance, element: SimpleElement, args: Args): void;
updateModifier(instance: ModifierInstance, args: Args): void;
destroyModifier(instance: ModifierInstance, args: Args): void;
createModifier(definition: unknown, args: Args): ModifierStateBucket;
installModifier(instance: ModifierStateBucket, element: Element, args: Args): void;
updateModifier(instance: ModifierStateBucket, args: Args): void;
destroyModifier(instance: ModifierStateBucket, args: Args): void;
}

export type ModifierDefinition<_Instance = unknown> = {};

export type SimpleModifier = (element: Element, ...args: unknown[]) => undefined | (() => void);

interface SimpleModifierStateBucket {
definition: SimpleModifier;
destructor?(): void;
element?: Element;
}

class SimpleModifierManager implements ModifierManager<SimpleModifierStateBucket> {
capabilities = capabilities('3.13');

createModifier(definition: SimpleModifier, args: Args): SimpleModifierStateBucket {
if (DEBUG) {
assert(Object.keys(args.named).length === 0, `You used named arguments with the ${definition.name} modifier, but it is a standard function. Normal functions cannot receive named arguments when used as modifiers.`);
}

return { definition };
}

installModifier(bucket: SimpleModifierStateBucket, element: Element, args: Args): void {
bucket.destructor = bucket.definition(element, ...args.positional);
bucket.element = element;
}

updateModifier(bucket: SimpleModifierStateBucket, args: Args): void {
this.destroyModifier(bucket);
this.installModifier(bucket, bucket.element!, args);
}

destroyModifier(bucket: SimpleModifierStateBucket): void {
const { destructor } = bucket;

if (destructor !== undefined) {
destructor();
}
}
}

const SIMPLE_MODIFIER_MANAGER = new SimpleModifierManager();

///////////

export class CustomModifierState<ModifierStateBucket> {
public tag = createUpdatableTag();

constructor(
public element: SimpleElement,
public delegate: ModifierManager<ModifierStateBucket>,
public modifier: ModifierStateBucket,
public argsProxy: Args,
public capturedArgs: CapturedArguments
) {}

destroy(): void {
const { delegate, modifier, argsProxy } = this;
delegate.destroyModifier(modifier, argsProxy);
}
}

export class CustomModifierManager<ModifierStateBucket>
implements
VMModifierManager<
CustomModifierState<ModifierStateBucket>,
ModifierDefinition<ModifierStateBucket>
> {
create(
element: SimpleElement,
definition: ModifierDefinition<ModifierStateBucket>,
args: VMArguments,
dynamicScope: DynamicScope
): CustomModifierState<ModifierStateBucket> {
const owner = dynamicScope.get(OWNER_KEY).value() as object;
let delegate = getModifierManager(owner, definition);

if (delegate === undefined) {
if (DEBUG) {
assert(
typeof definition === 'function',
`No modifier manager found for ${definition}, and it was not a plain function, so it could not be used as a modifier`
);
}

delegate = (SIMPLE_MODIFIER_MANAGER as unknown) as ModifierManager<ModifierStateBucket>;
}

const capturedArgs = args.capture();
const argsProxy = argsProxyFor(capturedArgs, 'modifier');

const instance = delegate.createModifier(definition, argsProxy);

return new CustomModifierState(element, delegate, instance, argsProxy, capturedArgs);
}

getTag({ tag, capturedArgs }: CustomModifierState<ModifierStateBucket>): Tag {
return combine([tag, capturedArgs.tag]);
}

install(state: CustomModifierState<ModifierStateBucket>): void {
const { element, argsProxy, delegate, modifier, tag } = state;

if (delegate.capabilities.disableAutoTracking === true) {
untrack(() => delegate.installModifier(modifier, element as Element, argsProxy));
} else {
const combinedTrackingTag = track(
() => delegate.installModifier(modifier, element as Element, argsProxy),
DEBUG && debugRenderMessage!(`(instance of a \`${debugToString!(modifier)}\` modifier)`)
);

update(tag, combinedTrackingTag);
}
}

update(state: CustomModifierState<ModifierStateBucket>): void {
const { argsProxy, delegate, modifier, tag } = state;

if (delegate.capabilities.disableAutoTracking === true) {
untrack(() => delegate.updateModifier(modifier, argsProxy));
} else {
const combinedTrackingTag = track(
() => delegate.updateModifier(modifier, argsProxy),
DEBUG && debugRenderMessage!(`(instance of a \`${debugToString!(modifier)}\` modifier)`)
);
update(tag, combinedTrackingTag);
}
}

getDestructor(state: CustomModifierState<ModifierStateBucket>): Destroyable {
return state;
}
}

export const CUSTOM_MODIFIER_MANAGER = new CustomModifierManager();

export class VMCustomModifierDefinition<ModifierStateBucket>
implements VMModifierDefinitionWithHandle {
public manager = CUSTOM_MODIFIER_MANAGER;

constructor(public handle: number, public state: ModifierDefinition<ModifierStateBucket>) {}
}
4 changes: 3 additions & 1 deletion packages/@glimmer/core/src/render-component/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,8 +116,10 @@ const context = JitContext(new CompileTimeResolver(resolver));
const program = new RuntimeProgramImpl(context.program.constants, context.program.heap);

function dictToReference(dict: Dict<unknown>, env: Environment): Dict<PathReference> {
const root = new ComponentRootReference(dict, env);

return Object.keys(dict).reduce((acc, key) => {
acc[key] = new ComponentRootReference(dict[key], env);
acc[key] = root.get(key);
return acc;
}, {} as Dict<PathReference>);
}
Expand Down
13 changes: 8 additions & 5 deletions packages/@glimmer/core/src/render-component/resolvers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,8 @@ import { ResolverDelegate, unwrapTemplate } from '@glimmer/opcode-compiler';
import {
vmDefinitionForComponent,
vmDefinitionForHelper,
vmDefinitionForModifier,
vmDefinitionForBuiltInHelper,
Modifier,
vmHandleForModifier,
VMHelperDefinition,
} from './vm-definitions';

Expand All @@ -22,6 +21,7 @@ import { TemplateMeta } from '../template';
import { HelperDefinition } from '../managers/helper';
import { ifHelper } from './built-ins';
import { DEBUG } from '@glimmer/env';
import { ModifierDefinition } from '../managers/modifier';

const builtInHelpers: { [key: string]: VMHelperDefinition } = {
if: vmDefinitionForBuiltInHelper(ifHelper),
Expand Down Expand Up @@ -92,10 +92,13 @@ export class CompileTimeResolver implements ResolverDelegate {

lookupModifier(name: string, referrer: TemplateMeta): Option<number> {
const scope = referrer.scope();
const modifier = scope[name] as Modifier;
const modifier = scope[name] as ModifierDefinition;

const definition = vmDefinitionForModifier(modifier);
const { handle } = definition;

this.inner.registry[handle] = definition;

const handle = vmHandleForModifier(modifier);
this.inner.registry[handle] = modifier;
return handle;
}

Expand Down
30 changes: 20 additions & 10 deletions packages/@glimmer/core/src/render-component/vm-definitions.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {
ComponentDefinition as VMComponentDefinition,
ModifierDefinition as VMModifierDefinition,
Helper as VMHelperFactory,
ModifierManager,
TemplateOk,
Expand All @@ -14,12 +15,18 @@ import {
TemplateOnlyComponent,
} from '../managers/component/template-only';
import { DEBUG } from '@glimmer/env';
import { ModifierDefinition, VMCustomModifierDefinition } from '../managers/modifier';

export interface VMComponentDefinitionWithHandle extends VMComponentDefinition {
handle: number;
template: TemplateOk<TemplateMeta>;
}

export interface VMModifierDefinitionWithHandle extends VMModifierDefinition {
handle: number;
}


export interface VMHelperDefinition {
helper: VMHelperFactory;
handle: number;
Expand All @@ -36,7 +43,7 @@ let HANDLE = 0;

const VM_COMPONENT_DEFINITIONS = new WeakMap<ComponentDefinition, VMComponentDefinitionWithHandle>();
const VM_HELPER_DEFINITIONS = new WeakMap<HelperDefinition, VMHelperDefinition>();
const VM_MODIFIER_HANDLES = new WeakMap<Modifier, number>();
const VM_MODIFIER_DEFINITIONS = new WeakMap<ModifierDefinition, VMModifierDefinitionWithHandle>();

export function vmDefinitionForComponent(
ComponentDefinition: ComponentDefinition
Expand All @@ -48,15 +55,8 @@ export function vmDefinitionForHelper(Helper: HelperDefinition): VMHelperDefinit
return VM_HELPER_DEFINITIONS.get(Helper) || createVMHelperDefinition(Helper);
}

export function vmHandleForModifier(modifier: Modifier): number {
let handle = VM_MODIFIER_HANDLES.get(modifier);

if (!handle) {
handle = HANDLE++;
VM_MODIFIER_HANDLES.set(modifier, handle);
}

return handle;
export function vmDefinitionForModifier(Modifier: ModifierDefinition): VMModifierDefinitionWithHandle {
return VM_MODIFIER_DEFINITIONS.get(Modifier) || createVMModifierDefinition(Modifier);
}

///////////
Expand Down Expand Up @@ -115,3 +115,13 @@ function createVMHelperDefinition(userDefinition: HelperDefinition): VMHelperDef
VM_HELPER_DEFINITIONS.set(userDefinition, definition);
return definition;
}

function createVMModifierDefinition(
Modifier: ModifierDefinition
): VMModifierDefinitionWithHandle {
const definition = new VMCustomModifierDefinition(HANDLE++, Modifier);

VM_MODIFIER_DEFINITIONS.set(Modifier, definition);

return definition;
}
11 changes: 11 additions & 0 deletions packages/@glimmer/core/src/utils/debug.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { DEBUG } from '@glimmer/env';

let debugRenderMessage: undefined | ((renderingStack: string) => string);

if (DEBUG) {
debugRenderMessage = (renderingStack: string): string => {
return `While rendering:\n----------------\n${renderingStack.replace(/^/gm, ' ')}`;
};
}

export default debugRenderMessage;
Loading

0 comments on commit 44e0323

Please sign in to comment.