Skip to content

Commit

Permalink
[FEAT] Implements Helper Managers
Browse files Browse the repository at this point in the history
Implements the helper manager feature specified in RFC 625. Highlights:

1. Generalizes the args proxy for use in both components and helpers.
   This does change the semantics slightly of `positional` arguments for
   component managers, notably it would make it so using computed
   properties with them would require a bit more work from custom
   component managers. The new semantics are more inline with the future
   of Ember, but if there many custom managers that use positional args
   we may want to reconsider this change.

2. Adds `getDebugName` to the interface for helper managers. This is an
   optional hook that is used for better logging purposes, and matches
   other internal APIs we've added recently.

3. `hasScheduledEffect` has not yet been implemented, and attempting to
   use it will cause an assertion to be thrown.

Helper managers are not exposed with this PR, and the version passed to
`helperCapabilities` is optimistic, but can be changed when we do expose
them (along with an appropriate feature flag).
  • Loading branch information
Chris Garrett committed Sep 28, 2020
1 parent 984e69c commit ff4dd17
Show file tree
Hide file tree
Showing 11 changed files with 720 additions and 205 deletions.
8 changes: 2 additions & 6 deletions packages/@ember/-internals/glimmer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -403,14 +403,10 @@ export { default as AbstractComponentManager } from './lib/component-managers/ab
export { INVOKE } from './lib/helpers/action';
export { default as OutletView } from './lib/views/outlet';
export { OutletState } from './lib/utils/outlet';
export { setComponentManager, setModifierManager, setHelperManager } from './lib/utils/managers';
export { capabilities } from './lib/component-managers/custom';
export {
setComponentManager,
getComponentManager,
setModifierManager,
getModifierManager,
} from './lib/utils/managers';
export { capabilities as modifierCapabilities } from './lib/modifiers/custom';
export { helperCapabilities, HelperManager } from './lib/helpers/custom';
export { isSerializationFirstNode } from './lib/utils/serialization-first-node-helpers';
export { setComponentTemplate, getComponentTemplate } from './lib/utils/component-template';
export { CapturedRenderNode } from './lib/utils/debug-render-tree';
126 changes: 12 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,8 @@
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 {
Bounds,
CapturedArguments,
CapturedNamedArguments,
ComponentCapabilities,
ComponentDefinition,
Destroyable,
Expand All @@ -16,13 +11,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, TemplateArgs } from '../utils/args-proxy';
import AbstractComponentManager from './abstract';

const CAPABILITIES = {
Expand Down Expand Up @@ -149,94 +144,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 +183,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 +219,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 +282,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: TemplateArgs,
public env: EmberVMEnvironment
) {
if (hasDestructors(delegate)) {
registerDestructor(this, () => delegate.destroyComponent(component));
Expand Down
107 changes: 93 additions & 14 deletions packages/@ember/-internals/glimmer/lib/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,19 @@

import { Factory } from '@ember/-internals/owner';
import { FrameworkObject } from '@ember/-internals/runtime';
import { symbol } from '@ember/-internals/utils';
import { getDebugName, symbol } from '@ember/-internals/utils';
import { join } from '@ember/runloop';
import { DEBUG } from '@glimmer/env';
import { Dict } from '@glimmer/interfaces';
import { createTag, dirtyTag } from '@glimmer/validator';
import {
consumeTag,
createTag,
deprecateMutationsInTrackingTransaction,
dirtyTag,
} from '@glimmer/validator';
import { helperCapabilities, HelperManager } from './helpers/custom';
import { TemplateArgs } from './utils/args-proxy';
import { setHelperManager } from './utils/managers';

export const RECOMPUTE_TAG = symbol('RECOMPUTE_TAG');

Expand All @@ -30,18 +39,6 @@ export interface SimpleHelper<T = unknown> {
compute: HelperFunction<T>;
}

export function isHelperFactory(
helper: any | undefined | null
): helper is Factory<SimpleHelper | HelperInstance, HelperFactory<SimpleHelper | HelperInstance>> {
return (
typeof helper === 'object' && helper !== null && helper.class && helper.class.isHelperFactory
);
}

export function isClassHelper(helper: SimpleHelper | HelperInstance): helper is HelperInstance {
return (helper as any).destroy !== undefined;
}

/**
Ember Helpers are functions that can compute values, and are used in templates.
For example, this code calls a helper named `format-currency`:
Expand Down Expand Up @@ -138,6 +135,56 @@ let Helper = FrameworkObject.extend({

Helper.isHelperFactory = true;

interface ClassicHelperStateBucket {
instance: HelperInstance;
args: TemplateArgs;
}

class ClassicHelperManager implements HelperManager<ClassicHelperStateBucket> {
capabilities = helperCapabilities('3.23', {
hasValue: true,
hasDestroyable: true,
});

createHelper(definition: ClassHelperFactory, args: TemplateArgs) {
return {
instance: definition.create(),
args,
};
}

getDestroyable({ instance }: ClassicHelperStateBucket) {
return instance;
}

getValue({ instance, args }: ClassicHelperStateBucket) {
let ret;
let { positional, named } = args;

if (DEBUG) {
deprecateMutationsInTrackingTransaction!(() => {
ret = instance.compute(positional, named);
});
} else {
ret = instance.compute(positional, named);
}

consumeTag(instance[RECOMPUTE_TAG]);

return ret;
}

getDebugName(definition: ClassHelperFactory) {
return getDebugName!(definition.class!['prototype']);
}
}

export const CLASSIC_HELPER_MANAGER = new ClassicHelperManager();

setHelperManager(() => CLASSIC_HELPER_MANAGER, Helper);

///////////

class Wrapper implements HelperFactory<SimpleHelper> {
isHelperFactory: true = true;

Expand All @@ -151,6 +198,38 @@ class Wrapper implements HelperFactory<SimpleHelper> {
}
}

class SimpleClassicHelperManager implements HelperManager<() => unknown> {
capabilities = helperCapabilities('3.23', {
hasValue: true,
});

createHelper(definition: Wrapper, args: TemplateArgs) {
if (DEBUG) {
return () => {
let ret;

deprecateMutationsInTrackingTransaction!(() => {
ret = definition.compute.call(null, args.positional, args.named);
});

return ret;
};
}

return definition.compute.bind(null, args.positional, args.named);
}

getValue(fn: () => unknown) {
return fn();
}

getDebugName(definition: Wrapper) {
return getDebugName!(definition.compute);
}
}

setHelperManager(() => new SimpleClassicHelperManager(), Wrapper.prototype);

/**
In many cases it is not necessary to use the full `Helper` class.
The `helper` method create pure-function helpers without instances.
Expand Down
Loading

0 comments on commit ff4dd17

Please sign in to comment.