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][FEAT][Tracked Properties] Adds Arg Proxy Feature Flag #17835

Merged
merged 2 commits into from
Apr 22, 2019
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
108 changes: 96 additions & 12 deletions packages/@ember/-internals/glimmer/lib/component-managers/custom.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { getCurrentTracker } from '@ember/-internals/metal';
import { Factory } from '@ember/-internals/owner';
import { HAS_NATIVE_PROXY } from '@ember/-internals/utils';
import { OwnedTemplateMeta } from '@ember/-internals/views';
import { EMBER_CUSTOM_COMPONENT_ARG_PROXY } from '@ember/canary-features';
import { assert } from '@ember/debug';
import { DEBUG } from '@glimmer/env';
import {
ComponentCapabilities,
Dict,
Expand Down Expand Up @@ -40,14 +44,22 @@ const CAPABILITIES = {
export interface OptionalCapabilities {
asyncLifecycleCallbacks?: boolean;
destructor?: boolean;
updateHook?: boolean;
}

export function capabilities(managerAPI: '3.4', options: OptionalCapabilities = {}): Capabilities {
assert('Invalid component manager compatibility specified', managerAPI === '3.4');

let updateHook = true;

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
updateHook = 'updateHook' in options ? Boolean(options.updateHook) : true;
}

return {
asyncLifeCycleCallbacks: Boolean(options.asyncLifecycleCallbacks),
destructor: Boolean(options.destructor),
updateHook,
};
}

Expand All @@ -61,6 +73,7 @@ export interface DefinitionState<ComponentInstance> {
export interface Capabilities {
asyncLifeCycleCallbacks: boolean;
destructor: boolean;
updateHook: boolean;
}

// TODO: export ICapturedArgumentsValue from glimmer and replace this
Expand Down Expand Up @@ -132,12 +145,12 @@ export interface ComponentArguments {
export default class CustomComponentManager<ComponentInstance>
extends AbstractComponentManager<
CustomComponentState<ComponentInstance>,
DefinitionState<ComponentInstance>
CustomComponentDefinitionState<ComponentInstance>
>
implements
WithStaticLayout<
CustomComponentState<ComponentInstance>,
DefinitionState<ComponentInstance>,
CustomComponentDefinitionState<ComponentInstance>,
OwnedTemplateMeta,
RuntimeResolver
> {
Expand All @@ -149,16 +162,82 @@ export default class CustomComponentManager<ComponentInstance>
const { delegate } = definition;
const capturedArgs = args.capture();

const component = delegate.createComponent(
definition.ComponentClass.class,
capturedArgs.value()
);
let value;
let namedArgsProxy = {};

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
if (HAS_NATIVE_PROXY) {
let handler: ProxyHandler<{}> = {
get(_target, prop) {
assert('args can only be strings', typeof prop === 'string');

let tracker = getCurrentTracker();
let ref = capturedArgs.named.get(prop as string);

if (tracker) {
tracker.add(ref.tag);
}

return ref.value();
},
};

if (DEBUG) {
handler.set = function(_target, prop) {
assert(
`You attempted to set ${definition.ComponentClass.class}#${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;
};
}

namedArgsProxy = new Proxy(namedArgsProxy, handler);
} else {
capturedArgs.named.names.forEach(name => {
Object.defineProperty(namedArgsProxy, name, {
get() {
let ref = capturedArgs.named.get(name);
let tracker = getCurrentTracker();

if (tracker) {
tracker.add(ref.tag);
}

return ref.value();
},
});
});
}

value = {
named: namedArgsProxy,
positional: capturedArgs.positional.value(),
};
} else {
value = capturedArgs.value();
}

const component = delegate.createComponent(definition.ComponentClass.class, value);

return new CustomComponentState(delegate, component, capturedArgs);
return new CustomComponentState(delegate, component, capturedArgs, namedArgsProxy);
}

update({ delegate, component, args }: CustomComponentState<ComponentInstance>) {
delegate.updateComponent(component, args.value());
update({ delegate, component, args, namedArgsProxy }: CustomComponentState<ComponentInstance>) {
let value;

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
value = {
named: namedArgsProxy!,
positional: args.positional.value(),
};
} else {
value = args.value();
}

delegate.updateComponent(component, value);
}

didCreate({ delegate, component }: CustomComponentState<ComponentInstance>) {
Expand Down Expand Up @@ -189,8 +268,12 @@ export default class CustomComponentManager<ComponentInstance>
}
}

getCapabilities(): ComponentCapabilities {
return CAPABILITIES;
getCapabilities({
delegate,
}: CustomComponentDefinitionState<ComponentInstance>): ComponentCapabilities {
return Object.assign({}, CAPABILITIES, {
updateHook: delegate.capabilities.updateHook,
});
}

getTag({ args }: CustomComponentState<ComponentInstance>): Tag {
Expand All @@ -215,7 +298,8 @@ export class CustomComponentState<ComponentInstance> {
constructor(
public delegate: ManagerDelegate<ComponentInstance>,
public component: ComponentInstance,
public args: CapturedArguments
public args: CapturedArguments,
public namedArgsProxy?: {}
) {}

destroy() {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
import { EMBER_METAL_TRACKED_PROPERTIES } from '@ember/canary-features';
import { Object as EmberObject, A } from '@ember/-internals/runtime';
import {
EMBER_CUSTOM_COMPONENT_ARG_PROXY,
EMBER_METAL_TRACKED_PROPERTIES,
} from '@ember/canary-features';
import { tracked, nativeDescDecorator as descriptor } from '@ember/-internals/metal';
import { moduleFor, RenderingTestCase, strip, runTask } from 'internal-test-helpers';
import GlimmerishComponent from '../../utils/glimmerish-component';
Expand Down Expand Up @@ -373,4 +376,79 @@ if (EMBER_METAL_TRACKED_PROPERTIES) {
}
}
);

if (EMBER_CUSTOM_COMPONENT_ARG_PROXY) {
moduleFor(
'Component Tracked Properties w/ Args Proxy',
class extends RenderingTestCase {
'@test downstream property changes do not invalidate upstream component getters/arguments'(
assert
) {
let outerRenderCount = 0;
let innerRenderCount = 0;

class OuterComponent extends GlimmerishComponent {
get count() {
outerRenderCount++;
return this.args.count;
}
}

class InnerComponent extends GlimmerishComponent {
@tracked count = 0;

get combinedCounts() {
innerRenderCount++;
return this.args.count + this.count;
}

updateInnerCount() {
this.count++;
}
}

this.registerComponent('outer', {
ComponentClass: OuterComponent,
template: '<Inner @count={{this.count}}/>',
});

this.registerComponent('inner', {
ComponentClass: InnerComponent,
template: '<button {{action this.updateInnerCount}}>{{this.combinedCounts}}</button>',
});

this.render('<Outer @count={{this.count}}/>', {
count: 0,
});

this.assertText('0');

assert.equal(outerRenderCount, 1);
assert.equal(innerRenderCount, 1);

runTask(() => this.$('button').click());

this.assertText('1');

assert.equal(
outerRenderCount,
1,
'updating inner component does not cause outer component to rerender'
);
assert.equal(
innerRenderCount,
2,
'updating inner component causes inner component to rerender'
);

runTask(() => this.context.set('count', 1));

this.assertText('2');

assert.equal(outerRenderCount, 2, 'outer component updates based on context');
assert.equal(innerRenderCount, 3, 'inner component updates based on outer component');
}
}
);
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,16 @@
import { setComponentManager, capabilities } from '@ember/-internals/glimmer';
import { get, set } from '@ember/-internals/metal';
import { setOwner } from '@ember/-internals/owner';

class GlimmerishComponentManager {
constructor(owner) {
this.capabilities = capabilities('3.4');
this.capabilities = capabilities('3.4', { updateHook: false });
this.owner = owner;
}

createComponent(Factory, args) {
return new Factory(this.owner, args.named);
}

updateComponent(component, args) {
set(component, 'args', args.named);
}

getContext(component) {
return component;
}
Expand All @@ -26,14 +21,6 @@ class GlimmerishComponent {
setOwner(this, owner);
this.args = args;
}

get args() {
return get(this, '__args__');
}

set args(args) {
set(this, '__args__', args);
}
}

setComponentManager(() => new GlimmerishComponentManager(), GlimmerishComponent);
Expand Down
4 changes: 4 additions & 0 deletions packages/@ember/canary-features/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export const DEFAULT_FEATURES = {
EMBER_ROUTING_BUILD_ROUTEINFO_METADATA: true,
EMBER_NATIVE_DECORATOR_SUPPORT: true,
EMBER_GLIMMER_FN_HELPER: null,
EMBER_CUSTOM_COMPONENT_ARG_PROXY: null,
};

/**
Expand Down Expand Up @@ -90,3 +91,6 @@ export const EMBER_ROUTING_BUILD_ROUTEINFO_METADATA = featureValue(
);
export const EMBER_NATIVE_DECORATOR_SUPPORT = featureValue(FEATURES.EMBER_NATIVE_DECORATOR_SUPPORT);
export const EMBER_GLIMMER_FN_HELPER = featureValue(FEATURES.EMBER_GLIMMER_FN_HELPER);
export const EMBER_CUSTOM_COMPONENT_ARG_PROXY = featureValue(
FEATURES.EMBER_CUSTOM_COMPONENT_ARG_PROXY
);