diff --git a/dist/amd/portal.d.ts b/dist/amd/portal.d.ts index 716e53b..d330d41 100644 --- a/dist/amd/portal.d.ts +++ b/dist/amd/portal.d.ts @@ -1,9 +1,10 @@ import { OverrideContext } from 'aurelia-binding'; -import { ViewSlot, BoundViewFactory } from 'aurelia-templating'; +import { ViewSlot, BoundViewFactory, View } from 'aurelia-templating'; +export declare type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; export declare class Portal { private viewFactory; private originalViewslot; - private static getTarget(target); + private static getTarget(target, context?); /** * Only needs the BoundViewFactory as a custom viewslot will be used */ @@ -12,17 +13,40 @@ export declare class Portal { * Target to render to, CSS string | Element */ target: string | Element | null | undefined; + renderContext: string | Element | null | undefined; strict: boolean; initialRender: boolean; + /** + * Will be called when the attribute receive new target after the first render. + */ + deactivating: PortalLifecycleCallback; + /** + * Will be called after `portaled` element has been added to target + */ + activating: PortalLifecycleCallback; + /** + * Will be called after activating has been resolved + */ + activated: PortalLifecycleCallback; + /** + * Will be called after deactivating has been resolved. + */ + deactivated: PortalLifecycleCallback; + /** + * The object that will becontextwhen calling life cycle methods above + */ + callbackContext: any; + private currentTarget; private isAttached; private viewSlot; private view; + private removed; constructor(viewFactory: BoundViewFactory, originalViewslot: ViewSlot); bind(bindingContext: any, overrideContext: OverrideContext): void; - attached(): void; + attached(): Promise | undefined; detached(): void; unbind(): void; private getTarget(); private render(); - targetChanged(): void; + targetChanged(): Promise | undefined; } diff --git a/dist/amd/portal.js b/dist/amd/portal.js index b15a109..49b02d4 100644 --- a/dist/amd/portal.js +++ b/dist/amd/portal.js @@ -16,19 +16,34 @@ define(["require", "exports", "aurelia-templating", "aurelia-pal"], function (re this.initialRender = false; } Portal_1 = Portal; - Portal.getTarget = function (target) { + Portal.getTarget = function (target, context) { if (target) { if (typeof target === 'string') { - target = document.querySelector(target); + var queryContext = document; + if (context) { + if (typeof context === 'string') { + context = document.querySelector(context); + } + if (context instanceof Element) { + queryContext = context; + } + } + target = queryContext.querySelector(target); } - if (target && (target instanceof Element)) { + if (target instanceof Element) { return target; } } return null; }; Portal.prototype.bind = function (bindingContext, overrideContext) { - var view = this.view = this.viewFactory.create(); + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } + var view = this.view; + if (!view) { + view = this.view = this.viewFactory.create(); + } var shouldInitRender = this.initialRender; if (shouldInitRender) { this.originalViewslot.add(view); @@ -40,19 +55,24 @@ define(["require", "exports", "aurelia-templating", "aurelia-pal"], function (re }; Portal.prototype.attached = function () { this.isAttached = true; - this.render(); + return this.render(); }; Portal.prototype.detached = function () { this.isAttached = false; - this.view.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } }; Portal.prototype.unbind = function () { - this.viewSlot.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; }; Portal.prototype.getTarget = function () { - var target = Portal_1.getTarget(this.target); + var target = Portal_1.getTarget(this.target, this.renderContext); if (target === null) { if (this.strict) { throw new Error('Render target not found.'); @@ -64,23 +84,53 @@ define(["require", "exports", "aurelia-templating", "aurelia-pal"], function (re return target; }; Portal.prototype.render = function () { + var _this = this; + var oldTarget = this.currentTarget; var view = this.view; - var target = this.getTarget(); + var target = this.currentTarget = this.getTarget(); var oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - if (this.isAttached) { - view.detached(); - } + if (oldTarget === target && oldViewSlot) { + return; } - if (this.isAttached) { - var viewSlot = this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); - viewSlot.add(view); - view.attached(); + var addAction = function () { + if (_this.isAttached) { + return Promise.resolve(typeof _this.activating === 'function' + ? _this.activating.call(_this.callbackContext, target, view) + : null).then(function () { + if (target === _this.currentTarget || oldTarget === unset) { + var viewSlot = _this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); + viewSlot.attached(); + viewSlot.add(view); + _this.removed = false; + } + return Promise.resolve().then(function () { + typeof _this.activated === 'function' + ? _this.activated.call(_this.callbackContext, target, view) + : null; + }); + }); + } + return Promise.resolve(null); + }; + if (oldViewSlot) { + return Promise.resolve(typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget, view) + : null).then(function () { + if (typeof _this.deactivated === 'function') { + _this.deactivated.call(_this.callbackContext, oldTarget, view); + } + _this.viewSlot = null; + if (!_this.removed) { + oldViewSlot.remove(view); + _this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); }; Portal.prototype.targetChanged = function () { - this.render(); + return this.render(); }; /** * Only needs the BoundViewFactory as a custom viewslot will be used @@ -92,12 +142,30 @@ define(["require", "exports", "aurelia-templating", "aurelia-pal"], function (re defaultValue: null }) ], Portal.prototype, "target", void 0); + __decorate([ + aurelia_templating_1.bindable({ changeHandler: 'targetChanged' }) + ], Portal.prototype, "renderContext", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "strict", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "initialRender", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "callbackContext", void 0); Portal = Portal_1 = __decorate([ aurelia_templating_1.templateController(), aurelia_templating_1.customAttribute('portal') @@ -106,4 +174,5 @@ define(["require", "exports", "aurelia-templating", "aurelia-pal"], function (re var Portal_1; }()); exports.Portal = Portal; + var unset = {}; }); diff --git a/dist/commonjs/portal.d.ts b/dist/commonjs/portal.d.ts index 716e53b..d330d41 100644 --- a/dist/commonjs/portal.d.ts +++ b/dist/commonjs/portal.d.ts @@ -1,9 +1,10 @@ import { OverrideContext } from 'aurelia-binding'; -import { ViewSlot, BoundViewFactory } from 'aurelia-templating'; +import { ViewSlot, BoundViewFactory, View } from 'aurelia-templating'; +export declare type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; export declare class Portal { private viewFactory; private originalViewslot; - private static getTarget(target); + private static getTarget(target, context?); /** * Only needs the BoundViewFactory as a custom viewslot will be used */ @@ -12,17 +13,40 @@ export declare class Portal { * Target to render to, CSS string | Element */ target: string | Element | null | undefined; + renderContext: string | Element | null | undefined; strict: boolean; initialRender: boolean; + /** + * Will be called when the attribute receive new target after the first render. + */ + deactivating: PortalLifecycleCallback; + /** + * Will be called after `portaled` element has been added to target + */ + activating: PortalLifecycleCallback; + /** + * Will be called after activating has been resolved + */ + activated: PortalLifecycleCallback; + /** + * Will be called after deactivating has been resolved. + */ + deactivated: PortalLifecycleCallback; + /** + * The object that will becontextwhen calling life cycle methods above + */ + callbackContext: any; + private currentTarget; private isAttached; private viewSlot; private view; + private removed; constructor(viewFactory: BoundViewFactory, originalViewslot: ViewSlot); bind(bindingContext: any, overrideContext: OverrideContext): void; - attached(): void; + attached(): Promise | undefined; detached(): void; unbind(): void; private getTarget(); private render(); - targetChanged(): void; + targetChanged(): Promise | undefined; } diff --git a/dist/commonjs/portal.js b/dist/commonjs/portal.js index a56ea1e..b73a860 100644 --- a/dist/commonjs/portal.js +++ b/dist/commonjs/portal.js @@ -17,19 +17,34 @@ var Portal = /** @class */ (function () { this.initialRender = false; } Portal_1 = Portal; - Portal.getTarget = function (target) { + Portal.getTarget = function (target, context) { if (target) { if (typeof target === 'string') { - target = document.querySelector(target); + var queryContext = document; + if (context) { + if (typeof context === 'string') { + context = document.querySelector(context); + } + if (context instanceof Element) { + queryContext = context; + } + } + target = queryContext.querySelector(target); } - if (target && (target instanceof Element)) { + if (target instanceof Element) { return target; } } return null; }; Portal.prototype.bind = function (bindingContext, overrideContext) { - var view = this.view = this.viewFactory.create(); + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } + var view = this.view; + if (!view) { + view = this.view = this.viewFactory.create(); + } var shouldInitRender = this.initialRender; if (shouldInitRender) { this.originalViewslot.add(view); @@ -41,19 +56,24 @@ var Portal = /** @class */ (function () { }; Portal.prototype.attached = function () { this.isAttached = true; - this.render(); + return this.render(); }; Portal.prototype.detached = function () { this.isAttached = false; - this.view.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } }; Portal.prototype.unbind = function () { - this.viewSlot.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; }; Portal.prototype.getTarget = function () { - var target = Portal_1.getTarget(this.target); + var target = Portal_1.getTarget(this.target, this.renderContext); if (target === null) { if (this.strict) { throw new Error('Render target not found.'); @@ -65,23 +85,53 @@ var Portal = /** @class */ (function () { return target; }; Portal.prototype.render = function () { + var _this = this; + var oldTarget = this.currentTarget; var view = this.view; - var target = this.getTarget(); + var target = this.currentTarget = this.getTarget(); var oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - if (this.isAttached) { - view.detached(); - } + if (oldTarget === target && oldViewSlot) { + return; } - if (this.isAttached) { - var viewSlot = this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); - viewSlot.add(view); - view.attached(); + var addAction = function () { + if (_this.isAttached) { + return Promise.resolve(typeof _this.activating === 'function' + ? _this.activating.call(_this.callbackContext, target, view) + : null).then(function () { + if (target === _this.currentTarget || oldTarget === unset) { + var viewSlot = _this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); + viewSlot.attached(); + viewSlot.add(view); + _this.removed = false; + } + return Promise.resolve().then(function () { + typeof _this.activated === 'function' + ? _this.activated.call(_this.callbackContext, target, view) + : null; + }); + }); + } + return Promise.resolve(null); + }; + if (oldViewSlot) { + return Promise.resolve(typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget, view) + : null).then(function () { + if (typeof _this.deactivated === 'function') { + _this.deactivated.call(_this.callbackContext, oldTarget, view); + } + _this.viewSlot = null; + if (!_this.removed) { + oldViewSlot.remove(view); + _this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); }; Portal.prototype.targetChanged = function () { - this.render(); + return this.render(); }; /** * Only needs the BoundViewFactory as a custom viewslot will be used @@ -93,12 +143,30 @@ var Portal = /** @class */ (function () { defaultValue: null }) ], Portal.prototype, "target", void 0); + __decorate([ + aurelia_templating_1.bindable({ changeHandler: 'targetChanged' }) + ], Portal.prototype, "renderContext", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "strict", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "initialRender", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "callbackContext", void 0); Portal = Portal_1 = __decorate([ aurelia_templating_1.templateController(), aurelia_templating_1.customAttribute('portal') @@ -107,3 +175,4 @@ var Portal = /** @class */ (function () { var Portal_1; }()); exports.Portal = Portal; +var unset = {}; diff --git a/dist/es2015/portal.d.ts b/dist/es2015/portal.d.ts index 716e53b..d330d41 100644 --- a/dist/es2015/portal.d.ts +++ b/dist/es2015/portal.d.ts @@ -1,9 +1,10 @@ import { OverrideContext } from 'aurelia-binding'; -import { ViewSlot, BoundViewFactory } from 'aurelia-templating'; +import { ViewSlot, BoundViewFactory, View } from 'aurelia-templating'; +export declare type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; export declare class Portal { private viewFactory; private originalViewslot; - private static getTarget(target); + private static getTarget(target, context?); /** * Only needs the BoundViewFactory as a custom viewslot will be used */ @@ -12,17 +13,40 @@ export declare class Portal { * Target to render to, CSS string | Element */ target: string | Element | null | undefined; + renderContext: string | Element | null | undefined; strict: boolean; initialRender: boolean; + /** + * Will be called when the attribute receive new target after the first render. + */ + deactivating: PortalLifecycleCallback; + /** + * Will be called after `portaled` element has been added to target + */ + activating: PortalLifecycleCallback; + /** + * Will be called after activating has been resolved + */ + activated: PortalLifecycleCallback; + /** + * Will be called after deactivating has been resolved. + */ + deactivated: PortalLifecycleCallback; + /** + * The object that will becontextwhen calling life cycle methods above + */ + callbackContext: any; + private currentTarget; private isAttached; private viewSlot; private view; + private removed; constructor(viewFactory: BoundViewFactory, originalViewslot: ViewSlot); bind(bindingContext: any, overrideContext: OverrideContext): void; - attached(): void; + attached(): Promise | undefined; detached(): void; unbind(): void; private getTarget(); private render(); - targetChanged(): void; + targetChanged(): Promise | undefined; } diff --git a/dist/es2015/portal.js b/dist/es2015/portal.js index 2bfa927..d5208d7 100644 --- a/dist/es2015/portal.js +++ b/dist/es2015/portal.js @@ -15,19 +15,34 @@ var Portal = /** @class */ (function () { this.initialRender = false; } Portal_1 = Portal; - Portal.getTarget = function (target) { + Portal.getTarget = function (target, context) { if (target) { if (typeof target === 'string') { - target = document.querySelector(target); + var queryContext = document; + if (context) { + if (typeof context === 'string') { + context = document.querySelector(context); + } + if (context instanceof Element) { + queryContext = context; + } + } + target = queryContext.querySelector(target); } - if (target && (target instanceof Element)) { + if (target instanceof Element) { return target; } } return null; }; Portal.prototype.bind = function (bindingContext, overrideContext) { - var view = this.view = this.viewFactory.create(); + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } + var view = this.view; + if (!view) { + view = this.view = this.viewFactory.create(); + } var shouldInitRender = this.initialRender; if (shouldInitRender) { this.originalViewslot.add(view); @@ -39,19 +54,24 @@ var Portal = /** @class */ (function () { }; Portal.prototype.attached = function () { this.isAttached = true; - this.render(); + return this.render(); }; Portal.prototype.detached = function () { this.isAttached = false; - this.view.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } }; Portal.prototype.unbind = function () { - this.viewSlot.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; }; Portal.prototype.getTarget = function () { - var target = Portal_1.getTarget(this.target); + var target = Portal_1.getTarget(this.target, this.renderContext); if (target === null) { if (this.strict) { throw new Error('Render target not found.'); @@ -63,23 +83,53 @@ var Portal = /** @class */ (function () { return target; }; Portal.prototype.render = function () { + var _this = this; + var oldTarget = this.currentTarget; var view = this.view; - var target = this.getTarget(); + var target = this.currentTarget = this.getTarget(); var oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - if (this.isAttached) { - view.detached(); - } + if (oldTarget === target && oldViewSlot) { + return; } - if (this.isAttached) { - var viewSlot = this.viewSlot = new ViewSlot(target, true); - viewSlot.add(view); - view.attached(); + var addAction = function () { + if (_this.isAttached) { + return Promise.resolve(typeof _this.activating === 'function' + ? _this.activating.call(_this.callbackContext, target, view) + : null).then(function () { + if (target === _this.currentTarget || oldTarget === unset) { + var viewSlot = _this.viewSlot = new ViewSlot(target, true); + viewSlot.attached(); + viewSlot.add(view); + _this.removed = false; + } + return Promise.resolve().then(function () { + typeof _this.activated === 'function' + ? _this.activated.call(_this.callbackContext, target, view) + : null; + }); + }); + } + return Promise.resolve(null); + }; + if (oldViewSlot) { + return Promise.resolve(typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget, view) + : null).then(function () { + if (typeof _this.deactivated === 'function') { + _this.deactivated.call(_this.callbackContext, oldTarget, view); + } + _this.viewSlot = null; + if (!_this.removed) { + oldViewSlot.remove(view); + _this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); }; Portal.prototype.targetChanged = function () { - this.render(); + return this.render(); }; /** * Only needs the BoundViewFactory as a custom viewslot will be used @@ -91,12 +141,30 @@ var Portal = /** @class */ (function () { defaultValue: null }) ], Portal.prototype, "target", void 0); + __decorate([ + bindable({ changeHandler: 'targetChanged' }) + ], Portal.prototype, "renderContext", void 0); __decorate([ bindable() ], Portal.prototype, "strict", void 0); __decorate([ bindable() ], Portal.prototype, "initialRender", void 0); + __decorate([ + bindable() + ], Portal.prototype, "deactivating", void 0); + __decorate([ + bindable() + ], Portal.prototype, "activating", void 0); + __decorate([ + bindable() + ], Portal.prototype, "activated", void 0); + __decorate([ + bindable() + ], Portal.prototype, "deactivated", void 0); + __decorate([ + bindable() + ], Portal.prototype, "callbackContext", void 0); Portal = Portal_1 = __decorate([ templateController(), customAttribute('portal') @@ -105,3 +173,4 @@ var Portal = /** @class */ (function () { var Portal_1; }()); export { Portal }; +var unset = {}; diff --git a/dist/native-modules/portal.d.ts b/dist/native-modules/portal.d.ts index 716e53b..d330d41 100644 --- a/dist/native-modules/portal.d.ts +++ b/dist/native-modules/portal.d.ts @@ -1,9 +1,10 @@ import { OverrideContext } from 'aurelia-binding'; -import { ViewSlot, BoundViewFactory } from 'aurelia-templating'; +import { ViewSlot, BoundViewFactory, View } from 'aurelia-templating'; +export declare type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; export declare class Portal { private viewFactory; private originalViewslot; - private static getTarget(target); + private static getTarget(target, context?); /** * Only needs the BoundViewFactory as a custom viewslot will be used */ @@ -12,17 +13,40 @@ export declare class Portal { * Target to render to, CSS string | Element */ target: string | Element | null | undefined; + renderContext: string | Element | null | undefined; strict: boolean; initialRender: boolean; + /** + * Will be called when the attribute receive new target after the first render. + */ + deactivating: PortalLifecycleCallback; + /** + * Will be called after `portaled` element has been added to target + */ + activating: PortalLifecycleCallback; + /** + * Will be called after activating has been resolved + */ + activated: PortalLifecycleCallback; + /** + * Will be called after deactivating has been resolved. + */ + deactivated: PortalLifecycleCallback; + /** + * The object that will becontextwhen calling life cycle methods above + */ + callbackContext: any; + private currentTarget; private isAttached; private viewSlot; private view; + private removed; constructor(viewFactory: BoundViewFactory, originalViewslot: ViewSlot); bind(bindingContext: any, overrideContext: OverrideContext): void; - attached(): void; + attached(): Promise | undefined; detached(): void; unbind(): void; private getTarget(); private render(); - targetChanged(): void; + targetChanged(): Promise | undefined; } diff --git a/dist/native-modules/portal.js b/dist/native-modules/portal.js index 2bfa927..d5208d7 100644 --- a/dist/native-modules/portal.js +++ b/dist/native-modules/portal.js @@ -15,19 +15,34 @@ var Portal = /** @class */ (function () { this.initialRender = false; } Portal_1 = Portal; - Portal.getTarget = function (target) { + Portal.getTarget = function (target, context) { if (target) { if (typeof target === 'string') { - target = document.querySelector(target); + var queryContext = document; + if (context) { + if (typeof context === 'string') { + context = document.querySelector(context); + } + if (context instanceof Element) { + queryContext = context; + } + } + target = queryContext.querySelector(target); } - if (target && (target instanceof Element)) { + if (target instanceof Element) { return target; } } return null; }; Portal.prototype.bind = function (bindingContext, overrideContext) { - var view = this.view = this.viewFactory.create(); + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } + var view = this.view; + if (!view) { + view = this.view = this.viewFactory.create(); + } var shouldInitRender = this.initialRender; if (shouldInitRender) { this.originalViewslot.add(view); @@ -39,19 +54,24 @@ var Portal = /** @class */ (function () { }; Portal.prototype.attached = function () { this.isAttached = true; - this.render(); + return this.render(); }; Portal.prototype.detached = function () { this.isAttached = false; - this.view.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } }; Portal.prototype.unbind = function () { - this.viewSlot.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; }; Portal.prototype.getTarget = function () { - var target = Portal_1.getTarget(this.target); + var target = Portal_1.getTarget(this.target, this.renderContext); if (target === null) { if (this.strict) { throw new Error('Render target not found.'); @@ -63,23 +83,53 @@ var Portal = /** @class */ (function () { return target; }; Portal.prototype.render = function () { + var _this = this; + var oldTarget = this.currentTarget; var view = this.view; - var target = this.getTarget(); + var target = this.currentTarget = this.getTarget(); var oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - if (this.isAttached) { - view.detached(); - } + if (oldTarget === target && oldViewSlot) { + return; } - if (this.isAttached) { - var viewSlot = this.viewSlot = new ViewSlot(target, true); - viewSlot.add(view); - view.attached(); + var addAction = function () { + if (_this.isAttached) { + return Promise.resolve(typeof _this.activating === 'function' + ? _this.activating.call(_this.callbackContext, target, view) + : null).then(function () { + if (target === _this.currentTarget || oldTarget === unset) { + var viewSlot = _this.viewSlot = new ViewSlot(target, true); + viewSlot.attached(); + viewSlot.add(view); + _this.removed = false; + } + return Promise.resolve().then(function () { + typeof _this.activated === 'function' + ? _this.activated.call(_this.callbackContext, target, view) + : null; + }); + }); + } + return Promise.resolve(null); + }; + if (oldViewSlot) { + return Promise.resolve(typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget, view) + : null).then(function () { + if (typeof _this.deactivated === 'function') { + _this.deactivated.call(_this.callbackContext, oldTarget, view); + } + _this.viewSlot = null; + if (!_this.removed) { + oldViewSlot.remove(view); + _this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); }; Portal.prototype.targetChanged = function () { - this.render(); + return this.render(); }; /** * Only needs the BoundViewFactory as a custom viewslot will be used @@ -91,12 +141,30 @@ var Portal = /** @class */ (function () { defaultValue: null }) ], Portal.prototype, "target", void 0); + __decorate([ + bindable({ changeHandler: 'targetChanged' }) + ], Portal.prototype, "renderContext", void 0); __decorate([ bindable() ], Portal.prototype, "strict", void 0); __decorate([ bindable() ], Portal.prototype, "initialRender", void 0); + __decorate([ + bindable() + ], Portal.prototype, "deactivating", void 0); + __decorate([ + bindable() + ], Portal.prototype, "activating", void 0); + __decorate([ + bindable() + ], Portal.prototype, "activated", void 0); + __decorate([ + bindable() + ], Portal.prototype, "deactivated", void 0); + __decorate([ + bindable() + ], Portal.prototype, "callbackContext", void 0); Portal = Portal_1 = __decorate([ templateController(), customAttribute('portal') @@ -105,3 +173,4 @@ var Portal = /** @class */ (function () { var Portal_1; }()); export { Portal }; +var unset = {}; diff --git a/dist/system/portal.d.ts b/dist/system/portal.d.ts index 716e53b..d330d41 100644 --- a/dist/system/portal.d.ts +++ b/dist/system/portal.d.ts @@ -1,9 +1,10 @@ import { OverrideContext } from 'aurelia-binding'; -import { ViewSlot, BoundViewFactory } from 'aurelia-templating'; +import { ViewSlot, BoundViewFactory, View } from 'aurelia-templating'; +export declare type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; export declare class Portal { private viewFactory; private originalViewslot; - private static getTarget(target); + private static getTarget(target, context?); /** * Only needs the BoundViewFactory as a custom viewslot will be used */ @@ -12,17 +13,40 @@ export declare class Portal { * Target to render to, CSS string | Element */ target: string | Element | null | undefined; + renderContext: string | Element | null | undefined; strict: boolean; initialRender: boolean; + /** + * Will be called when the attribute receive new target after the first render. + */ + deactivating: PortalLifecycleCallback; + /** + * Will be called after `portaled` element has been added to target + */ + activating: PortalLifecycleCallback; + /** + * Will be called after activating has been resolved + */ + activated: PortalLifecycleCallback; + /** + * Will be called after deactivating has been resolved. + */ + deactivated: PortalLifecycleCallback; + /** + * The object that will becontextwhen calling life cycle methods above + */ + callbackContext: any; + private currentTarget; private isAttached; private viewSlot; private view; + private removed; constructor(viewFactory: BoundViewFactory, originalViewslot: ViewSlot); bind(bindingContext: any, overrideContext: OverrideContext): void; - attached(): void; + attached(): Promise | undefined; detached(): void; unbind(): void; private getTarget(); private render(); - targetChanged(): void; + targetChanged(): Promise | undefined; } diff --git a/dist/system/portal.js b/dist/system/portal.js index bd55c96..fef6133 100644 --- a/dist/system/portal.js +++ b/dist/system/portal.js @@ -7,7 +7,7 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont return c > 3 && r && Object.defineProperty(target, key, r), r; }; var __moduleName = context_1 && context_1.id; - var aurelia_templating_1, aurelia_pal_1, document, Portal; + var aurelia_templating_1, aurelia_pal_1, document, Portal, unset; return { setters: [ function (aurelia_templating_1_1) { @@ -27,19 +27,34 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont this.initialRender = false; } Portal_1 = Portal; - Portal.getTarget = function (target) { + Portal.getTarget = function (target, context) { if (target) { if (typeof target === 'string') { - target = document.querySelector(target); + var queryContext = document; + if (context) { + if (typeof context === 'string') { + context = document.querySelector(context); + } + if (context instanceof Element) { + queryContext = context; + } + } + target = queryContext.querySelector(target); } - if (target && (target instanceof Element)) { + if (target instanceof Element) { return target; } } return null; }; Portal.prototype.bind = function (bindingContext, overrideContext) { - var view = this.view = this.viewFactory.create(); + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } + var view = this.view; + if (!view) { + view = this.view = this.viewFactory.create(); + } var shouldInitRender = this.initialRender; if (shouldInitRender) { this.originalViewslot.add(view); @@ -51,19 +66,24 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont }; Portal.prototype.attached = function () { this.isAttached = true; - this.render(); + return this.render(); }; Portal.prototype.detached = function () { this.isAttached = false; - this.view.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } }; Portal.prototype.unbind = function () { - this.viewSlot.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; }; Portal.prototype.getTarget = function () { - var target = Portal_1.getTarget(this.target); + var target = Portal_1.getTarget(this.target, this.renderContext); if (target === null) { if (this.strict) { throw new Error('Render target not found.'); @@ -75,23 +95,53 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont return target; }; Portal.prototype.render = function () { + var _this = this; + var oldTarget = this.currentTarget; var view = this.view; - var target = this.getTarget(); + var target = this.currentTarget = this.getTarget(); var oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - if (this.isAttached) { - view.detached(); - } + if (oldTarget === target && oldViewSlot) { + return; } - if (this.isAttached) { - var viewSlot = this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); - viewSlot.add(view); - view.attached(); + var addAction = function () { + if (_this.isAttached) { + return Promise.resolve(typeof _this.activating === 'function' + ? _this.activating.call(_this.callbackContext, target, view) + : null).then(function () { + if (target === _this.currentTarget || oldTarget === unset) { + var viewSlot = _this.viewSlot = new aurelia_templating_1.ViewSlot(target, true); + viewSlot.attached(); + viewSlot.add(view); + _this.removed = false; + } + return Promise.resolve().then(function () { + typeof _this.activated === 'function' + ? _this.activated.call(_this.callbackContext, target, view) + : null; + }); + }); + } + return Promise.resolve(null); + }; + if (oldViewSlot) { + return Promise.resolve(typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget, view) + : null).then(function () { + if (typeof _this.deactivated === 'function') { + _this.deactivated.call(_this.callbackContext, oldTarget, view); + } + _this.viewSlot = null; + if (!_this.removed) { + oldViewSlot.remove(view); + _this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); }; Portal.prototype.targetChanged = function () { - this.render(); + return this.render(); }; /** * Only needs the BoundViewFactory as a custom viewslot will be used @@ -103,12 +153,30 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont defaultValue: null }) ], Portal.prototype, "target", void 0); + __decorate([ + aurelia_templating_1.bindable({ changeHandler: 'targetChanged' }) + ], Portal.prototype, "renderContext", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "strict", void 0); __decorate([ aurelia_templating_1.bindable() ], Portal.prototype, "initialRender", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activating", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "activated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "deactivated", void 0); + __decorate([ + aurelia_templating_1.bindable() + ], Portal.prototype, "callbackContext", void 0); Portal = Portal_1 = __decorate([ aurelia_templating_1.templateController(), aurelia_templating_1.customAttribute('portal') @@ -117,6 +185,7 @@ System.register(["aurelia-templating", "aurelia-pal"], function (exports_1, cont var Portal_1; }()); exports_1("Portal", Portal); + unset = {}; } }; }); diff --git a/src/portal.ts b/src/portal.ts index 78c2b67..5dbd96f 100644 --- a/src/portal.ts +++ b/src/portal.ts @@ -13,6 +13,8 @@ const document: Document = PLATFORM.global.document; type PortalTarget = string | Element | null | undefined +export type PortalLifecycleCallback = (target: Element, view: View) => Promise | any; + @templateController() @customAttribute('portal') export class Portal { @@ -56,9 +58,32 @@ export class Portal { @bindable() public strict: boolean = false; @bindable() public initialRender: boolean = false; + /** + * Will be called when the attribute receive new target after the first render. + */ + @bindable() public deactivating: PortalLifecycleCallback + /** + * Will be called after `portaled` element has been added to target + */ + @bindable() public activating: PortalLifecycleCallback + /** + * Will be called after activating has been resolved + */ + @bindable() public activated: PortalLifecycleCallback + /** + * Will be called after deactivating has been resolved. + */ + @bindable() public deactivated: PortalLifecycleCallback + /** + * The object that will becontextwhen calling life cycle methods above + */ + @bindable() public callbackContext: any + + private currentTarget: typeof unset | Element | null private isAttached: boolean; private viewSlot: ViewSlot | null; private view: View; + private removed: boolean; constructor( private viewFactory: BoundViewFactory, @@ -67,6 +92,9 @@ export class Portal { } public bind(bindingContext: any, overrideContext: OverrideContext) { + if (!this.callbackContext) { + this.callbackContext = bindingContext; + } let view = this.view; if (!view) { view = this.view = this.viewFactory.create(); @@ -83,18 +111,23 @@ export class Portal { public attached() { this.isAttached = true; - this.render(); + return this.render(); } public detached() { this.isAttached = false; - this.viewSlot!.detached(); + if (this.viewSlot) { + this.viewSlot.detached(); + } } public unbind() { - this.viewSlot!.remove(this.view); + if (this.viewSlot) { + this.viewSlot.remove(this.view); + this.viewSlot = null; + } this.view.unbind(); - this.viewSlot = null; + this.callbackContext = null; } private getTarget(): Element | null { @@ -110,21 +143,61 @@ export class Portal { } private render() { + const oldTarget = this.currentTarget; const view = this.view; - const target = this.getTarget(); + const target = this.currentTarget = this.getTarget(); const oldViewSlot = this.viewSlot; - if (oldViewSlot) { - oldViewSlot.remove(view); - this.viewSlot = null; + + if (oldTarget === target && oldViewSlot) { + return; + } + + let addAction = () => { + if (this.isAttached) { + return Promise.resolve( + typeof this.activating === 'function' + ? this.activating.call(this.callbackContext, target!, view) + : null + ).then(() => { + if (target === this.currentTarget || oldTarget === unset) { + const viewSlot = this.viewSlot = new ViewSlot(target!, true); + viewSlot.attached(); + viewSlot.add(view); + this.removed = false; + } + return Promise.resolve().then(() => { + typeof this.activated === 'function' + ? this.activated.call(this.callbackContext, target!, view) + : null + }); + }); + } + return Promise.resolve(null); } - if (this.isAttached) { - const viewSlot = this.viewSlot = new ViewSlot(target!, true); - viewSlot.attached(); - viewSlot.add(view); + + if (oldViewSlot) { + return Promise.resolve( + typeof this.deactivating === 'function' + ? this.deactivating.call(this.callbackContext, oldTarget as Element, view) + : null + ).then(() => { + if (typeof this.deactivated === 'function') { + this.deactivated.call(this.callbackContext, oldTarget as Element, view); + } + this.viewSlot = null; + if (!this.removed) { + oldViewSlot.remove(view); + this.removed = true; + } + return addAction(); + }); } + return Promise.resolve(addAction()); } public targetChanged() { - this.render(); + return this.render(); } } + +const unset = {}; diff --git a/test/unit/portal.spec.ts b/test/unit/portal.spec.ts index 3ca0a5a..9718194 100644 --- a/test/unit/portal.spec.ts +++ b/test/unit/portal.spec.ts @@ -104,69 +104,94 @@ describe('Portal attribute', () => { describe('rendering', () => { - it('renders to body when target is not specified', () => { + it('renders to body when target is not specified', async (done) => { portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.body.lastElementChild.tagName).toBe('FORM'); + done(); }); - it('renders to specified target via CSS selector', () => { + it('renders to specified target via CSS selector', async (done) => { portal.target = '.square3'; portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.querySelector('.square3 form')).not.toBeFalsy(); + done(); }); - it('renders with renderContext', () => { + it('renders with renderContext', async (done) => { portal.target = '.square1'; portal.renderContext = '.round'; portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.querySelector('.round form')).not.toBeFalsy(); + done(); }); - it('renders to first square with renderContext selector query with no result', () => { + it('renders to first square with renderContext selector query with no result', async (done) => { portal.target = '.square1'; portal.renderContext = '.super-round'; portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.querySelector('.round form')).toBe(null); expect(document.querySelector('.square1 form')).not.toBeFalsy(); + done(); }); - it('re-renders after target has changed', () => { + it('does not re-render when target has not been changed', async (done) => { + let callCount = 0; + portal.activating = function() { + callCount++; + } + portal.bind(bindingContext, overrideContext); + await portal.attached(); + + expect(callCount).toBe(1); + await portal.targetChanged(); + await portal.targetChanged(); + + expect(callCount).toBe(1); + + done(); + }); + + it('re-renders after target has changed', async (done) => { portal.target = '.square2'; portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.querySelector('.square2 form')).not.toBeFalsy(); portal.target = '.square3'; - portal.targetChanged(); + await portal.targetChanged(); expect(document.querySelector('.square2 form')).toBeFalsy(); expect(document.querySelector('.square3 form')).not.toBeFalsy(); + + done(); }); - it('does not render when not attached', () => { + it('does not render when not attached', async (done) => { portal.bind(bindingContext, overrideContext); - portal.targetChanged(); + await portal.targetChanged(); expect(document.querySelector('form')).toBe(null); expect((portal as any).viewSlot).toBe(undefined); - portal.attached(); + await portal.attached(); expect(document.querySelector('form')).not.toBeFalsy(); portal.detached(); + + done(); }); it('throws in strict mode', () => { @@ -177,25 +202,84 @@ describe('Portal attribute', () => { expect(() => portal.attached()).toThrow(); }); - it('call detached()', () => { + it('call detached()', async (done) => { portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); spyOn(view, 'detached').and.callThrough(); portal.detached(); expect(view.detached).toHaveBeenCalled(); + + done(); }); }); - it('un-renders', () => { + describe('life-cycle', () => { + + it('calls life cycle', async (done) => { + let activateCalled = false; + let activatedCalled = false; + + let deactivateCalled = false; + let deactivatingResolvedTime: number; + + let deactivatedCalled = false; + let deactivatedResolvedTime: number; + + portal.activating = function() { + activateCalled = true; + } + portal.activated = function() { + activatedCalled = true; + } + portal.deactivating = function() { + return new Promise((resolve) => { + setTimeout(() => { + deactivateCalled = true; + deactivatingResolvedTime = +new Date(); + resolve(); + }, 1000); + }); + } + portal.deactivated = function() { + deactivatedCalled = true; + deactivatedResolvedTime = +new Date(); + } + + portal.bind(bindingContext, overrideContext); + await portal.attached(); + + expect(activateCalled).toBe(true); + expect(activatedCalled).toBe(true); + + portal.target = '.square3'; + + expect((portal as any).viewSlot).toBeTruthy(); + await portal.targetChanged(); + + expect(deactivateCalled).toBe(true); + expect(deactivatedCalled).toBe(true); + + // deactivated will be called immediately after resolving deactivate + // In this test it should be less than 10 + expect(Math.abs(deactivatedResolvedTime - deactivatingResolvedTime)).toBeLessThan(10); + + done(); + }); + + }); + + it('un-renders', async (done) => { portal.bind(bindingContext, overrideContext); - portal.attached(); + await portal.attached(); expect(document.querySelector('form')).not.toBeFalsy(); portal.detached(); portal.unbind(); expect(document.querySelector('form')).toBeFalsy(); + + done(); }); });