UNPKG

10 kBPlain TextView Raw
1import {createFullOverrideContext, updateOverrideContexts, updateOverrideContext, indexOf} from './repeat-utilities';
2import {mergeSplice} from 'aurelia-binding';
3import { Repeat } from './repeat';
4
5/**
6 * A strategy for repeating a template over an array.
7 */
8export class ArrayRepeatStrategy {
9 /**
10 * Gets an observer for the specified collection.
11 * @param observerLocator The observer locator instance.
12 * @param items The items to be observed.
13 */
14 getCollectionObserver(observerLocator, items) {
15 return observerLocator.getArrayObserver(items);
16 }
17
18 /**
19 * Handle the repeat's collection instance changing.
20 * @param repeat The repeater instance.
21 * @param items The new array instance.
22 */
23 instanceChanged(repeat, items) {
24 const $repeat = repeat as Repeat;
25 const itemsLength = items.length;
26
27 // if the new instance does not contain any items,
28 // just remove all views and don't do any further processing
29 if (!items || itemsLength === 0) {
30 $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle);
31 return;
32 }
33
34 const children = $repeat.views();
35 const viewsLength = children.length;
36
37 // likewise, if we previously didn't have any views,
38 // simply make them and return
39 if (viewsLength === 0) {
40 this._standardProcessInstanceChanged($repeat, items);
41 return;
42 }
43
44 if ($repeat.viewsRequireLifecycle) {
45 const childrenSnapshot = children.slice(0);
46 const itemNameInBindingContext = $repeat.local;
47 const matcher = $repeat.matcher();
48
49 // the cache of the current state (it will be transformed along with the views to keep track of indicies)
50 let itemsPreviouslyInViews = [];
51 const viewsToRemove = [];
52
53 for (let index = 0; index < viewsLength; index++) {
54 const view = childrenSnapshot[index];
55 const oldItem = view.bindingContext[itemNameInBindingContext];
56
57 if (indexOf(items, oldItem, matcher) === -1) {
58 // remove the item if no longer in the new instance of items
59 viewsToRemove.push(view);
60 } else {
61 // or add the item to the cache list
62 itemsPreviouslyInViews.push(oldItem);
63 }
64 }
65
66 let updateViews;
67 let removePromise;
68
69 if (itemsPreviouslyInViews.length > 0) {
70 removePromise = $repeat.removeViews(viewsToRemove, true, !$repeat.viewsRequireLifecycle);
71 updateViews = () => {
72 // update views (create new and move existing)
73 for (let index = 0; index < itemsLength; index++) {
74 const item = items[index];
75 const indexOfView = indexOf(itemsPreviouslyInViews, item, matcher, index);
76 let view;
77
78 if (indexOfView === -1) { // create views for new items
79 const overrideContext = createFullOverrideContext($repeat, items[index], index, itemsLength);
80 $repeat.insertView(index, overrideContext.bindingContext, overrideContext);
81 // reflect the change in our cache list so indicies are valid
82 itemsPreviouslyInViews.splice(index, 0, undefined);
83 } else if (indexOfView === index) { // leave unchanged items
84 view = children[indexOfView];
85 itemsPreviouslyInViews[indexOfView] = undefined;
86 } else { // move the element to the right place
87 view = children[indexOfView];
88 $repeat.moveView(indexOfView, index);
89 itemsPreviouslyInViews.splice(indexOfView, 1);
90 itemsPreviouslyInViews.splice(index, 0, undefined);
91 }
92
93 if (view) {
94 updateOverrideContext(view.overrideContext, index, itemsLength);
95 }
96 }
97
98 // remove extraneous elements in case of duplicates,
99 // also update binding contexts if objects changed using the matcher function
100 this._inPlaceProcessItems($repeat, items);
101 };
102 } else {
103 // if all of the items are different, remove all and add all from scratch
104 removePromise = $repeat.removeAllViews(true, !$repeat.viewsRequireLifecycle);
105 updateViews = () => this._standardProcessInstanceChanged($repeat, items);
106 }
107
108 if (removePromise instanceof Promise) {
109 removePromise.then(updateViews);
110 } else {
111 updateViews();
112 }
113 } else {
114 // no lifecycle needed, use the fast in-place processing
115 this._inPlaceProcessItems($repeat, items);
116 }
117 }
118
119 /**
120 * @internal
121 */
122 _standardProcessInstanceChanged(repeat, items) {
123 for (let i = 0, ii = items.length; i < ii; i++) {
124 let overrideContext = createFullOverrideContext(repeat, items[i], i, ii);
125 repeat.addView(overrideContext.bindingContext, overrideContext);
126 }
127 }
128
129 /**
130 * @internal
131 */
132 _inPlaceProcessItems(repeat, items) {
133 let itemsLength = items.length;
134 let viewsLength = repeat.viewCount();
135 // remove unneeded views.
136 while (viewsLength > itemsLength) {
137 viewsLength--;
138 repeat.removeView(viewsLength, true, !repeat.viewsRequireLifecycle);
139 }
140 // avoid repeated evaluating the property-getter for the "local" property.
141 let local = repeat.local;
142 // re-evaluate bindings on existing views.
143 for (let i = 0; i < viewsLength; i++) {
144 let view = repeat.view(i);
145 let last = i === itemsLength - 1;
146 let middle = i !== 0 && !last;
147 let bindingContext = view.bindingContext;
148 let overrideContext = view.overrideContext;
149 // any changes to the binding context?
150 if (bindingContext[local] === items[i]
151 && overrideContext.$middle === middle
152 && overrideContext.$last === last) {
153 // no changes. continue...
154 continue;
155 }
156 // update the binding context and refresh the bindings.
157 bindingContext[local] = items[i];
158 overrideContext.$middle = middle;
159 overrideContext.$last = last;
160 repeat.updateBindings(view);
161 }
162 // add new views
163 for (let i = viewsLength; i < itemsLength; i++) {
164 let overrideContext = createFullOverrideContext(repeat, items[i], i, itemsLength);
165 repeat.addView(overrideContext.bindingContext, overrideContext);
166 }
167 }
168
169 /**
170 * Handle the repeat's collection instance mutating.
171 * @param repeat The repeat instance.
172 * @param array The modified array.
173 * @param splices Records of array changes.
174 */
175 instanceMutated(repeat, array, splices) {
176 if (repeat.__queuedSplices) {
177 for (let i = 0, ii = splices.length; i < ii; ++i) {
178 let {index, removed, addedCount} = splices[i];
179 mergeSplice(repeat.__queuedSplices, index, removed, addedCount);
180 }
181 // Array.prototype.slice is used here to clone the array
182 repeat.__array = array.slice(0);
183 return;
184 }
185
186 // Array.prototype.slice is used here to clone the array
187 let maybePromise = this._runSplices(repeat, array.slice(0), splices);
188 if (maybePromise instanceof Promise) {
189 let queuedSplices = repeat.__queuedSplices = [];
190
191 let runQueuedSplices = () => {
192 if (!queuedSplices.length) {
193 repeat.__queuedSplices = undefined;
194 repeat.__array = undefined;
195 return;
196 }
197
198 let nextPromise = this._runSplices(repeat, repeat.__array, queuedSplices) || Promise.resolve();
199 queuedSplices = repeat.__queuedSplices = [];
200 nextPromise.then(runQueuedSplices);
201 };
202
203 maybePromise.then(runQueuedSplices);
204 }
205 }
206
207 /**
208 * Run a normalised set of splices against the viewSlot children.
209 * @param repeat The repeat instance.
210 * @param array The modified array.
211 * @param splices Records of array changes.
212 * @return {Promise|undefined} A promise if animations have to be run.
213 * @pre The splices must be normalised so as:
214 * * Any item added may not be later removed.
215 * * Removals are ordered by asending index
216 * @internal
217 */
218 _runSplices(repeat, array, splices) {
219 let removeDelta = 0;
220 let rmPromises = [];
221
222 for (let i = 0, ii = splices.length; i < ii; ++i) {
223 let splice = splices[i];
224 let removed = splice.removed;
225
226 for (let j = 0, jj = removed.length; j < jj; ++j) {
227 // the rmPromises.length correction works due to the ordered removal precondition
228 let viewOrPromise = repeat.removeView(splice.index + removeDelta + rmPromises.length, true);
229 if (viewOrPromise instanceof Promise) {
230 rmPromises.push(viewOrPromise);
231 }
232 }
233 removeDelta -= splice.addedCount;
234 }
235
236 if (rmPromises.length > 0) {
237 return Promise.all(rmPromises).then(() => {
238 let spliceIndexLow = this._handleAddedSplices(repeat, array, splices);
239 updateOverrideContexts(repeat.views(), spliceIndexLow);
240 });
241 }
242
243 let spliceIndexLow = this._handleAddedSplices(repeat, array, splices);
244 updateOverrideContexts(repeat.views(), spliceIndexLow);
245
246 return undefined;
247 }
248
249 /**
250 * @internal
251 */
252 _handleAddedSplices(repeat, array, splices) {
253 let spliceIndex: number;
254 let spliceIndexLow: number;
255 let arrayLength = array.length;
256 for (let i = 0, ii = splices.length; i < ii; ++i) {
257 let splice = splices[i];
258 let addIndex = spliceIndex = splice.index;
259 let end = splice.index + splice.addedCount;
260
261 if (typeof spliceIndexLow === 'undefined' || spliceIndexLow === null || spliceIndexLow > splice.index) {
262 spliceIndexLow = spliceIndex;
263 }
264
265 for (; addIndex < end; ++addIndex) {
266 let overrideContext = createFullOverrideContext(repeat, array[addIndex], addIndex, arrayLength);
267 repeat.insertView(addIndex, overrideContext.bindingContext, overrideContext);
268 }
269 }
270
271 return spliceIndexLow;
272 }
273}