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

Custom component manager #16308

Merged
merged 2 commits into from
Mar 6, 2018
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
37 changes: 33 additions & 4 deletions packages/ember-glimmer/lib/component-managers/curly.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
Arguments,
Bounds,
ComponentDefinition,
ComponentManager,
ElementOperations,
Invocation,
PreparedArguments,
Expand All @@ -39,13 +40,13 @@ import {
getOwner,
guidFor,
} from 'ember-utils';
import { OwnedTemplateMeta, setViewElement } from 'ember-views';
import { addChildView, OwnedTemplateMeta, setViewElement } from 'ember-views';
import {
BOUNDS,
DIRTY_TAG,
HAS_BLOCK,
IS_DISPATCHING_ATTRS,
ROOT_REF,
ROOT_REF
} from '../component';
import Environment from '../environment';
import { DynamicScope } from '../renderer';
Expand Down Expand Up @@ -198,38 +199,64 @@ export default class CurlyComponentManager extends AbstractManager<ComponentStat
return { positional: EMPTY_ARRAY, named };
}

/*
* This hook is responsible for actually instantiating the component instance.
* It also is where we perform additional bookkeeping to support legacy
* features like exposed by view mixins like ChildViewSupport, ActionSupport,
* etc.
*/
create(environment: Environment, state: DefinitionState, args: Arguments, dynamicScope: DynamicScope, callerSelfRef: VersionedPathReference<Opaque>, hasBlock: boolean): ComponentStateBucket {
if (DEBUG) {
this._pushToDebugStack(`component:${state.name}`, environment);
}

// Get the nearest concrete component instance from the scope. "Virtual"
// components will be skipped.
let parentView = dynamicScope.view;

// Get the Ember.Component subclass to instantiate for this component.
let factory = state.ComponentClass;

// Capture the arguments, which tells Glimmer to give us our own, stable
// copy of the Arguments object that is safe to hold on to between renders.
let capturedArgs = args.named.capture();
let props = processComponentArgs(capturedArgs);

// Alias `id` argument to `elementId` property on the component instance.
aliasIdToElementId(args, props);

// Set component instance's parentView property to point to nearest concrete
// component.
props.parentView = parentView;

// Set whether this component was invoked with a block
// (`{{#my-component}}{{/my-component}}`) or without one
// (`{{my-component}}`).
props[HAS_BLOCK] = hasBlock;

// Save the current `this` context of the template as the component's
// `_targetObject`, so bubbled actions are routed to the right place.
props._targetObject = callerSelfRef.value();

// static layout asserts CurriedDefinition
if (state.template) {
props.layout = state.template;
}

// Now that we've built up all of the properties to set on the component instance,
// actually create it.
let component = factory.create(props);

let finalizer = _instrumentStart('render.component', initialRenderInstrumentDetails, component);

// We become the new parentView for downstream components, so save our
// component off on the dynamic scope.
dynamicScope.view = component;

// Unless we're the root component, we need to add ourselves to our parent
// component's childViews array.
if (parentView !== null && parentView !== undefined) {
parentView.appendChild(component);
addChildView(parentView, component);
}

if (ENV._ENABLE_DID_INIT_ATTRS_SUPPORT === true) {
Expand All @@ -251,6 +278,8 @@ export default class CurlyComponentManager extends AbstractManager<ComponentStat
}
}

// Track additional lifecycle metadata about this component in a state bucket.
// Essentially we're saving off all the state we'll need in the future.
let bucket = new ComponentStateBucket(environment, component, capturedArgs, finalizer);

if (args.named.has('class')) {
Expand Down Expand Up @@ -486,7 +515,7 @@ export class CurlyComponentDefinition implements ComponentDefinition {
public symbolTable: ProgramSymbolTable | undefined;

// tslint:disable-next-line:no-shadowed-variable
constructor(public name: string, public manager: CurlyComponentManager = CURLY_COMPONENT_MANAGER, public ComponentClass: any, public handle: Option<VMHandle>, template: OwnedTemplate, args?: CurriedArgs) {
constructor(public name: string, public manager: ComponentManager<ComponentStateBucket, DefinitionState> = CURLY_COMPONENT_MANAGER, public ComponentClass: any, public handle: Option<VMHandle>, template: OwnedTemplate, args?: CurriedArgs) {
const layout = template && template.asLayout();
const symbolTable = layout ? layout.symbolTable : undefined;
this.symbolTable = symbolTable;
Expand Down
149 changes: 149 additions & 0 deletions packages/ember-glimmer/lib/component-managers/custom.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
import { ComponentCapabilities, Opaque, Option } from '@glimmer/interfaces';
import { PathReference, Tag } from '@glimmer/reference';
import { Arguments, Bounds, CapturedNamedArguments, PrimitiveReference } from '@glimmer/runtime';
import { Destroyable } from '@glimmer/util';

import { addChildView } from 'ember-views';

import Environment from '../environment';
import { DynamicScope, Renderer } from '../renderer';
import { RootReference } from '../utils/references';
import AbstractComponentManager from './abstract';
import DefinitionState from './definition-state';

export interface CustomComponentManagerDelegate<T> {
version: 'string';
create(options: { ComponentClass: T, args: {} }): T;
getContext(instance: T): Opaque;
update(instance: T, args: {}): void;
destroy?(instance: T): void;
didCreate?(instance: T): void;
didUpdate?(instance: T): void;
getView?(instance: T): any;
}

export interface ComponentArguments<T = {}> {
positional: Opaque[];
named: T;
}

/**
The CustomComponentManager allows addons to provide custom component
implementations that integrate seamlessly into Ember. This is accomplished
through a delegate, registered with the custom component manager, which
implements a set of hooks that determine component behavior.

To create a custom component manager, instantiate a new CustomComponentManager
class and pass the delegate as the first argument:

```js
let manager = new CustomComponentManager({
// ...delegate implementation...
});
```

## Delegate Hooks

Throughout the lifecycle of a component, the component manager will invoke
delegate hooks that are responsible for surfacing those lifecycle changes to
the end developer.

* `create()` - invoked when a new instance of a component should be created
* `update()` - invoked when the arguments passed to a component change
* `getContext()` - returns the object that should be
*/
export default class CustomComponentManager<T> extends AbstractComponentManager<CustomComponentState<T> | null, DefinitionState> {
constructor(private delegate: CustomComponentManagerDelegate<T>) {
super();
}

create(_env: Environment, definition: DefinitionState, args: Arguments, dynamicScope: DynamicScope): CustomComponentState<T> {
const { delegate } = this;
const capturedArgs = args.named.capture();

const component = delegate.create({
args: capturedArgs.value(),
ComponentClass: definition.ComponentClass as any as T
});

const { view: parentView } = dynamicScope;

if (parentView !== null && parentView !== undefined) {
addChildView(parentView, component);
}

dynamicScope.view = component;

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

update({ component, args }: CustomComponentState<T>) {
this.delegate.update(component, args.value());
}

getContext(component: T) {
this.delegate.getContext(component);
}

getLayout(state: DefinitionState) {
return {
handle: state.template.asLayout().compile(),
symbolTable: state.symbolTable
};
}

getSelf({ component }: CustomComponentState<T>): PrimitiveReference<null> | PathReference<Opaque> {
const context = this.delegate.getContext(component);
return new RootReference(context);
}

getDestructor(state: CustomComponentState<T>): Option<Destroyable> {
return state;
}

getCapabilities(_state: DefinitionState): ComponentCapabilities {
return {
dynamicLayout: false,
dynamicTag: false,
prepareArgs: false,
createArgs: true,
attributeHook: false,
elementHook: false
};
}

getTag({ args }: CustomComponentState<T>): Tag {
return args.tag;
}

didRenderLayout({ component }: CustomComponentState<T>, _bounds: Bounds) {
const renderer = getRenderer(component);
renderer.register(component);
}
}

/**
* Stores internal state about a component instance after it's been created.
*/
class CustomComponentState<T> {
constructor(
public delegate: CustomComponentManagerDelegate<T>,
public component: T,
public args: CapturedNamedArguments
) {}

destroy() {
const { delegate, component } = this;

let renderer = getRenderer(component);
renderer.unregister(component);

if (delegate.destroy) { delegate.destroy(component); }
}
}

function getRenderer(component: {}): Renderer {
let renderer = component['renderer'];
if (!renderer) { throw new Error(`missing renderer for component ${component}`); }
return renderer as Renderer;
}
3 changes: 2 additions & 1 deletion packages/ember-glimmer/lib/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
ViewMixin,
ViewStateSupport,
} from 'ember-views';

import { RootReference, UPDATE } from './utils/references';

export const DIRTY_TAG = symbol('DIRTY_TAG');
Expand Down Expand Up @@ -914,4 +915,4 @@ Component.reopenClass({
positionalParams: [],
});

export default Component;
export default Component;
2 changes: 2 additions & 0 deletions packages/ember-glimmer/lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -303,3 +303,5 @@ export { UpdatableReference, INVOKE } from './utils/references';
export { default as iterableFor } from './utils/iterable';
export { default as DebugStack } from './utils/debug-stack';
export { default as OutletView } from './views/outlet';
export { default as CustomComponentManager } from './component-managers/custom';
export { COMPONENT_MANAGER, componentManager } from './utils/custom-component-manager';
2 changes: 1 addition & 1 deletion packages/ember-glimmer/lib/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export type IBuilder = (env: Environment, cursor: Cursor) => ElementBuilder;

export class DynamicScope implements GlimmerDynamicScope {
constructor(
public view: Component | null,
public view: Component | {} | null,
public outletState: VersionedPathReference<OutletState | undefined>,
public rootOutletState?: VersionedPathReference<OutletState | undefined>) {
}
Expand Down
14 changes: 12 additions & 2 deletions packages/ember-glimmer/lib/resolver.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import {
} from '@glimmer/interfaces';
import { LazyCompiler, Macros, PartialDefinition } from '@glimmer/opcode-compiler';
import {
ComponentManager,
getDynamicVar,
Helper,
ModifierManager,
Expand All @@ -20,9 +21,10 @@ import {
lookupPartial,
OwnedTemplateMeta,
} from 'ember-views';
import { EMBER_MODULE_UNIFICATION } from 'ember/features';
import { EMBER_MODULE_UNIFICATION, GLIMMER_CUSTOM_COMPONENT_MANAGER } from 'ember/features';
import CompileTimeLookup from './compile-time-lookup';
import { CurlyComponentDefinition } from './component-managers/curly';
import DefinitionState from './component-managers/definition-state';
import { TemplateOnlyComponentDefinition } from './component-managers/template-only';
import { isHelperFactory, isSimpleHelper } from './helper';
import { default as classHelper } from './helpers/-class';
Expand All @@ -46,6 +48,8 @@ import { mountHelper } from './syntax/mount';
import { outletHelper } from './syntax/outlet';
import { renderHelper } from './syntax/render';
import { Factory as TemplateFactory, Injections, OwnedTemplate } from './template';
import ComponentStateBucket from './utils/curly-component-state-bucket';
import getCustomComponentManager from './utils/custom-component-manager';
import { ClassBasedHelperReference, SimpleHelperReference } from './utils/references';

function instrumentationPayload(name: string) {
Expand Down Expand Up @@ -293,11 +297,17 @@ export default class RuntimeResolver implements IRuntimeResolver<OwnedTemplateMe
return new TemplateOnlyComponentDefinition(layout);
}

let manager: ComponentManager<ComponentStateBucket, DefinitionState> | undefined;

if (GLIMMER_CUSTOM_COMPONENT_MANAGER && component && component.class) {
manager = getCustomComponentManager(meta.owner, component.class);
}

let finalizer = _instrumentStart('render.getComponentDefinition', instrumentationPayload, name);
let definition = (layout || component) ?
new CurlyComponentDefinition(
name,
undefined,
manager,
component || meta.owner.factoryFor(P`component:-default`),
null,
layout
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ export interface Component {
elementId: string;
tagName: string;
isDestroying: boolean;
appendChild(view: Component): void;
appendChild(view: {}): void;
trigger(event: string): void;
destroy(): void;
setProperties(props: {
Expand Down
36 changes: 36 additions & 0 deletions packages/ember-glimmer/lib/utils/custom-component-manager.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { ComponentManager } from '@glimmer/runtime';

import { assert } from 'ember-debug';
import { Owner, symbol } from 'ember-utils';

import DefinitionState from '../component-managers/definition-state';
import ComponentStateBucket from '../utils/curly-component-state-bucket';

import { GLIMMER_CUSTOM_COMPONENT_MANAGER } from 'ember/features';

export const COMPONENT_MANAGER = symbol('COMPONENT_MANAGER');

export function componentManager(obj: any, managerId: String) {
if ('reopenClass' in obj) {
return obj.reopenClass({
[COMPONENT_MANAGER]: managerId
});
}

obj[COMPONENT_MANAGER] = managerId;
return obj;
}

export default function getCustomComponentManager(owner: Owner, obj: {}): ComponentManager<ComponentStateBucket, DefinitionState> | undefined {
if (!GLIMMER_CUSTOM_COMPONENT_MANAGER) { return; }

if (!obj) { return; }

let managerId = obj[COMPONENT_MANAGER];
if (!managerId) { return; }

let manager = owner.lookup(`component-manager:${managerId}`) as ComponentManager<ComponentStateBucket, DefinitionState>;
assert(`Could not find custom component manager '${managerId}' for ${obj}`, !!manager);

return manager;
}
Loading