Skip to content

Commit

Permalink
Merge pull request #850 from chiragpat/render-component-cleanup
Browse files Browse the repository at this point in the history
Exposing a primitive renderComponent API
  • Loading branch information
chadhietala committed Oct 2, 2018
2 parents 58a2bef + 5176609 commit 7f041fd
Show file tree
Hide file tree
Showing 7 changed files with 183 additions and 170 deletions.
180 changes: 34 additions & 146 deletions packages/@glimmer/bundle-compiler/test/entry-point-test.ts
Original file line number Diff line number Diff line change
@@ -1,162 +1,50 @@
import { module, test, EagerTestEnvironment } from '@glimmer/test-helpers';
import { BundleCompiler, CompilerDelegate } from '@glimmer/bundle-compiler';
import { RuntimeResolver, ComponentCapabilities, Option } from '@glimmer/interfaces';
import { RuntimeProgram } from '@glimmer/program';
import {
LowLevelVM,
NewElementBuilder,
ComponentManager,
MINIMAL_CAPABILITIES,
UNDEFINED_REFERENCE,
PrimitiveReference,
} from '@glimmer/runtime';
import { CONSTANT_TAG, VersionedPathReference, Tag } from '@glimmer/reference';
import { Destroyable } from '@glimmer/util';

class TestCompilerDelegate implements CompilerDelegate<{}> {
hasComponentInScope(): boolean {
return false;
}

resolveComponent(): never {
throw new Error('Method not implemented.');
}

getComponentCapabilities(): never {
throw new Error('Method not implemented.');
}

hasHelperInScope(): boolean {
return false;
}

resolveHelper(): never {
throw new Error('Method not implemented.');
}

hasModifierInScope(): boolean {
return false;
}

resolveModifier(): never {
throw new Error('Method not implemented.');
}

hasPartialInScope(): boolean {
return false;
}

resolvePartial(): never {
throw new Error('Method not implemented.');
}
}

class SimpleResolver implements RuntimeResolver<{}> {
lookupComponentDefinition(): never {
throw new Error('Method not implemented.');
}

lookupPartial(): never {
throw new Error('Method not implemented.');
}

resolve(): never {
throw new Error('Method not implemented.');
}
}

class BasicManager implements ComponentManager<null, null> {
getCapabilities(): ComponentCapabilities {
return MINIMAL_CAPABILITIES;
}

prepareArgs(): never {
throw new Error('Method not implemented.');
}

create(): null {
return null;
}

getSelf(): VersionedPathReference {
return UNDEFINED_REFERENCE;
}

getTag(): Tag {
return CONSTANT_TAG;
}

didRenderLayout(): void {
return;
}

didCreate(): void {
return;
}

update(): void {
return;
}

didUpdateLayout(): void {
return;
}

didUpdate(): void {
return;
}

getDestructor(): Option<Destroyable> {
return null;
}
}
import { module, test, EagerRenderDelegate } from '@glimmer/test-helpers';
import { PrimitiveReference } from '@glimmer/runtime';

export class EntryPointTest {
@test
'an entry point'() {
let compiler = new BundleCompiler(new TestCompilerDelegate());

let titleLocator = { module: 'ui/components/Title', name: 'default' };
let titleBlock = compiler.add(titleLocator, '<h1>{{@title}}</h1>');
let { main, heap, pool, table } = compiler.compile();

let env = new EagerTestEnvironment();
let program = RuntimeProgram.hydrate(heap, pool, new SimpleResolver());
let delegate = new EagerRenderDelegate();
delegate.registerComponent('Basic', 'Basic', 'Title', `<h1>hello {{@title}}</h1>`);

let element = document.createElement('div');
let builder = NewElementBuilder.forInitialRender(env, { element, nextSibling: null });

env.begin();
let title = PrimitiveReference.create('renderComponent');
delegate.renderComponent('Title', { title }, element);

let vm = LowLevelVM.empty(program, env, builder);

let title = table.vmHandleByModuleLocator.get(titleLocator);

vm.pushFrame();

// push three blocks onto the stack; TODO: Optimize
for (let i = 0; i <= 9; i++) {
vm.stack.push(null);
}
QUnit.assert.equal(element.innerHTML, '<h1>hello renderComponent</h1>');
}

// @title="hello renderComponent"
vm.stack.push(PrimitiveReference.create('hello renderComponent'));
vm.args.setup(vm.stack, ['@title'], ['main', 'else', 'attrs'], 0, false);
vm.stack.push(vm.args);
@test
'does not leak args between invocations'() {
let delegate = new EagerRenderDelegate();
delegate.registerComponent('Basic', 'Basic', 'Title', `<h1>hello {{@title}}</h1>`);

// Setup `main()` by pushing an invocation and definition onto the stack
vm.stack.push({
handle: title,
symbolTable: { hasEval: false, symbols: titleBlock.symbols, referrer: null },
});
vm.stack.push({ state: null, manager: new BasicManager() });
let element = document.createElement('div');
let title = PrimitiveReference.create('renderComponent');
delegate.renderComponent('Title', { title }, element);
QUnit.assert.equal(element.innerHTML, '<h1>hello renderComponent</h1>');

// invoke main()
vm.execute(main);
element = document.createElement('div');
let newTitle = PrimitiveReference.create('new title');
delegate.renderComponent('Title', { title: newTitle }, element);
QUnit.assert.equal(element.innerHTML, '<h1>hello new title</h1>');
}

env.commit();
@test
'can render different components per call'() {
let delegate = new EagerRenderDelegate();
delegate.registerComponent('Basic', 'Basic', 'Title', `<h1>hello {{@title}}</h1>`);
delegate.registerComponent('Basic', 'Basic', 'Body', `<p>body {{@body}}</p>`);

let element = document.createElement('div');
let title = PrimitiveReference.create('renderComponent');
delegate.renderComponent('Title', { title }, element);
QUnit.assert.equal(element.innerHTML, '<h1>hello renderComponent</h1>');

element = document.createElement('div');
let body = PrimitiveReference.create('text');
delegate.renderComponent('Body', { body }, element);
QUnit.assert.equal(element.innerHTML, '<p>body text</p>');
}
}

Expand Down
2 changes: 1 addition & 1 deletion packages/@glimmer/runtime/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import './lib/bootstrap';

export { default as renderMain, TemplateIterator } from './lib/render';
export { renderMain, renderComponent, TemplateIterator } from './lib/render';

export {
NULL_REFERENCE,
Expand Down
8 changes: 4 additions & 4 deletions packages/@glimmer/runtime/lib/compiled/opcodes/component.ts
Original file line number Diff line number Diff line change
Expand Up @@ -495,9 +495,9 @@ APPEND_OPCODES.add(Op.GetComponentLayout, (vm, { op1: _state }) => {

let invoke: { handle: number; symbolTable: ProgramSymbolTable };

if (hasStaticLayout(capabilities, manager)) {
if (hasStaticLayoutCapability(capabilities, manager)) {
invoke = manager.getLayout(definitionState, resolver);
} else if (hasDynamicLayout(capabilities, manager)) {
} else if (hasDynamicLayoutCapability(capabilities, manager)) {
invoke = manager.getDynamicLayout(instanceState, resolver);
} else {
throw unreachable();
Expand All @@ -507,7 +507,7 @@ APPEND_OPCODES.add(Op.GetComponentLayout, (vm, { op1: _state }) => {
stack.push(invoke.handle);
});

function hasStaticLayout(
export function hasStaticLayoutCapability(
capabilities: CapabilityFlags,
_manager: InternalComponentManager
): _manager is WithStaticLayout<
Expand All @@ -519,7 +519,7 @@ function hasStaticLayout(
return hasCapability(capabilities, Capability.DynamicLayout) === false;
}

function hasDynamicLayout(
export function hasDynamicLayoutCapability(
capabilities: CapabilityFlags,
_manager: InternalComponentManager
): _manager is WithDynamicLayout<ComponentInstanceState, Opaque, RuntimeResolver<Opaque>> {
Expand Down
74 changes: 72 additions & 2 deletions packages/@glimmer/runtime/lib/render.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@ import { RuntimeProgram } from './vm/append';
import { ElementBuilder } from './vm/element-builder';
import { DynamicScope, Environment } from './environment';
import { PathReference } from '@glimmer/reference';
import { Opaque } from '@glimmer/interfaces';
import { Opaque, Dict } from '@glimmer/interfaces';
import { resolveComponent } from './component/resolve';
import { expect } from '@glimmer/util';
import { capabilityFlagsFrom } from './capabilities';
import { hasStaticLayoutCapability } from './compiled/opcodes/component';

export interface TemplateIterator {
next(): IteratorResult<RenderResult>;
Expand All @@ -16,7 +20,7 @@ class TemplateIteratorImpl<T> implements TemplateIterator {
}
}

export default function render<T>(
export function renderMain<T>(
program: RuntimeProgram<T>,
env: Environment,
self: PathReference<Opaque>,
Expand All @@ -27,3 +31,69 @@ export default function render<T>(
let vm = VM.initial(program, env, self, dynamicScope, builder, handle);
return new TemplateIteratorImpl(vm);
}

export type RenderComponentArgs = Dict<PathReference<Opaque>>;

/**
* Returns a TemplateIterator configured to render a root component.
*/
export function renderComponent<T>(
program: RuntimeProgram<T>,
env: Environment,
builder: ElementBuilder,
main: number,
name: string,
args: RenderComponentArgs = {}
): TemplateIterator {
const vm = VM.empty(program, env, builder, main);
const { resolver } = vm.constants;

const definition = expect(
resolveComponent(resolver, name, null),
`could not find component "${name}"`
);

const { manager, state } = definition;
const capabilities = capabilityFlagsFrom(manager.getCapabilities(state));

let invocation;

if (hasStaticLayoutCapability(capabilities, manager)) {
invocation = manager.getLayout(state, resolver);
} else {
throw new Error('Cannot invoke components with dynamic layouts as a root component.');
}

// Get a list of tuples of argument names and references, like
// [['title', reference], ['name', reference]]
const argList = Object.keys(args).map(key => [key, args[key]]);

const blockNames = ['main', 'else', 'attrs'];
// Prefix argument names with `@` symbol
const argNames = argList.map(([name]) => `@${name}`);

vm.pushFrame();

// Push blocks on to the stack, three stack values per block
for (let i = 0; i < 3 * blockNames.length; i++) {
vm.stack.push(null);
}

vm.stack.push(null);

// For each argument, push its backing reference on to the stack
argList.forEach(([, reference]) => {
vm.stack.push(reference);
});

// Configure VM based on blocks and args just pushed on to the stack.
vm.args.setup(vm.stack, argNames, blockNames, 0, false);

// Needed for the Op.Main opcode: arguments, component invocation object, and
// component definition.
vm.stack.push(vm.args);
vm.stack.push(invocation);
vm.stack.push(definition);

return new TemplateIteratorImpl(vm);
}
8 changes: 7 additions & 1 deletion packages/@glimmer/runtime/lib/vm/append.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,12 @@ export default class VM<T> implements PublicVM {
return vm;
}

static empty<T>(program: RuntimeProgram<T>, env: Environment, elementStack: ElementBuilder) {
static empty<T>(
program: RuntimeProgram<T>,
env: Environment,
elementStack: ElementBuilder,
handle: number
) {
let dynamicScope: DynamicScope = {
get() {
return UNDEFINED_REFERENCE;
Expand All @@ -226,6 +231,7 @@ export default class VM<T> implements PublicVM {
elementStack
);
vm.updatingOpcodeStack.push(new LinkedList<UpdatingOpcode>());
vm.pc = vm.heap.getaddr(handle);
return vm;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ export class Modules {

resolve(name: string, referrer: Locator, defaultRoot?: string): Option<string> {
let local =
referrer &&
referrer.locator.module &&
referrer.locator.module.replace(/^((.*)\/)?([^\/]*)$/, `$1${name}`);
if (local && this.registry[local]) {
Expand Down
Loading

0 comments on commit 7f041fd

Please sign in to comment.