-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #16308 from smfoote/custom-component-manager
Custom component manager
- Loading branch information
Showing
14 changed files
with
526 additions
and
43 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
149 changes: 149 additions & 0 deletions
149
packages/ember-glimmer/lib/component-managers/custom.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
36 changes: 36 additions & 0 deletions
36
packages/ember-glimmer/lib/utils/custom-component-manager.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
Oops, something went wrong.