Skip to content

Commit

Permalink
Merge pull request #16308 from smfoote/custom-component-manager
Browse files Browse the repository at this point in the history
Custom component manager
  • Loading branch information
rwjblue authored Mar 6, 2018
2 parents 3240b4a + 8cca013 commit caca9d8
Show file tree
Hide file tree
Showing 14 changed files with 526 additions and 43 deletions.
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

0 comments on commit caca9d8

Please sign in to comment.