UNPKG

13.2 kBPlain TextView Raw
1/*eslint no-loop-func:0, no-unused-vars:0*/
2import {inject} from 'aurelia-dependency-injection';
3import {ObserverLocator, BindingExpression} from 'aurelia-binding';
4import {
5 BoundViewFactory,
6 TargetInstruction,
7 ViewSlot,
8 ViewResources,
9 customAttribute,
10 bindable,
11 templateController,
12 View,
13 ViewFactory
14} from 'aurelia-templating';
15import {RepeatStrategyLocator} from './repeat-strategy-locator';
16import {
17 getItemsSourceExpression,
18 unwrapExpression,
19 isOneTime,
20 updateOneTimeBinding
21} from './repeat-utilities';
22import {viewsRequireLifecycle} from './analyze-view-factory';
23import {AbstractRepeater} from './abstract-repeater';
24
25const matcherExtractionMarker = '__marker_extracted__';
26
27/**
28 * Binding to iterate over iterable objects (Array, Map and Number) to genereate a template for each iteration.
29 */
30@customAttribute('repeat')
31@templateController
32@inject(BoundViewFactory, TargetInstruction, ViewSlot, ViewResources, ObserverLocator, RepeatStrategyLocator)
33export class Repeat extends AbstractRepeater {
34
35 /**
36 * Setting this to `true` to enable legacy behavior, where a repeat would take first `matcher` binding
37 * any where inside its view if there's no `matcher` binding on the repeated element itself.
38 *
39 * Default value is true to avoid breaking change
40 * @default true
41 */
42 static useInnerMatcher = true;
43
44 /**
45 * List of items to bind the repeater to.
46 *
47 * @property items
48 */
49 @bindable items;
50 /**
51 * Local variable which gets assigned on each iteration.
52 *
53 * @property local
54 */
55 @bindable local;
56 /**
57 * Key when iterating over Maps.
58 *
59 * @property key
60 */
61 @bindable key;
62 /**
63 * Value when iterating over Maps.
64 *
65 * @property value
66 */
67 @bindable value;
68
69 /**@internal*/
70 viewFactory: any;
71 /**@internal*/
72 instruction: any;
73 /**@internal*/
74 viewSlot: any;
75 /**@internal*/
76 lookupFunctions: any;
77 /**@internal*/
78 observerLocator: any;
79 /**@internal*/
80 strategyLocator: any;
81 /**@internal*/
82 ignoreMutation: boolean;
83 /**@internal*/
84 sourceExpression: any;
85 /**@internal*/
86 isOneTime: any;
87 /**@internal*/
88 viewsRequireLifecycle: any;
89 /**@internal*/
90 scope: { bindingContext: any; overrideContext: any; };
91 /**@internal*/
92 matcherBinding: any;
93 /**@internal*/
94 collectionObserver: any;
95 /**@internal*/
96 strategy: any;
97 /**@internal */
98 callContext: 'handleCollectionMutated' | 'handleInnerCollectionMutated';
99
100 /**
101 * Creates an instance of Repeat.
102 * @param viewFactory The factory generating the view
103 * @param instruction The instructions for how the element should be enhanced.
104 * @param viewResources Collection of resources used to compile the the views.
105 * @param viewSlot The slot the view is injected in to.
106 * @param observerLocator The observer locator instance.
107 * @param collectionStrategyLocator The strategy locator to locate best strategy to iterate the collection.
108 */
109 constructor(viewFactory, instruction, viewSlot, viewResources, observerLocator, strategyLocator) {
110 super({
111 local: 'item',
112 viewsRequireLifecycle: viewsRequireLifecycle(viewFactory)
113 });
114
115 this.viewFactory = viewFactory;
116 this.instruction = instruction;
117 this.viewSlot = viewSlot;
118 this.lookupFunctions = viewResources.lookupFunctions;
119 this.observerLocator = observerLocator;
120 this.key = 'key';
121 this.value = 'value';
122 this.strategyLocator = strategyLocator;
123 this.ignoreMutation = false;
124 this.sourceExpression = getItemsSourceExpression(this.instruction, 'repeat.for');
125 this.isOneTime = isOneTime(this.sourceExpression);
126 this.viewsRequireLifecycle = viewsRequireLifecycle(viewFactory);
127 }
128
129 call(context, changes) {
130 this[context](this.items, changes);
131 }
132
133 /**
134 * Binds the repeat to the binding context and override context.
135 * @param bindingContext The binding context.
136 * @param overrideContext An override context for binding.
137 */
138 bind(bindingContext, overrideContext) {
139 this.scope = { bindingContext, overrideContext };
140 const instruction = this.instruction;
141 if (!(matcherExtractionMarker in instruction)) {
142 instruction[matcherExtractionMarker] = this._captureAndRemoveMatcherBinding();
143 }
144 this.matcherBinding = instruction[matcherExtractionMarker];
145 this.itemsChanged();
146 }
147
148 /**
149 * Unbinds the repeat
150 */
151 unbind() {
152 this.scope = null;
153 this.items = null;
154 this.matcherBinding = null;
155 this.viewSlot.removeAll(true, true);
156 this._unsubscribeCollection();
157 }
158
159 /**
160 * @internal
161 */
162 _unsubscribeCollection() {
163 if (this.collectionObserver) {
164 this.collectionObserver.unsubscribe(this.callContext, this);
165 this.collectionObserver = null;
166 this.callContext = null;
167 }
168 }
169
170 /**
171 * Invoked everytime the item property changes.
172 */
173 itemsChanged() {
174 this._unsubscribeCollection();
175
176 // still bound?
177 if (!this.scope) {
178 return;
179 }
180
181 let items = this.items;
182 this.strategy = this.strategyLocator.getStrategy(items);
183 if (!this.strategy) {
184 throw new Error(`Value for '${this.sourceExpression}' is non-repeatable`);
185 }
186
187 if (!this.isOneTime && !this._observeInnerCollection()) {
188 this._observeCollection();
189 }
190 this.ignoreMutation = true;
191 this.strategy.instanceChanged(this, items);
192 this.observerLocator.taskQueue.queueMicroTask(() => {
193 this.ignoreMutation = false;
194 });
195 }
196
197 /**
198 * @internal
199 */
200 _getInnerCollection() {
201 let expression = unwrapExpression(this.sourceExpression);
202 if (!expression) {
203 return null;
204 }
205 return expression.evaluate(this.scope, null);
206 }
207
208 /**
209 * Invoked when the underlying collection changes.
210 */
211 handleCollectionMutated(collection, changes) {
212 if (!this.collectionObserver) {
213 return;
214 }
215 if (this.ignoreMutation) {
216 return;
217 }
218 this.strategy.instanceMutated(this, collection, changes);
219 }
220
221 /**
222 * Invoked when the underlying inner collection changes.
223 */
224 // eslint-disable-next-line @typescript-eslint/no-unused-vars
225 handleInnerCollectionMutated(collection, changes) {
226 if (!this.collectionObserver) {
227 return;
228 }
229 // guard against source expressions that have observable side-effects that could
230 // cause an infinite loop- eg a value converter that mutates the source array.
231 if (this.ignoreMutation) {
232 return;
233 }
234 this.ignoreMutation = true;
235 let newItems = this.sourceExpression.evaluate(this.scope, this.lookupFunctions);
236 this.observerLocator.taskQueue.queueMicroTask(() => this.ignoreMutation = false);
237
238 // call itemsChanged...
239 if (newItems === this.items) {
240 // call itemsChanged directly.
241 this.itemsChanged();
242 } else {
243 // call itemsChanged indirectly by assigning the new collection value to
244 // the items property, which will trigger the self-subscriber to call itemsChanged.
245 this.items = newItems;
246 }
247 }
248
249 /**
250 * @internal
251 */
252 _observeInnerCollection() {
253 let items = this._getInnerCollection();
254 let strategy = this.strategyLocator.getStrategy(items);
255 if (!strategy) {
256 return false;
257 }
258 this.collectionObserver = strategy.getCollectionObserver(this.observerLocator, items);
259 if (!this.collectionObserver) {
260 return false;
261 }
262 this.callContext = 'handleInnerCollectionMutated';
263 this.collectionObserver.subscribe(this.callContext, this);
264 return true;
265 }
266
267 /**
268 * @internal
269 */
270 _observeCollection() {
271 let items = this.items;
272 this.collectionObserver = this.strategy.getCollectionObserver(this.observerLocator, items);
273 if (this.collectionObserver) {
274 this.callContext = 'handleCollectionMutated';
275 this.collectionObserver.subscribe(this.callContext, this);
276 }
277 }
278
279 /**
280 * Capture and remove matcher binding is a way to cache matcher binding + reduce redundant work
281 * caused by multiple unnecessary matcher bindings
282 * @internal
283 */
284 _captureAndRemoveMatcherBinding() {
285 const viewFactory: ViewFactory = this.viewFactory.viewFactory;
286 if (viewFactory) {
287 const template = viewFactory.template;
288 const instructions = viewFactory.instructions as Record<string, TargetInstruction>;
289 // legacy behavior enabled when Repeat.useInnerMathcer === true
290 if (Repeat.useInnerMatcher) {
291 return extractMatcherBindingExpression(instructions);
292 }
293 // if the template has more than 1 immediate child element
294 // it's a repeat put on a <template/> element
295 // not valid for matcher binding
296 if (getChildrenCount(template) > 1) {
297 return undefined;
298 }
299 // if the root element does not have any instruction
300 // it means there's no matcher binding
301 // no need to do any further work
302 const repeatedElement = getFirstElementChild(template);
303 if (!repeatedElement.hasAttribute('au-target-id')) {
304 return undefined;
305 }
306 const repeatedElementTargetId = repeatedElement.getAttribute('au-target-id');
307 return extractMatcherBindingExpression(instructions, repeatedElementTargetId);
308 }
309
310 return undefined;
311 }
312
313 // @override AbstractRepeater
314 viewCount() { return this.viewSlot.children.length; }
315 views() { return this.viewSlot.children; }
316 view(index) { return this.viewSlot.children[index]; }
317 matcher() {
318 const matcherBinding = this.matcherBinding;
319 return matcherBinding
320 ? matcherBinding.sourceExpression.evaluate(this.scope, matcherBinding.lookupFunctions)
321 : null;
322 }
323
324 addView(bindingContext, overrideContext) {
325 let view = this.viewFactory.create();
326 view.bind(bindingContext, overrideContext);
327 this.viewSlot.add(view);
328 }
329
330 insertView(index, bindingContext, overrideContext) {
331 let view = this.viewFactory.create();
332 view.bind(bindingContext, overrideContext);
333 this.viewSlot.insert(index, view);
334 }
335
336 moveView(sourceIndex, targetIndex) {
337 this.viewSlot.move(sourceIndex, targetIndex);
338 }
339
340 removeAllViews(returnToCache, skipAnimation) {
341 return this.viewSlot.removeAll(returnToCache, skipAnimation);
342 }
343
344 removeViews(viewsToRemove, returnToCache, skipAnimation) {
345 return this.viewSlot.removeMany(viewsToRemove, returnToCache, skipAnimation);
346 }
347
348 removeView(index, returnToCache, skipAnimation) {
349 return this.viewSlot.removeAt(index, returnToCache, skipAnimation);
350 }
351
352 updateBindings(view: View) {
353 const $view = view as View & { bindings: any[]; controllers: any[] };
354 let j = $view.bindings.length;
355 while (j--) {
356 updateOneTimeBinding($view.bindings[j]);
357 }
358 j = $view.controllers.length;
359 while (j--) {
360 let k = $view.controllers[j].boundProperties.length;
361 while (k--) {
362 let binding = $view.controllers[j].boundProperties[k].binding;
363 updateOneTimeBinding(binding);
364 }
365 }
366 }
367}
368
369/**
370 * Iterate a record of TargetInstruction and their expressions to find first binding expression that targets property named "matcher"
371 */
372const extractMatcherBindingExpression = (instructions: Record<string, TargetInstruction>, targetedElementId?: string): BindingExpression | undefined => {
373 const instructionIds = Object.keys(instructions);
374 for (let i = 0; i < instructionIds.length; i++) {
375 const instructionId = instructionIds[i];
376 // matcher binding can only works when root element is not a <template/>
377 // checking first el child
378 if (targetedElementId !== undefined && instructionId !== targetedElementId) {
379 continue;
380 }
381 const expressions = instructions[instructionId].expressions as BindingExpression[];
382 if (expressions) {
383 for (let ii = 0; ii < expressions.length; ii++) {
384 if (expressions[ii].targetProperty === 'matcher') {
385 const matcherBindingExpression = expressions[ii];
386 expressions.splice(ii, 1);
387 return matcherBindingExpression;
388 }
389 }
390 }
391 }
392};
393
394/**
395 * Calculate the number of child elements of an element
396 *
397 * Note: API .childElementCount/.children are not available in IE11
398 */
399const getChildrenCount = (el: Element | DocumentFragment) => {
400 const childNodes = el.childNodes;
401 let count = 0;
402 for (let i = 0, ii = childNodes.length; ii > i; ++i) {
403 if (childNodes[i].nodeType === /* element */1) {
404 ++count;
405 }
406 }
407 return count;
408};
409
410/**
411 * Get the first child element of an element / doc fragment
412 *
413 * Note: API .firstElementChild is not available in IE11
414 */
415const getFirstElementChild = (el: Element | DocumentFragment) => {
416 let firstChild = el.firstChild as Element;
417 while (firstChild !== null) {
418 if (firstChild.nodeType === /* element */1) {
419 return firstChild;
420 }
421 firstChild = firstChild.nextSibling as Element;
422 }
423 return null;
424};