Skip to content

Commit

Permalink
Merge pull request #19163 from emberjs/modifier-args-proxy
Browse files Browse the repository at this point in the history
  • Loading branch information
rwjblue committed Sep 28, 2020
2 parents 984e69c + d7f56c4 commit aeef923
Show file tree
Hide file tree
Showing 6 changed files with 721 additions and 340 deletions.
127 changes: 13 additions & 114 deletions packages/@ember/-internals/glimmer/lib/component-managers/custom.ts
@@ -1,13 +1,9 @@
import { ENV } from '@ember/-internals/environment';
import { CUSTOM_TAG_FOR } from '@ember/-internals/metal';
import { Factory } from '@ember/-internals/owner';
import { HAS_NATIVE_PROXY } from '@ember/-internals/utils';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import {
Arguments,
Bounds,
CapturedArguments,
CapturedNamedArguments,
ComponentCapabilities,
ComponentDefinition,
Destroyable,
Expand All @@ -16,13 +12,13 @@ import {
VMArguments,
WithStaticLayout,
} from '@glimmer/interfaces';
import { createConstRef, Reference, valueForRef } from '@glimmer/reference';
import { registerDestructor, reifyPositional } from '@glimmer/runtime';
import { createConstRef, Reference } from '@glimmer/reference';
import { registerDestructor } from '@glimmer/runtime';
import { unwrapTemplate } from '@glimmer/util';
import { Tag, track } from '@glimmer/validator';
import { EmberVMEnvironment } from '../environment';
import RuntimeResolver from '../resolver';
import { OwnedTemplate } from '../template';
import { argsProxyFor } from '../utils/args-proxy';
import AbstractComponentManager from './abstract';

const CAPABILITIES = {
Expand Down Expand Up @@ -149,94 +145,6 @@ export interface ComponentArguments {
named: Dict<unknown>;
}

function tagForNamedArg<NamedArgs extends CapturedNamedArguments, K extends keyof NamedArgs>(
namedArgs: NamedArgs,
key: K
): Tag {
return track(() => valueForRef(namedArgs[key]));
}

let namedArgsProxyFor: (namedArgs: CapturedNamedArguments, debugName?: string) => Args['named'];

if (HAS_NATIVE_PROXY) {
namedArgsProxyFor = <NamedArgs extends CapturedNamedArguments>(
namedArgs: NamedArgs,
debugName?: string
) => {
let getTag = (key: keyof Args) => tagForNamedArg(namedArgs, key);

let handler: ProxyHandler<{}> = {
get(_target, prop) {
let ref = namedArgs[prop as string];

if (ref !== undefined) {
return valueForRef(ref);
} else if (prop === CUSTOM_TAG_FOR) {
return getTag;
}
},

has(_target, prop) {
return namedArgs[prop as string] !== undefined;
},

ownKeys(_target) {
return Object.keys(namedArgs);
},

getOwnPropertyDescriptor(_target, prop) {
assert(
'args proxies do not have real property descriptors, so you should never need to call getOwnPropertyDescriptor yourself. This code exists for enumerability, such as in for-in loops and Object.keys()',
namedArgs[prop as string] !== undefined
);

return {
enumerable: true,
configurable: true,
};
},
};

if (DEBUG) {
handler.set = function(_target, prop) {
assert(
`You attempted to set ${debugName}#${String(
prop
)} on a components arguments. Component arguments are immutable and cannot be updated directly, they always represent the values that are passed to your component. If you want to set default values, you should use a getter instead`
);

return false;
};
}

return new Proxy({}, handler);
};
} else {
namedArgsProxyFor = <NamedArgs extends CapturedNamedArguments>(namedArgs: NamedArgs) => {
let getTag = (key: keyof Args) => tagForNamedArg(namedArgs, key);

let proxy = {};

Object.defineProperty(proxy, CUSTOM_TAG_FOR, {
configurable: false,
enumerable: false,
value: getTag,
});

Object.keys(namedArgs).forEach(name => {
Object.defineProperty(proxy, name, {
enumerable: true,
configurable: true,
get() {
return valueForRef(namedArgs[name]);
},
});
});

return proxy;
};
}

/**
The CustomComponentManager allows addons to provide custom component
implementations that integrate seamlessly into Ember. This is accomplished
Expand Down Expand Up @@ -276,25 +184,20 @@ export default class CustomComponentManager<ComponentInstance>
create(
env: EmberVMEnvironment,
definition: CustomComponentDefinitionState<ComponentInstance>,
args: VMArguments
vmArgs: VMArguments
): CustomComponentState<ComponentInstance> {
let { delegate } = definition;
let capturedArgs = args.capture();
let { named, positional } = capturedArgs;
let namedArgsProxy = namedArgsProxyFor(named);
let args = argsProxyFor(vmArgs.capture(), 'component');

let component = delegate.createComponent(definition.ComponentClass.class, {
named: namedArgsProxy,
positional: reifyPositional(positional),
});
let component = delegate.createComponent(definition.ComponentClass.class, args);

let bucket = new CustomComponentState(delegate, component, capturedArgs, env, namedArgsProxy);
let bucket = new CustomComponentState(delegate, component, args, env);

if (ENV._DEBUG_RENDER_TREE) {
env.extra.debugRenderTree.create(bucket, {
type: 'component',
name: definition.name,
args: args.capture(),
args: vmArgs.capture(),
instance: component,
template: definition.template,
});
Expand All @@ -317,12 +220,9 @@ export default class CustomComponentManager<ComponentInstance>
}

if (hasUpdateHook(bucket.delegate)) {
let { delegate, component, args, namedArgsProxy } = bucket;
let { delegate, component, args } = bucket;

delegate.updateComponent(component, {
named: namedArgsProxy,
positional: reifyPositional(args.positional),
});
delegate.updateComponent(component, args);
}
}

Expand Down Expand Up @@ -383,9 +283,8 @@ export class CustomComponentState<ComponentInstance> {
constructor(
public delegate: ManagerDelegate<ComponentInstance>,
public component: ComponentInstance,
public args: CapturedArguments,
public env: EmberVMEnvironment,
public namedArgsProxy: Args['named']
public args: Arguments,
public env: EmberVMEnvironment
) {
if (hasDestructors(delegate)) {
registerDestructor(this, () => delegate.destroyComponent(component));
Expand Down
112 changes: 69 additions & 43 deletions packages/@ember/-internals/glimmer/lib/modifiers/custom.ts
@@ -1,10 +1,11 @@
import { Factory } from '@ember/-internals/owner';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import { CapturedArguments, Dict, ModifierManager, VMArguments } from '@glimmer/interfaces';
import { Arguments, ModifierManager, VMArguments } from '@glimmer/interfaces';
import { registerDestructor, reifyArgs } from '@glimmer/runtime';
import { createUpdatableTag, untrack } from '@glimmer/validator';
import { createUpdatableTag, untrack, UpdatableTag } from '@glimmer/validator';
import { SimpleElement } from '@simple-dom/interface';
import { argsProxyFor } from '../utils/args-proxy';

export interface CustomModifierDefinitionState<ModifierInstance> {
ModifierClass: Factory<ModifierInstance>;
Expand All @@ -13,21 +14,33 @@ export interface CustomModifierDefinitionState<ModifierInstance> {
}

export interface OptionalCapabilities {
disableAutoTracking?: boolean;
'3.13': {
disableAutoTracking?: boolean;
};

// uses args proxy, does not provide a way to opt-out
'3.22': {
disableAutoTracking?: boolean;
};
}

export interface Capabilities {
disableAutoTracking: boolean;
useArgsProxy: boolean;
}

export function capabilities(
managerAPI: string,
optionalFeatures: OptionalCapabilities = {}
export function capabilities<Version extends keyof OptionalCapabilities>(
managerAPI: Version,
optionalFeatures: OptionalCapabilities[Version] = {}
): Capabilities {
assert('Invalid modifier manager compatibility specified', managerAPI === '3.13');
assert(
'Invalid modifier manager compatibility specified',
managerAPI === '3.13' || managerAPI === '3.22'
);

return {
disableAutoTracking: Boolean(optionalFeatures.disableAutoTracking),
useArgsProxy: managerAPI === '3.13' ? false : true,
};
}

Expand All @@ -53,32 +66,21 @@ export class CustomModifierDefinition<ModifierInstance> {
}
}

export class CustomModifierState<ModifierInstance> {
public tag = createUpdatableTag();
export interface CustomModifierState<ModifierInstance> {
tag: UpdatableTag;
element: SimpleElement;
delegate: ModifierManagerDelegate<ModifierInstance>;
modifier: ModifierInstance;
args: Arguments;
debugName?: string;

constructor(
public element: SimpleElement,
public delegate: ModifierManagerDelegate<ModifierInstance>,
public modifier: ModifierInstance,
public args: CapturedArguments
) {
registerDestructor(this, () => delegate.destroyModifier(modifier, reifyArgs(args)));
}
}

// TODO: export ICapturedArgumentsValue from glimmer and replace this
export interface Args {
named: Dict<unknown>;
positional: unknown[];
}

export interface ModifierManagerDelegate<ModifierInstance> {
capabilities: Capabilities;
createModifier(factory: 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(factory: unknown, args: Arguments): ModifierInstance;
installModifier(instance: ModifierInstance, element: SimpleElement, args: Arguments): void;
updateModifier(instance: ModifierInstance, args: Arguments): void;
destroyModifier(instance: ModifierInstance, args: Arguments): void;
}

/**
Expand Down Expand Up @@ -114,18 +116,49 @@ class InteractiveCustomModifierManager<ModifierInstance>
create(
element: SimpleElement,
definition: CustomModifierDefinitionState<ModifierInstance>,
args: VMArguments
vmArgs: VMArguments
) {
let { delegate, ModifierClass } = definition;
const capturedArgs = args.capture();
let capturedArgs = vmArgs.capture();

assert(
'Custom modifier managers must define their capabilities using the capabilities() helper function',
typeof delegate.capabilities === 'object' && delegate.capabilities !== null
);

let instance = definition.delegate.createModifier(ModifierClass, reifyArgs(capturedArgs));
let state = new CustomModifierState(element, delegate, instance, capturedArgs);
let useArgsProxy = delegate.capabilities.useArgsProxy;

let args = useArgsProxy ? argsProxyFor(capturedArgs, 'modifier') : reifyArgs(capturedArgs);
let instance = delegate.createModifier(ModifierClass, args);

let tag = createUpdatableTag();
let state: CustomModifierState<ModifierInstance>;
if (useArgsProxy) {
state = {
tag,
element,
delegate,
args,
modifier: instance,
};
} else {
state = {
tag,
element,
delegate,
modifier: instance,
get args() {
return reifyArgs(capturedArgs);
},
};
}

if (DEBUG) {
state.debugName = definition.name;
}

registerDestructor(state, () => delegate.destroyModifier(instance, state.args));

return state;
}

Expand All @@ -140,30 +173,23 @@ class InteractiveCustomModifierManager<ModifierInstance>
install(state: CustomModifierState<ModifierInstance>) {
let { element, args, delegate, modifier } = state;

assert(
'Custom modifier managers must define their capabilities using the capabilities() helper function',
typeof delegate.capabilities === 'object' && delegate.capabilities !== null
);

let { capabilities } = delegate;
let argsValue = reifyArgs(args);

if (capabilities.disableAutoTracking === true) {
untrack(() => delegate.installModifier(modifier, element, argsValue));
untrack(() => delegate.installModifier(modifier, element, args));
} else {
delegate.installModifier(modifier, element, argsValue);
delegate.installModifier(modifier, element, args);
}
}

update(state: CustomModifierState<ModifierInstance>) {
let { args, delegate, modifier } = state;
let { capabilities } = delegate;
let argsValue = reifyArgs(args);

if (capabilities.disableAutoTracking === true) {
untrack(() => delegate.updateModifier(modifier, argsValue));
untrack(() => delegate.updateModifier(modifier, args));
} else {
delegate.updateModifier(modifier, argsValue);
delegate.updateModifier(modifier, args);
}
}

Expand Down

0 comments on commit aeef923

Please sign in to comment.