UNPKG

@surface/custom-element

Version:

Provides support of directives and data binding on custom elements.

128 lines (127 loc) 5.11 kB
import { CancellationTokenSource } from "@surface/core"; import { tryEvaluateExpression, tryEvaluatePattern, tryObserveByObservable } from "../common.js"; import TemplateProcessor from "../processors/template-processor.js"; import { scheduler } from "../singletons.js"; import TemplateBlock from "./template-block.js"; class Cache { constructor() { this.entries = []; this.changed = false; this.cursor = 0; this.stored = 0; } trim() { if (this.stored > 0) { this.entries.splice(this.cursor, this.stored - this.cursor).forEach(x => (x[2].dispose(), x[1].dispose())); this.stored = 0; } } add(value, block, disposable) { this.entries.push([value, block, disposable]); } hasChanges(index, value) { if (this.changed || !Object.is(value, this.entries[index]?.[0])) { if (!this.changed) { this.changed = true; this.cursor = index; } return true; } return false; } resize(size) { if (!this.changed) { this.cursor = size; } this.trim(); this.changed = false; this.cursor = 0; this.stored = size; } } export default class LoopDirective { constructor(template, descriptor, context) { this.cache = new Cache(); this.cancellationTokenSource = new CancellationTokenSource(); this.templateBlock = new TemplateBlock(); this.disposed = false; this.task = () => { if (this.disposed) { return; } const elements = tryEvaluateExpression(this.context.scope, this.descriptor.right, this.descriptor.rawExpression, this.descriptor.stackTrace); if (elements[Symbol.iterator]().next().done) { this.cache.resize(0); } else { const size = this.iterator(elements, this.action); const task = () => { this.cache.resize(size); this.templateBlock.setContent(this.tree, false); }; void scheduler.enqueue(task, "high", this.cancellationTokenSource.token); } }; this.template = template; this.descriptor = descriptor; this.context = context; this.tree = document.createDocumentFragment(); this.iterator = descriptor.operator == "in" ? this.forInIterator : this.forOfIterator; const parent = this.template.parentNode; this.templateBlock.insertAt(parent, template); const listener = () => void scheduler.enqueue(this.task, "normal", this.cancellationTokenSource.token); this.subscription = tryObserveByObservable(context.scope, descriptor, listener, true); listener(); } action(value, index) { if (this.cache.hasChanges(index, value)) { const directiveScope = tryEvaluatePattern(this.context.scope, this.descriptor.left, value, this.descriptor.rawExpression, this.descriptor.stackTrace); const mergedScope = { $index: index, ...this.context.scope, ...directiveScope }; const content = this.template.content.cloneNode(true); const context = { directives: this.context.directives, host: this.context.host, parentNode: this.context.parentNode, root: content, scope: mergedScope, templateDescriptor: this.descriptor.descriptor, }; const disposable = TemplateProcessor.process(context); const block = new TemplateBlock(); this.cache.add(value, block, disposable); block.connect(this.tree); block.setContent(content); const count = index + 1; if (Math.ceil(count / LoopDirective.maximumAmount) * LoopDirective.maximumAmount == count) { this.cache.trim(); this.templateBlock.setContent(this.tree, false); } } } forOfIterator(elements) { let index = 0; for (const element of elements) { const current = index++; void scheduler.enqueue(() => this.action(element, current), "high", this.cancellationTokenSource.token); } return index; } forInIterator(elements) { let index = 0; for (const element in elements) { const current = index++; void scheduler.enqueue(() => this.action(element, current), "high", this.cancellationTokenSource.token); } return index; } dispose() { if (!this.disposed) { this.cancellationTokenSource.cancel(); this.cache.resize(0); this.subscription.unsubscribe(); this.templateBlock.dispose(); this.disposed = true; } } } LoopDirective.maximumAmount = 1000;