import {BindingExpression, createOverrideContext, Scope} from 'aurelia-binding'; import { Container } from 'aurelia-dependency-injection'; import { BehaviorPropertyObserver } from './behavior-property-observer'; import { BoundPropertyInfo } from './bindable-property'; import { ElementEvents } from './element-events'; import { HtmlBehaviorResource } from './html-behavior'; import { BehaviorInstruction } from './instructions'; import { ComponentAttached, ComponentBind, ComponentCreated, ComponentDetached, ComponentUnbind } from './interfaces'; import { View } from './view'; /** * Controls a view model (and optionally its view), according to a particular behavior and by following a set of instructions. */ export class Controller { /** * The HtmlBehaviorResource that provides the base behavior for this controller. */ behavior: HtmlBehaviorResource; /** * The developer's view model instance which provides the custom behavior for this controller. */ viewModel: Object; /** * The view associated with the component being controlled by this controller. * Note: Not all components will have a view, so the value may be null. */ view: View; /** @internal */ private instruction: BehaviorInstruction; /** @internal */ private isAttached: boolean; /** @internal */ private isBound: boolean; /** @internal */ private scope: any; /** @internal */ private container: Container; /** @internal */ private elementEvents: any; /** @internal */ private boundProperties: BoundPropertyInfo[]; /** * Creates an instance of Controller. * @param behavior The HtmlBehaviorResource that provides the base behavior for this controller. * @param instruction The instructions pertaining to the controller's behavior. * @param viewModel The developer's view model instance which provides the custom behavior for this controller. * @param container The container that the controller's view was created from. */ constructor(behavior: HtmlBehaviorResource, instruction: BehaviorInstruction, viewModel: Object, container: Container) { this.behavior = behavior; this.instruction = instruction; this.viewModel = viewModel; this.isAttached = false; this.view = null; this.isBound = false; this.scope = null; this.container = container; this.elementEvents = container.elementEvents || null; let observerLookup = behavior.observerLocator.getOrCreateObserversLookup(viewModel); let handlesBind = behavior.handlesBind; let attributes = instruction.attributes; let boundProperties = this.boundProperties = []; let properties = behavior.properties; let i: number; let ii: number; behavior._ensurePropertiesDefined(viewModel, observerLookup); for (i = 0, ii = properties.length; i < ii; ++i) { properties[i]._initialize(viewModel, observerLookup, attributes as Record, handlesBind, boundProperties); } } /** * Invoked when the view which contains this controller is created. * @param owningView The view inside which this controller resides. */ created(owningView: View): void { if (this.behavior.handlesCreated) { (this.viewModel as ComponentCreated).created(owningView, this.view); } } /** * Used to automate the proper binding of this controller and its view. Used by the composition engine for dynamic component creation. * This should be considered a semi-private API and is subject to change without notice, even across minor or patch releases. * @param overrideContext An override context for binding. * @param owningView The view inside which this controller resides. */ automate(overrideContext?: Object, owningView?: View): void { this.view.bindingContext = this.viewModel; this.view.overrideContext = overrideContext || createOverrideContext(this.viewModel); this.view._isUserControlled = true; if (this.behavior.handlesCreated) { (this.viewModel as ComponentCreated).created(owningView || null, this.view); } this.bind(this.view); } /** * Binds the controller to the scope. * @param scope The binding scope. */ bind(scope: Object): void { let skipSelfSubscriber = this.behavior.handlesBind; let boundProperties = this.boundProperties; let i; let ii; let x: BoundPropertyInfo; let observer: BehaviorPropertyObserver; let selfSubscriber; if (this.isBound) { if (this.scope === scope) { return; } this.unbind(); } this.isBound = true; this.scope = scope; for (i = 0, ii = boundProperties.length; i < ii; ++i) { x = boundProperties[i]; observer = x.observer; selfSubscriber = observer.selfSubscriber; observer.publishing = false; if (skipSelfSubscriber) { observer.selfSubscriber = null; } x.binding.bind(scope as Scope); observer.call(); observer.publishing = true; observer.selfSubscriber = selfSubscriber; } let overrideContext; if (this.view !== null) { if (skipSelfSubscriber) { this.view.viewModelScope = scope; } // do we need to create an overrideContext or is the scope's overrideContext // valid for this viewModel? if (this.viewModel === (scope as Scope).overrideContext.bindingContext) { overrideContext = (scope as Scope).overrideContext; // should we inherit the parent scope? (eg compose / router) } else if (this.instruction.inheritBindingContext) { overrideContext = createOverrideContext(this.viewModel, (scope as Scope).overrideContext); // create the overrideContext and capture the parent without making it // available to AccessScope. We may need it later for template-part replacements. } else { overrideContext = createOverrideContext(this.viewModel); overrideContext.__parentOverrideContext = (scope as Scope).overrideContext; } this.view.bind(this.viewModel, overrideContext); } else if (skipSelfSubscriber) { overrideContext = (scope as Scope).overrideContext; // the factoryCreateInstruction's partReplacements will either be null or an object // containing the replacements. If there are partReplacements we need to preserve the parent // context to allow replacement parts to bind to both the custom element scope and the ambient scope. // Note that factoryCreateInstruction is a property defined on BoundViewFactory. The code below assumes the // behavior stores a the BoundViewFactory on its viewModel under the name of viewFactory. This is implemented // by the replaceable custom attribute. if ((scope as Scope).overrideContext.__parentOverrideContext !== undefined && (this.viewModel as any).viewFactory && (this.viewModel as any).viewFactory.factoryCreateInstruction.partReplacements) { // clone the overrideContext and connect the ambient context. overrideContext = Object.assign({}, (scope as Scope).overrideContext); overrideContext.parentOverrideContext = (scope as Scope).overrideContext.__parentOverrideContext; } (this.viewModel as ComponentBind).bind((scope as Scope).bindingContext, overrideContext); } } /** * Unbinds the controller. */ unbind(): void { if (this.isBound) { let boundProperties = this.boundProperties; let i; let ii; this.isBound = false; this.scope = null; if (this.view !== null) { this.view.unbind(); } if (this.behavior.handlesUnbind) { (this.viewModel as ComponentUnbind).unbind(); } if (this.elementEvents !== null) { this.elementEvents.disposeAll(); } for (i = 0, ii = boundProperties.length; i < ii; ++i) { boundProperties[i].binding.unbind(); } } } /** * Attaches the controller. */ attached(): void { if (this.isAttached) { return; } this.isAttached = true; if (this.behavior.handlesAttached) { (this.viewModel as ComponentAttached).attached(); } if (this.view !== null) { this.view.attached(); } } /** * Detaches the controller. */ detached(): void { if (this.isAttached) { this.isAttached = false; if (this.view !== null) { this.view.detached(); } if (this.behavior.handlesDetached) { (this.viewModel as ComponentDetached).detached(); } } } } /** @internal */ declare module 'aurelia-dependency-injection' { interface Container { elementEvents: ElementEvents; } } /** @internal */ declare module 'aurelia-binding' { interface ObserverLocator { getOrCreateObserversLookup(object: object): Record; } interface OverrideContext { __parentOverrideContext: OverrideContext; } }