@microsoft/mgt
Version:
The Microsoft Graph Toolkit
177 lines (152 loc) • 5.27 kB
text/typescript
/**
* -------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All Rights Reserved. Licensed under the MIT License.
* See License in the project root for license information.
* -------------------------------------------------------------------------------------------
*/
import { html, PropertyValues } from 'lit-element';
import { equals } from '../utils/Utils';
import { MgtBaseComponent } from './baseComponent';
import { TemplateHelper } from './templateHelper';
/**
* Lookup for rendered component templates and contexts by slot name.
*/
interface RenderedTemplates {
[name: string]: {
/**
* Reference to the data context used to render the slot.
*/
context: any;
/**
* Reference to the rendered DOM element corresponding to the slot.
*/
slot: HTMLElement;
};
}
/**
* An abstract class that defines a templatable web component
*
* @export
* @abstract
* @class MgtTemplatedComponent
* @extends {MgtBaseComponent}
*/
export abstract class MgtTemplatedComponent extends MgtBaseComponent {
/**
* Collection of functions to be used in template binding
*
* @type {*}
* @memberof MgtTemplatedComponent
*/
public templateConverters: any = {};
/**
* Holds all templates defined by developer
*
* @protected
* @memberof MgtTemplatedComponent
*/
protected templates = {};
private _renderedSlots = false;
private _renderedTemplates: RenderedTemplates = {};
private _slotNamesAddedDuringRender = [];
constructor() {
super();
this.templateConverters.lower = (str: string) => str.toLowerCase();
this.templateConverters.upper = (str: string) => str.toUpperCase();
}
/**
* Updates the element. This method reflects property values to attributes.
* It can be overridden to render and keep updated element DOM.
* Setting properties inside this method will *not* trigger
* another update.
*
* * @param _changedProperties Map of changed properties with old values
*/
protected update(changedProperties) {
this.templates = this.getTemplates();
this._slotNamesAddedDuringRender = [];
super.update(changedProperties);
}
/**
* Invoked whenever the element is updated. Implement to perform
* post-updating tasks via DOM APIs, for example, focusing an element.
*
* Setting properties inside this method will trigger the element to update
* again after this update cycle completes.
*
* * @param changedProperties Map of changed properties with old values
*/
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
this.removeUnusedSlottedElements();
}
/**
* Render a <template> by type and return content to render
*
* @param templateType type of template (indicated by the data-type attribute)
* @param context the data context that should be expanded in template
* @param slotName the slot name that will be used to host the new rendered template. set to a unique value if multiple templates of this type will be rendered. default is templateType
*/
protected renderTemplate(templateType: string, context: object, slotName?: string) {
if (!this.templates[templateType]) {
return null;
}
slotName = slotName || templateType;
this._slotNamesAddedDuringRender.push(slotName);
this._renderedSlots = true;
const template = html`
<slot name=${slotName}></slot>
`;
if (this._renderedTemplates.hasOwnProperty(slotName)) {
const { context: existingContext, slot } = this._renderedTemplates[slotName];
if (equals(existingContext, context)) {
return template;
}
this.removeChild(slot);
}
const templateContent = TemplateHelper.renderTemplate(
this.templates[templateType],
context,
this.templateConverters
);
const div = document.createElement('div');
div.slot = slotName;
div.dataset.generated = 'template';
if (templateContent) {
div.appendChild(templateContent);
}
this.appendChild(div);
this._renderedTemplates[slotName] = { context, slot: div };
this.fireCustomEvent('templateRendered', { templateType, context, element: div });
return template;
}
private getTemplates() {
const templates: any = {};
// tslint:disable-next-line: prefer-for-of
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i];
if (child.nodeName === 'TEMPLATE') {
const template = child as HTMLElement;
if (template.dataset.type) {
templates[template.dataset.type] = template;
} else {
templates.default = template;
}
}
}
return templates;
}
private removeUnusedSlottedElements() {
if (this._renderedSlots) {
for (let i = 0; i < this.children.length; i++) {
const child = this.children[i] as HTMLElement;
if (child.dataset && child.dataset.generated && !this._slotNamesAddedDuringRender.includes(child.slot)) {
this.removeChild(child);
delete this._renderedTemplates[child.slot];
i--;
}
}
this._renderedSlots = false;
}
}
}