Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[BUGFIX beta] Use args proxy for modifier managers. #19163

Merged
merged 2 commits into from
Sep 28, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 13 additions & 114 deletions packages/@ember/-internals/glimmer/lib/component-managers/custom.ts
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -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
Loading