@miso.ai/client-sdk-ui 1.10.0-beta.1 → 1.10.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/dist/cjs/actor/data.js +1 -1
  2. package/dist/cjs/actor/tracker.js +46 -70
  3. package/dist/cjs/actor/trackers.js +5 -1
  4. package/dist/cjs/actor/utils.js +6 -2
  5. package/dist/cjs/actor/view.js +14 -3
  6. package/dist/cjs/actor/views.js +9 -1
  7. package/dist/cjs/affiliation/index.js +126 -0
  8. package/dist/cjs/combo/ask/options.js +1 -1
  9. package/dist/cjs/combo/base.js +1 -1
  10. package/dist/cjs/constants.js +6 -3
  11. package/dist/cjs/defaults/ask/controls.js +1 -1
  12. package/dist/cjs/defaults/ask/phrases.js +1 -1
  13. package/dist/cjs/defaults/ask/templates.js +14 -4
  14. package/dist/cjs/element/role/index.js +7 -0
  15. package/dist/cjs/element/role/miso-affiliation.js +20 -0
  16. package/dist/cjs/layout/affiliation/affiliation.js +330 -0
  17. package/dist/cjs/layout/banner/banner.js +1 -1
  18. package/dist/cjs/layout/container/container.js +1 -1
  19. package/dist/cjs/layout/error/error.js +1 -1
  20. package/dist/cjs/layout/index.js +7 -0
  21. package/dist/cjs/layout/input/option-list.js +35 -50
  22. package/dist/cjs/layout/input/search-box.js +17 -5
  23. package/dist/cjs/layout/list/cards.js +1 -1
  24. package/dist/cjs/layout/list/carousel.js +5 -3
  25. package/dist/cjs/layout/list/collection.js +38 -48
  26. package/dist/cjs/layout/list/list.js +1 -1
  27. package/dist/cjs/layout/radio/feedback.js +1 -1
  28. package/dist/cjs/layout/radio/radio.js +1 -1
  29. package/dist/cjs/layout/raf.js +1 -1
  30. package/dist/cjs/layout/template.js +1 -1
  31. package/dist/cjs/layout/templates.js +135 -44
  32. package/dist/cjs/layout/text/text.js +1 -1
  33. package/dist/cjs/layout/text/typewriter/index.js +3 -3
  34. package/dist/cjs/layout/trackable.js +124 -0
  35. package/dist/cjs/plugin.js +1 -1
  36. package/dist/cjs/util/bindings.js +58 -110
  37. package/dist/cjs/util/states.js +31 -24
  38. package/dist/cjs/util/trackers.js +75 -0
  39. package/dist/cjs/util/viewables.js +55 -0
  40. package/dist/cjs/version.js +1 -1
  41. package/dist/cjs/workflow/ask.js +14 -5
  42. package/dist/cjs/workflow/asks.js +1 -1
  43. package/dist/cjs/workflow/base.js +5 -5
  44. package/dist/cjs/workflow/explore.js +1 -1
  45. package/dist/cjs/workflow/options.js +6 -1
  46. package/dist/cjs/workflow/recommendation.js +2 -0
  47. package/package.json +2 -2
  48. package/src/actor/tracker.js +39 -62
  49. package/src/actor/trackers.js +3 -1
  50. package/src/actor/utils.js +6 -2
  51. package/src/actor/view.js +12 -1
  52. package/src/actor/views.js +11 -0
  53. package/src/affiliation/index.js +108 -0
  54. package/src/constants.js +10 -1
  55. package/src/defaults/ask/phrases.js +1 -1
  56. package/src/defaults/ask/templates.js +13 -2
  57. package/src/element/role/index.js +1 -0
  58. package/src/element/role/miso-affiliation.js +16 -0
  59. package/src/layout/affiliation/affiliation.js +295 -0
  60. package/src/layout/index.js +1 -0
  61. package/src/layout/input/option-list.js +35 -43
  62. package/src/layout/input/search-box.js +7 -2
  63. package/src/layout/list/carousel.js +4 -2
  64. package/src/layout/list/collection.js +37 -43
  65. package/src/layout/templates.js +101 -26
  66. package/src/layout/text/typewriter/index.js +2 -2
  67. package/src/layout/trackable.js +106 -0
  68. package/src/util/bindings.js +47 -103
  69. package/src/util/states.js +26 -20
  70. package/src/util/trackers.js +69 -0
  71. package/src/util/viewables.js +47 -0
  72. package/src/version.js +1 -1
  73. package/src/workflow/ask.js +7 -3
  74. package/src/workflow/base.js +1 -1
  75. package/src/workflow/options.js +5 -1
  76. package/src/workflow/recommendation.js +1 -0
  77. package/dist/cjs/util/tracker.js +0 -348
  78. package/src/util/tracker.js +0 -318
@@ -0,0 +1,295 @@
1
+ import { CarouselItemViewabilityObserver, requestAnimationFrame as raf } from '@miso.ai/commons';
2
+ import { STATUS, LAYOUT_CATEGORY, EVENT_TYPE } from '../../constants.js';
3
+ import CollectionLayout from '../list/collection.js';
4
+ import { affiliation } from '../templates.js';
5
+
6
+ const TYPE = 'affiliation';
7
+ const DEFAULT_CLASSNAME = 'miso-affiliation';
8
+
9
+ function root(layout, state) {
10
+ const { className, role, templates } = layout;
11
+ const { status } = state;
12
+ const roleAttr = role ? ` data-role="${role}"` : '';
13
+ const data = state.value || {};
14
+ const channelAttr = data.channel ? ` data-channel="${data.channel}"` : '';
15
+ return `<div class="${className} ${status}"${roleAttr}${channelAttr}>${status === STATUS.READY ? templates[status](layout, state) : ''}</div>`;
16
+ }
17
+
18
+ function ready(layout, state) {
19
+ const { templates } = layout;
20
+ const items = layout._getItems(state);
21
+
22
+ if (items && items.length > 0) {
23
+ return templates.control(layout, state, 'previous') + templates.body(layout, state) + templates.control(layout, state, 'next');
24
+ } else {
25
+ return templates.empty(layout, state);
26
+ }
27
+ }
28
+
29
+ function control(layout, state, direction) {
30
+ const { className } = layout;
31
+ return `<div class="${className}__control-${direction}" data-role="${direction}"></div>`;
32
+ }
33
+
34
+ function body(layout, state) {
35
+ const { className, templates } = layout;
36
+ const items = layout._getItems(state);
37
+ return `<div class="${className}__body">${templates.list(layout, state, items)}</div>`;
38
+ }
39
+
40
+ function header(layout, state) {
41
+ const { className, templates } = layout;
42
+ let { headerText } = templates;
43
+ if (typeof headerText === 'function') {
44
+ headerText = headerText(layout, state);
45
+ }
46
+ if (!headerText) {
47
+ return '';
48
+ }
49
+ return `<div class="${className}__header"><div class="${className}__header-tab">${headerText}</div></div>`;
50
+ }
51
+
52
+ function item(layout, state, value, index) {
53
+ const { className, templates, options } = layout;
54
+ const { itemType } = options;
55
+ const header = templates.itemHeader(layout, state, value, { index });
56
+ const body = templates[itemType](layout, state, value, { index });
57
+ return `<li class="${className}__item">${header}${body}</li>`;
58
+ }
59
+
60
+ function itemHeader(layout, state, value, meta) {
61
+ const { className, templates } = layout;
62
+ let { headerText } = templates;
63
+ if (typeof headerText === 'function') {
64
+ headerText = headerText(layout, state, value, meta);
65
+ }
66
+ if (!headerText) {
67
+ return '';
68
+ }
69
+ return `<div class="${className}__item-header"><div class="${className}__item-header-tab">${headerText}</div></div>`;
70
+ }
71
+
72
+ const DEFAULT_TEMPLATES = Object.freeze({
73
+ root,
74
+ [STATUS.READY]: ready,
75
+ control,
76
+ body,
77
+ header,
78
+ item,
79
+ itemHeader,
80
+ affiliation,
81
+ ctaText: 'View',
82
+ headerText: 'Featured',
83
+ });
84
+
85
+ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
86
+ ...CollectionLayout.defaultTemplates,
87
+ ...DEFAULT_TEMPLATES,
88
+ });
89
+
90
+ function normalizeAutoplayOptions(options) {
91
+ if (options === true) {
92
+ options = { interval: 5000 };
93
+ } else if (typeof options === 'number') {
94
+ if (options <= 0) {
95
+ options = false;
96
+ } else {
97
+ options = { interval: Math.max(options, 500) };
98
+ }
99
+ }
100
+ return options;
101
+ }
102
+
103
+ function autoplayOptionsEquals(a, b) {
104
+ if (a === b) {
105
+ return true;
106
+ }
107
+ if (!a || !b) {
108
+ return false;
109
+ }
110
+ return a.interval === b.interval;
111
+ }
112
+
113
+ export default class AffiliationLayout extends CollectionLayout {
114
+
115
+ static get category() {
116
+ return LAYOUT_CATEGORY.AFFILIATION;
117
+ }
118
+
119
+ static get type() {
120
+ return TYPE;
121
+ }
122
+
123
+ static get defaultTemplates() {
124
+ return INHERITED_DEFAULT_TEMPLATES;
125
+ }
126
+
127
+ static get defaultClassName() {
128
+ return DEFAULT_CLASSNAME;
129
+ }
130
+
131
+ constructor({ className = DEFAULT_CLASSNAME, templates, autoplay, ...options } = {}) {
132
+ super({ className, itemType: 'affiliation', templates: { ...DEFAULT_TEMPLATES, ...templates }, ...options });
133
+ this._itemCount = undefined;
134
+ this._itemIndex = 0;
135
+ this._displayedCount = undefined;
136
+ this._displayedIndex = undefined;
137
+ this._viewable = undefined;
138
+ this._autoplay = {};
139
+
140
+ this.autoplay(autoplay);
141
+ }
142
+
143
+ initialize(view) {
144
+ super.initialize(view);
145
+ const { viewable: options } = this._view.trackerOptions || {};
146
+ this._viewable = new CarouselItemViewabilityObserver(this._onViewable.bind(this), options);
147
+ }
148
+
149
+ autoplay(options = true) {
150
+ options = normalizeAutoplayOptions(options);
151
+ if (autoplayOptionsEquals(options, this._autoplay.options)) {
152
+ return;
153
+ }
154
+ if (this._autoplay.options) {
155
+ clearInterval(this._autoplay.intervalId);
156
+ }
157
+ this._autoplay = { options };
158
+ // TODO: use slower transition speed for autoplay
159
+ // TODO: pause on hover
160
+ // TODO: cooldown after manual interaction
161
+ if (options) {
162
+ this._autoplay.intervalId = setInterval(() => this.next(), options.interval);
163
+ }
164
+ }
165
+
166
+ get itemIndex() {
167
+ return this._itemIndex;
168
+ }
169
+
170
+ set itemIndex(index) {
171
+ if (this._itemCount === undefined) {
172
+ return;
173
+ }
174
+ if (index < 0) {
175
+ index = this._itemCount - 1;
176
+ } else if (index >= this._itemCount) {
177
+ index = 0;
178
+ }
179
+ this._itemIndex = index;
180
+ raf(() => this._syncItemIndex());
181
+ }
182
+
183
+ next() {
184
+ if (this._itemCount === undefined) {
185
+ return;
186
+ }
187
+ this.itemIndex++;
188
+ }
189
+
190
+ previous() {
191
+ if (this._itemCount === undefined) {
192
+ return;
193
+ }
194
+ this.itemIndex--;
195
+ }
196
+
197
+ _getItems(state) {
198
+ const { products } = state.value || {};
199
+ return products;
200
+ }
201
+
202
+ _render(element, states, controls) {
203
+ super._render(element, states, controls);
204
+
205
+ // update item count, but raf to display as attribute
206
+ const items = this._getItems(states.state);
207
+ this._itemCount = items ? items.length : undefined;
208
+
209
+ raf(() => {
210
+ this._syncItemCount();
211
+ this._syncItemIndex();
212
+ });
213
+ }
214
+
215
+ _syncElement(element) {
216
+ const oldElement = this._element;
217
+ super._syncElement(element);
218
+ this._syncViewableElement(oldElement, element);
219
+ }
220
+
221
+ _syncViewableElement(oldElement, newElement) {
222
+ if (oldElement === newElement) {
223
+ return;
224
+ }
225
+ oldElement && this._viewable.unobserve(oldElement);
226
+ newElement && this._viewable.observe(newElement);
227
+ }
228
+
229
+ _syncItemCount() {
230
+ const itemCount = this._itemCount;
231
+ if (itemCount === this._displayedCount) {
232
+ return;
233
+ }
234
+ let { element } = this._view;
235
+ element = element.firstElementChild;
236
+ if (!element) {
237
+ return;
238
+ }
239
+ this._displayedCount = itemCount;
240
+ if (itemCount === undefined) {
241
+ element.removeAttribute('data-item-count');
242
+ } else {
243
+ element.setAttribute('data-item-count', itemCount);
244
+ }
245
+ }
246
+
247
+ _syncItemIndex() {
248
+ const index = this._itemIndex;
249
+ if (index === this._displayedIndex) {
250
+ return;
251
+ }
252
+ const { element } = this._view;
253
+ const listElement = element && this._getListElement(element);
254
+ if (!listElement) {
255
+ return;
256
+ }
257
+ this._displayedIndex = index;
258
+ listElement.style.left = `${-index * 100}%`;
259
+ this._viewable.display(index);
260
+ this._view._events.emit('display', { index });
261
+ }
262
+
263
+ _onViewable(index) {
264
+ const item = this._getRenderedItemData(index);
265
+ if (!item) {
266
+ return;
267
+ }
268
+ this._view._track(EVENT_TYPE.VIEWABLE, [item]);
269
+ }
270
+
271
+ _getRenderedItemData(index) {
272
+ const { value } = this._rendered.get(this._element) || {};
273
+ if (!value) {
274
+ return undefined;
275
+ }
276
+ const { products = [] } = value;
277
+ return products[index];
278
+ }
279
+
280
+ _trackViewables() {} // omit default behavior for we want to track by other means
281
+
282
+ _onClick(event) {
283
+ super._onClick(event);
284
+ const element = event.target;
285
+ if (element.closest(`[data-role="previous"]`)) {
286
+ this.previous();
287
+ return;
288
+ }
289
+ if (element.closest(`[data-role="next"]`)) {
290
+ this.next();
291
+ return;
292
+ }
293
+ }
294
+
295
+ }
@@ -11,3 +11,4 @@ export { default as OptionListLayout } from './input/option-list.js';
11
11
  export { default as FeedbackLayout } from './radio/feedback.js';
12
12
  export { default as TextLayout } from './text/text.js';
13
13
  export { default as TypewriterLayout } from './text/typewriter/index.js';
14
+ export { default as AffiliationLayout } from './affiliation/affiliation.js';
@@ -1,9 +1,8 @@
1
- import { defineValues, escapeHtml } from '@miso.ai/commons';
1
+ import { escapeHtml } from '@miso.ai/commons';
2
2
  import { LAYOUT_CATEGORY } from '../../constants.js';
3
3
  import { fields } from '../../actor/index.js';
4
4
  import TemplateBasedLayout from '../template.js';
5
-
6
- const OPTION_VALUE = Symbol.for('miso.option-value');
5
+ import { makeTrackable } from '../trackable.js';
7
6
 
8
7
  const TYPE = 'option-list';
9
8
  const DEFAULT_CLASSNAME = 'miso-option-list';
@@ -64,11 +63,7 @@ export default class OptionListLayout extends TemplateBasedLayout {
64
63
  templates: { ...DEFAULT_TEMPLATES, ...templates },
65
64
  ...options,
66
65
  });
67
- defineValues(this, {
68
- bindings: Object.freeze({
69
- list: this._listBindings.bind(this),
70
- }),
71
- });
66
+ this._initTrackable();
72
67
  }
73
68
 
74
69
  initialize(view) {
@@ -89,59 +84,48 @@ export default class OptionListLayout extends TemplateBasedLayout {
89
84
 
90
85
  _render(element, data, controls) {
91
86
  super._render(element, data, controls);
92
- this._syncOptionValues(element, data);
87
+ const { state } = data;
88
+ this._syncBindings(element, state);
89
+ this._syncOptionValues(element, state);
93
90
  }
94
91
 
95
- _syncOptionValues(element, { state }) {
96
- if (!element) {
97
- return;
98
- }
99
- const optionsElement = this._getOptionsElement(element);
92
+ _unrender() {
93
+ this._clearBindings();
94
+ }
95
+
96
+ _syncOptionValues(element, state) {
97
+ const optionsElement = element && this._getOptionsElement(element);
100
98
  if (!optionsElement) {
101
99
  return;
102
100
  }
103
- const optionValues = state.value || [];
101
+ const optionValues = this._getItems(state) || [];
104
102
  optionsElement.classList[optionValues.length === 0 ? 'add' : 'remove']('empty');
105
- let i = 0;
106
- for (const optionElement of this._listOptionElements(optionsElement)) {
107
- optionElement[OPTION_VALUE] = optionValues[i++];
108
- }
109
103
  }
110
104
 
111
- _listBindings(rootElement) {
112
- if (!rootElement) {
113
- return [];
114
- }
115
- const optionsElement = this._getOptionsElement(rootElement);
116
- const bindings = [];
117
- for (const element of this._listOptionElements(optionsElement)) {
118
- const value = element[OPTION_VALUE];
119
- if (!value) {
120
- continue;
121
- }
122
- const key = value.text || value;
123
- bindings.push({ key, value, element });
124
- }
125
- return bindings;
105
+ _getItems(state) {
106
+ return state.value;
126
107
  }
127
108
 
128
109
  _getOptionsElement(element) {
129
110
  return element.querySelector(`[data-role="options"]`);
130
111
  }
131
112
 
132
- _listOptionElements(optionsElement) {
133
- return optionsElement ? Array.from(optionsElement.querySelectorAll(`[data-role="option"]`)) : [];
113
+ _getItemElements(element) {
114
+ return element ? Array.from(element.querySelectorAll(`[data-role="option"]`)) : [];
134
115
  }
135
116
 
136
- _handleClick(e) {
137
- for (let element = e.target; element && element !== this._view.element; element = element.parentElement) {
138
- const option = element[OPTION_VALUE];
139
- if (!option) {
140
- continue;
141
- }
142
- this._submit(option.text);
117
+ _handleClick(event) {
118
+ const element = event.target.closest(`[data-role="option"]`);
119
+ if (!element) {
120
+ return;
121
+ }
122
+ const binding = this._bindings.get(element);
123
+ const option = binding && binding.value;
124
+ if (!option) {
143
125
  return;
144
126
  }
127
+ this._trackClick(event, binding);
128
+ this._submit(option.text);
145
129
  }
146
130
 
147
131
  async _submit(value) {
@@ -152,4 +136,12 @@ export default class OptionListLayout extends TemplateBasedLayout {
152
136
  this._view.hub.trigger(fields.query(), { q: value });
153
137
  }
154
138
 
139
+ destroy() {
140
+ this._destroyTrackable();
141
+ this._view = undefined;
142
+ super.destroy();
143
+ }
144
+
155
145
  }
146
+
147
+ makeTrackable(OptionListLayout.prototype);
@@ -285,7 +285,8 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
285
285
  // TODO: select completion items with arrow keys
286
286
  }
287
287
 
288
- _handleClick({ target }) {
288
+ _handleClick(event) {
289
+ const { target } = event;
289
290
  const { inputElement } = this._context();
290
291
  for (let element = target; element && element !== this._element; element = element.parentElement) {
291
292
  if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
@@ -303,12 +304,16 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
303
304
  if (inputElement) {
304
305
  inputElement.value = value;
305
306
  }
306
- this._submit(value);
307
+ this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
308
+ if (!event.defaultPrevented) {
309
+ this._submit(value);
310
+ }
307
311
  }
308
312
  this.close();
309
313
  return;
310
314
  }
311
315
  if (element.matches('[data-role="product-item"]')) {
316
+ this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
312
317
  return;
313
318
  }
314
319
  }
@@ -17,7 +17,8 @@ const INHERITED_DEFAULT_TEMPLATES = Object.freeze({
17
17
 
18
18
  function ready(layout, state) {
19
19
  const { templates } = layout;
20
- const nonEmpty = state.value && state.value.length > 0;
20
+ const items = layout._getItems(state);
21
+ const nonEmpty = items && items.length > 0;
21
22
  const html = Object.getPrototypeOf(layout.constructor).defaultTemplates.ready(layout, state);
22
23
  return nonEmpty ? templates.carousel(layout, state, html) : html;
23
24
  }
@@ -60,7 +61,8 @@ export default class CarouselLayout extends CollectionLayout {
60
61
  _render(element, states, controls) {
61
62
  super._render(element, states, controls);
62
63
  const { state } = states;
63
- this._itemCount = state.value ? state.value.length : undefined;
64
+ const items = this._getItems(state);
65
+ this._itemCount = items ? items.length : undefined;
64
66
  this.syncSize();
65
67
  }
66
68
 
@@ -1,9 +1,7 @@
1
- import { defineValues } from '@miso.ai/commons';
2
1
  import { STATUS, LAYOUT_CATEGORY } from '../../constants.js';
3
2
  import TemplateBasedLayout from '../template.js';
4
- import { product, article, question, productInfoBlock, articleInfoBlock, imageBlock, indexBlock, helpers } from '../templates.js';
5
-
6
- const VALUE = Symbol.for('miso.value');
3
+ import { makeTrackable } from '../trackable.js';
4
+ import { product, article, question, productInfoBlock, articleInfoBlock, titleBlock, brandBlock, descriptionBlock, dateBlock, priceBlock, discountRateText, ctaBlock, cta, imageBlock, indexBlock, helpers } from '../templates.js';
7
5
 
8
6
  function root(layout, state) {
9
7
  const { className, role, templates } = layout;
@@ -14,7 +12,7 @@ function root(layout, state) {
14
12
 
15
13
  function ready(layout, state) {
16
14
  const { templates } = layout;
17
- const values = state.value;
15
+ const values = layout._getItems(state);
18
16
 
19
17
  // TODO: handle categories, attributes, etc. by introducing sublayout
20
18
  if ((values && values.length > 0) || state.ongoing) {
@@ -28,7 +26,7 @@ function list(layout, state, values) {
28
26
  const { className, templates, options } = layout;
29
27
  const listTag = templates.ordered ? 'ol' : 'ul';
30
28
  // TODO: support separator?
31
- return `<${listTag} class="${className}__list" data-item-type="${options.itemType}">${templates.items(layout, state, values)}</${listTag}>`;
29
+ return `<${listTag} class="${className}__list" data-role="list" data-item-type="${options.itemType}">${templates.items(layout, state, values)}</${listTag}>`;
32
30
  }
33
31
 
34
32
  function items(layout, state, values, { offset = 0 } = {}) {
@@ -59,6 +57,14 @@ const DEFAULT_TEMPLATES = Object.freeze({
59
57
  item,
60
58
  productInfoBlock,
61
59
  articleInfoBlock,
60
+ brandBlock,
61
+ titleBlock,
62
+ descriptionBlock,
63
+ dateBlock,
64
+ priceBlock,
65
+ discountRateText,
66
+ ctaBlock,
67
+ cta,
62
68
  imageBlock,
63
69
  indexBlock,
64
70
  helpers,
@@ -80,18 +86,13 @@ export default class CollectionLayout extends TemplateBasedLayout {
80
86
  itemType,
81
87
  ...options,
82
88
  });
83
- defineValues(this, {
84
- bindings: Object.freeze({
85
- list: this._listBindings.bind(this),
86
- }),
87
- });
88
89
  this._view = undefined;
90
+ this._initTrackable();
89
91
  }
90
92
 
91
93
  initialize(view) {
92
94
  this._view = view;
93
- const { proxyElement } = this._view = view;
94
- this._unsubscribes.push(proxyElement.on('click', this._onClick.bind(this)));
95
+ this._unsubscribes.push(view.proxyElement.on('click', this._onClick.bind(this)));
95
96
  }
96
97
 
97
98
  _preprocess({ state, rendered }) {
@@ -117,7 +118,7 @@ export default class CollectionLayout extends TemplateBasedLayout {
117
118
  _html(state, rendered, incremental) {
118
119
  if (incremental) {
119
120
  const offset = rendered.value.length;
120
- const values = state.value.slice(offset);
121
+ const values = (this._getItems(state) || []).slice(offset);
121
122
  return values.length > 0 ? this.templates.items(this, state, values, { offset }) : '';
122
123
  } else {
123
124
  return this.templates.root(this, state);
@@ -136,42 +137,23 @@ export default class CollectionLayout extends TemplateBasedLayout {
136
137
  } else {
137
138
  element.innerHTML = html;
138
139
  }
139
- this._syncValues(element, state);
140
+ this._syncBindings(element, state);
140
141
  }
141
142
 
142
- _syncValues(element, state) {
143
- if (!element) {
144
- return;
145
- }
146
- const values = state.value || [];
147
- let i = 0;
148
- for (const itemElement of this._listItemElements(element)) {
149
- itemElement[VALUE] = values[i++];
150
- }
143
+ _unrender() {
144
+ this._clearBindings();
151
145
  }
152
146
 
153
- _getListElement(element) {
154
- return element.querySelector(`.${this.className}__list`);
147
+ _getItems(state) {
148
+ return state.value;
155
149
  }
156
150
 
157
- _listItemElements(element) {
158
- return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
151
+ _getListElement(element) {
152
+ return element.querySelector('[data-role="list"]');
159
153
  }
160
154
 
161
- _listBindings(rootElement) {
162
- if (!rootElement) {
163
- return [];
164
- }
165
- const bindings = [];
166
- for (const element of this._listItemElements(rootElement)) {
167
- const value = element[VALUE];
168
- if (!value) {
169
- continue;
170
- }
171
- const key = this.options.itemType === 'product' ? value.product_id : value;
172
- bindings.push({ element, key, value });
173
- }
174
- return bindings;
155
+ _getItemElements(element) {
156
+ return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
175
157
  }
176
158
 
177
159
  _onClick(event) {
@@ -179,14 +161,26 @@ export default class CollectionLayout extends TemplateBasedLayout {
179
161
  if (!element) {
180
162
  return;
181
163
  }
182
- const value = element[VALUE];
164
+ const binding = this._bindings.get(element);
165
+ if (!binding) {
166
+ return;
167
+ }
168
+ this._trackClick(event, binding);
169
+ this._emitClickEvent(event, binding);
170
+ }
171
+
172
+ _emitClickEvent(event, binding) {
183
173
  const { session } = this._view._state;
174
+ const { value, element } = binding;
184
175
  this._view._events.emit('click', { session, value, element, domEvent: event });
185
176
  }
186
177
 
187
178
  destroy() {
179
+ this._destroyTrackable();
188
180
  this._view = undefined;
189
181
  super.destroy();
190
182
  }
191
183
 
192
184
  }
185
+
186
+ makeTrackable(CollectionLayout.prototype);