@miso.ai/client-sdk-ui 1.10.0-beta.0 → 1.10.0-beta.10

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 (90) hide show
  1. package/dist/cjs/actor/autocomplete.js +157 -0
  2. package/dist/cjs/actor/data.js +4 -12
  3. package/dist/cjs/actor/fields.js +8 -0
  4. package/dist/cjs/actor/index.js +7 -0
  5. package/dist/cjs/actor/tracker.js +46 -70
  6. package/dist/cjs/actor/trackers.js +5 -1
  7. package/dist/cjs/actor/utils.js +16 -2
  8. package/dist/cjs/actor/view.js +14 -3
  9. package/dist/cjs/actor/views.js +9 -1
  10. package/dist/cjs/affiliation/index.js +126 -0
  11. package/dist/cjs/combo/ask/options.js +1 -1
  12. package/dist/cjs/combo/base.js +1 -1
  13. package/dist/cjs/constants.js +6 -3
  14. package/dist/cjs/defaults/ask/controls.js +1 -1
  15. package/dist/cjs/defaults/ask/phrases.js +1 -1
  16. package/dist/cjs/defaults/ask/templates.js +35 -4
  17. package/dist/cjs/element/role/index.js +7 -0
  18. package/dist/cjs/element/role/miso-affiliation.js +20 -0
  19. package/dist/cjs/layout/affiliation/affiliation.js +330 -0
  20. package/dist/cjs/layout/banner/banner.js +1 -1
  21. package/dist/cjs/layout/container/container.js +1 -1
  22. package/dist/cjs/layout/error/error.js +1 -1
  23. package/dist/cjs/layout/index.js +7 -0
  24. package/dist/cjs/layout/input/option-list.js +35 -50
  25. package/dist/cjs/layout/input/search-box.js +227 -129
  26. package/dist/cjs/layout/list/cards.js +1 -1
  27. package/dist/cjs/layout/list/carousel.js +5 -3
  28. package/dist/cjs/layout/list/collection.js +44 -49
  29. package/dist/cjs/layout/list/list.js +1 -1
  30. package/dist/cjs/layout/radio/feedback.js +1 -1
  31. package/dist/cjs/layout/radio/radio.js +1 -1
  32. package/dist/cjs/layout/raf.js +1 -1
  33. package/dist/cjs/layout/template.js +1 -1
  34. package/dist/cjs/layout/templates.js +172 -66
  35. package/dist/cjs/layout/text/text.js +1 -1
  36. package/dist/cjs/layout/text/typewriter/index.js +3 -3
  37. package/dist/cjs/layout/trackable.js +124 -0
  38. package/dist/cjs/plugin.js +1 -1
  39. package/dist/cjs/source/index.js +20 -8
  40. package/dist/cjs/util/bindings.js +58 -110
  41. package/dist/cjs/util/states.js +31 -24
  42. package/dist/cjs/util/trackers.js +75 -0
  43. package/dist/cjs/util/viewables.js +55 -0
  44. package/dist/cjs/version.js +1 -1
  45. package/dist/cjs/workflow/ask.js +60 -19
  46. package/dist/cjs/workflow/asks.js +1 -1
  47. package/dist/cjs/workflow/base.js +6 -7
  48. package/dist/cjs/workflow/explore.js +5 -2
  49. package/dist/cjs/workflow/options.js +60 -14
  50. package/dist/cjs/workflow/recommendation.js +2 -0
  51. package/dist/cjs/workflow/search.js +35 -7
  52. package/package.json +2 -2
  53. package/src/actor/autocomplete.js +131 -0
  54. package/src/actor/data.js +3 -10
  55. package/src/actor/fields.js +8 -0
  56. package/src/actor/index.js +1 -0
  57. package/src/actor/tracker.js +39 -62
  58. package/src/actor/trackers.js +3 -1
  59. package/src/actor/utils.js +14 -2
  60. package/src/actor/view.js +12 -1
  61. package/src/actor/views.js +11 -0
  62. package/src/affiliation/index.js +108 -0
  63. package/src/constants.js +10 -1
  64. package/src/defaults/ask/phrases.js +1 -1
  65. package/src/defaults/ask/templates.js +34 -2
  66. package/src/element/role/index.js +1 -0
  67. package/src/element/role/miso-affiliation.js +16 -0
  68. package/src/layout/affiliation/affiliation.js +295 -0
  69. package/src/layout/index.js +1 -0
  70. package/src/layout/input/option-list.js +35 -43
  71. package/src/layout/input/search-box.js +192 -95
  72. package/src/layout/list/carousel.js +4 -2
  73. package/src/layout/list/collection.js +42 -43
  74. package/src/layout/templates.js +127 -46
  75. package/src/layout/text/typewriter/index.js +2 -2
  76. package/src/layout/trackable.js +106 -0
  77. package/src/source/index.js +14 -1
  78. package/src/util/bindings.js +47 -103
  79. package/src/util/states.js +26 -20
  80. package/src/util/trackers.js +69 -0
  81. package/src/util/viewables.js +47 -0
  82. package/src/version.js +1 -1
  83. package/src/workflow/ask.js +37 -3
  84. package/src/workflow/base.js +2 -3
  85. package/src/workflow/explore.js +1 -1
  86. package/src/workflow/options.js +54 -13
  87. package/src/workflow/recommendation.js +1 -0
  88. package/src/workflow/search.js +33 -1
  89. package/dist/cjs/util/tracker.js +0 -348
  90. package/src/util/tracker.js +0 -318
@@ -1,53 +1,104 @@
1
- import { escapeHtml } from '@miso.ai/commons';
2
- import { LAYOUT_CATEGORY } from '../../constants.js';
1
+ import { defineValues, escapeHtml, findInAncestors, debounce } from '@miso.ai/commons';
2
+ import { LAYOUT_CATEGORY, STATUS } from '../../constants.js';
3
3
  import { fields } from '../../actor/index.js';
4
4
  import TemplateBasedLayout from '../template.js';
5
+ import { helpers, imageBlock, productInfoBlock } from '../templates.js';
5
6
  import { SEND, SEARCH } from '../../asset/svgs.js';
6
7
 
7
8
  const TYPE = 'search-box';
8
9
  const DEFAULT_CLASSNAME = 'miso-search-box';
10
+ const DEFAULT_AUTOCOMPLETE_CLASSNAME = 'miso-autocomplete';
9
11
 
10
12
  function root(layout) {
11
- const { className, role, templates, options, workflow } = layout;
12
- const { autocomplete, placeholder, buttonText } = options;
13
+ const { className, role, options, workflow, autocomplete } = layout;
14
+ const { placeholder, buttonText } = options;
13
15
  const roleAttr = role ? `data-role="${role}"` : '';
14
16
  const buttonContent = buttonText || workflow === 'search' ? SEARCH : SEND;
15
17
  return `
16
18
  <div class="${className}" ${roleAttr}>
17
19
  <div class="${className}__input-group">
18
20
  <input class="${className}__input" type="text" data-role="input" ${placeholder ? `placeholder="${placeholder}"` : ''}>
19
- <button class="${className}__button" type="submit" data-role="button">${buttonContent}</button>
21
+ <button class="${className}__button" type="submit" data-role="submit">${buttonContent}</button>
20
22
  </div>
21
- ${autocomplete ? templates.autocomplete(layout) : ''}
22
- </div>
23
- `;
23
+ <div class="${autocomplete.className}" data-role="autocomplete"></div>
24
+ </div>`.trim();
24
25
  }
25
26
 
26
- function autocomplete(layout) {
27
- const { className, templates } = layout;
28
- return `
29
- <div class="${className}__autocomplete" data-role="autocomplete">
30
- ${templates.suggestionList(layout)}
31
- </div>
32
- `;
27
+ function completions(layout, items) {
28
+ const { templates, workflow, autocomplete } = layout;
29
+ // use options under layout.autocomplete from now on
30
+ layout = {
31
+ templates,
32
+ workflow,
33
+ ...autocomplete,
34
+ };
35
+
36
+ const products = [], queries = [];
37
+ for (const item of items) {
38
+ (item.product ? products : queries).push(item);
39
+ }
40
+
41
+ return templates.queries(layout, queries) + (queries.length > 0 && products.length > 0 ? `<hr>` : '') + templates.products(layout, products);
42
+ }
43
+
44
+ function queries(layout, items) {
45
+ const { templates } = layout;
46
+ return templates.completionList(layout, 'query', items);
47
+ }
48
+
49
+ function products(layout, items) {
50
+ const { templates } = layout;
51
+ return templates.completionList(layout, 'product', items);
33
52
  }
34
53
 
35
- function suggestionList(layout) {
36
- const { className } = layout;
37
- return `<ol class="${className}__suggestion-list" data-role="suggestion-list"></ol>`;
54
+ function query(layout, { text, text_with_inverted_markups } = {}) {
55
+ return text_with_inverted_markups || escapeHtml(text);
38
56
  }
39
57
 
40
- function suggestionItem(layout, value) {
41
- const { className } = layout;
42
- const text = escapeHtml(value.text || value);
43
- return `<li class="${className}__suggestion-item" data-role="suggestion-item" tabindex="0">${text}</li>`;
58
+ function product(layout, { product }) {
59
+ const { templates } = layout;
60
+ const [openTag, closeTag] = helpers.tagPair(layout, product, { role: false });
61
+ return [
62
+ openTag,
63
+ templates.imageBlock(layout, product),
64
+ templates.productInfoBlock(layout, product),
65
+ closeTag,
66
+ ].join('');
67
+ }
68
+
69
+ function completionList(layout, type, items) {
70
+ const { className, templates } = layout;
71
+ return `
72
+ <ul class="${className}__${type}-list" data-role="${type}-list">
73
+ ${items.map(item => templates.completionItem(layout, type, item)).join('')}
74
+ </ul>`.trim();
75
+ }
76
+
77
+ function completionItem(layout, type, item) {
78
+ const { className, templates } = layout;
79
+ return `
80
+ <li class="${className}__${type}-item" data-role="${type}-item" data-index="${item._index}" tabindex="0">
81
+ ${templates[type](layout, item)}
82
+ </li>`.trim();
44
83
  }
45
84
 
46
85
  const DEFAULT_TEMPLATES = Object.freeze({
47
86
  root,
48
- autocomplete,
49
- suggestionList,
50
- suggestionItem,
87
+ completions,
88
+ completionList,
89
+ completionItem,
90
+ queries,
91
+ products,
92
+ query,
93
+ product,
94
+ imageBlock,
95
+ productInfoBlock,
96
+ helpers,
97
+ });
98
+
99
+ const DEFAULT_AUTOCOMPLETE_OPTIONS = Object.freeze({
100
+ className: DEFAULT_AUTOCOMPLETE_CLASSNAME,
101
+ debounce: 300,
51
102
  });
52
103
 
53
104
  export default class SearchBoxLayout extends TemplateBasedLayout {
@@ -68,31 +119,36 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
68
119
  return DEFAULT_CLASSNAME;
69
120
  }
70
121
 
71
- constructor({ className = DEFAULT_CLASSNAME, templates, ...options } = {}) {
122
+ constructor({ className = DEFAULT_CLASSNAME, templates, autocomplete, ...options } = {}) {
72
123
  super({
73
124
  className,
74
125
  templates: { ...DEFAULT_TEMPLATES, ...templates },
75
126
  ...options,
76
127
  });
128
+ defineValues(this, {
129
+ autocomplete: { ...DEFAULT_AUTOCOMPLETE_OPTIONS, ...autocomplete },
130
+ });
77
131
  this._contexts = new WeakMap();
78
- this._suggestionItems = new WeakMap(); // TODO: put them on elements as layout can be potentially replaced
132
+ this._prevInput = undefined;
133
+ this._debounce = debounce(this.autocomplete.debounce);
79
134
  }
80
135
 
81
136
  initialize(view) {
82
- const { autocomplete } = this.options;
83
137
  this._view = view;
84
138
  const { proxyElement, hub } = view;
85
139
  this._unsubscribes = [
86
140
  ...this._unsubscribes,
141
+ proxyElement.on('input', (e) => this._syncInput(e)),
87
142
  proxyElement.on('keydown', (e) => this._handleKeyDown(e)),
88
143
  proxyElement.on('click', (e) => this._handleClick(e)),
144
+ proxyElement.on('focusin', (e) => this._handleFocusIn(e)),
145
+ proxyElement.on('focusout', (e) => this._handleFocusOut(e)),
89
146
  ];
90
- if (autocomplete) {
91
- this._unsubscribes.push(hub.on(fields.suggestions(), () => this._handleInput()));
92
- }
147
+ this._unsubscribes.push(hub.on(fields.completions(), () => view.refresh({ force: true })));
93
148
  }
94
149
 
95
150
  focus() {
151
+ // TODO: support blur as well
96
152
  if (this._element) {
97
153
  this._context().focus();
98
154
  } else {
@@ -101,6 +157,14 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
101
157
  }
102
158
  }
103
159
 
160
+ open() {
161
+ this._element.classList.add('open');
162
+ }
163
+
164
+ close() {
165
+ this._element.classList.remove('open');
166
+ }
167
+
104
168
  set value(value) {
105
169
  if (this._element) {
106
170
  this._context().value = value;
@@ -111,12 +175,10 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
111
175
 
112
176
  _preprocess({ state }) {
113
177
  state = super._preprocess({ state });
114
- const { autocomplete } = this.options;
115
- if (!autocomplete) {
116
- return state;
117
- }
118
- const suggestions = this._view.hub.states[fields.suggestions()];
119
- return { ...state, suggestions };
178
+ return {
179
+ ...state,
180
+ completions: this._view.hub.states[fields.completions()],
181
+ };
120
182
  }
121
183
 
122
184
  _render(element, data, controls) {
@@ -124,42 +186,32 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
124
186
  // only render the template once, and also skip if there are children already to allow customized DOM
125
187
  super._render(element, data, controls);
126
188
  }
127
- this._setupAutocomplete(element);
128
- this._renderSuggestions(element, data, controls);
189
+ this._updateCompletions(element, data, controls);
129
190
  this._fullfillFocusRequest(element);
130
191
  this._fullfillValueRequest(element);
131
192
  }
132
193
 
133
- _setupAutocomplete(element) {
134
- const { autocomplete } = this.options;
135
- if (!autocomplete) {
194
+ _updateCompletions(element, { state, rendered }, { writeToState }) {
195
+ if (!state.completions) {
136
196
  return;
137
197
  }
138
- this._context(element).setupAutocomplete();
139
- }
140
-
141
- _renderSuggestions(element, { state }, { writeToState } = {}) {
142
- const { autocomplete } = this.options;
143
- if (!autocomplete) {
198
+ const { status = STATUS.INITIAL, index, value: completions } = state.completions;
199
+ if (rendered && rendered.completions.index === index && rendered.completions.status === status) {
144
200
  return;
145
201
  }
146
-
147
- const { suggestionListElement, autocompleteElement } = this._context(element);
148
- if (!suggestionListElement) {
149
- // not actually rendered
150
- writeToState && writeToState({ suggestions: { value: [] } });
202
+ const context = this._context(element);
203
+ const { autocompleteElement } = context;
204
+ if (!autocompleteElement) {
151
205
  return;
152
206
  }
153
207
 
154
- // TODO: incremental
155
- const { suggestions = {} } = state;
156
- const suggestionItems = suggestions.value || [];
157
- autocompleteElement.classList[suggestionItems.length === 0 ? 'add' : 'remove']('empty');
158
- suggestionListElement.innerHTML = suggestionItems.map((suggestion) => this.templates.suggestionItem(this, suggestion)).join('');
159
- let i = 0;
160
- for (const itemElement of suggestionListElement.children) {
161
- this._suggestionItems.set(itemElement, suggestionItems[i++]);
162
- }
208
+ const items = context.trackItems(completions);
209
+
210
+ element.classList[items.length > 0 ? 'add' : 'remove']('nonempty');
211
+ autocompleteElement.setAttribute('data-status', status);
212
+ autocompleteElement.innerHTML = this.templates.completions(this, items);
213
+
214
+ writeToState({ completions: { index, status } });
163
215
  }
164
216
 
165
217
  _fullfillFocusRequest(element) {
@@ -190,39 +242,80 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
190
242
  return context;
191
243
  }
192
244
 
245
+ _syncInput() {
246
+ const { inputElement } = this._context();
247
+ if (!inputElement) {
248
+ return;
249
+ }
250
+ const value = inputElement.value.trim();
251
+
252
+ if (this._prevInput === value) {
253
+ return;
254
+ }
255
+ this._prevInput = value;
256
+
257
+ const self = this;
258
+ this._debounce(() => self._view.hub.update(fields.input(), { value }));
259
+ }
260
+
261
+ _handleFocusIn({ target }) {
262
+ if (!this._element) {
263
+ return;
264
+ }
265
+ if (target.matches('[data-role="input"]')) {
266
+ this._syncInput();
267
+ this.open();
268
+ }
269
+ }
270
+
271
+ _handleFocusOut({ relatedTarget }) {
272
+ if (!this._element) {
273
+ return;
274
+ }
275
+ if (!relatedTarget || !findInAncestors(relatedTarget, element => element === this._element || undefined)) {
276
+ this.close();
277
+ }
278
+ }
279
+
193
280
  _handleKeyDown({ key, target, isComposing }) {
194
281
  if (!isComposing && key === 'Enter' && target.matches('input')) {
195
282
  this._submit(target.value);
196
283
  target.blur();
197
284
  }
285
+ // TODO: select completion items with arrow keys
198
286
  }
199
287
 
200
- _handleClick({ target }) {
288
+ _handleClick(event) {
289
+ const { target } = event;
201
290
  const { inputElement } = this._context();
202
- for (let element = target; element && element !== this._view.element; element = element.parentElement) {
203
- if (element.matches('[type="submit"]') || element.matches('[data-role="button"]')) {
291
+ for (let element = target; element && element !== this._element; element = element.parentElement) {
292
+ if (element.matches('[data-role="submit"]') || element.matches('[data-role="button"]') || element.matches('[type="submit"]')) {
204
293
  if (inputElement) {
205
294
  inputElement.blur();
206
295
  this._submit(inputElement.value);
207
296
  }
208
297
  return;
209
298
  }
210
- }
211
- const { autocomplete } = this.options;
212
- if (!autocomplete) {
213
- return;
214
- }
215
- for (let element = target; element && element !== this._view.element; element = element.parentElement) {
216
- const suggestion = this._suggestionItems.get(element);
217
- if (!suggestion) {
218
- continue;
299
+ if (element.matches('[data-role="query-item"]')) {
300
+ const index = element.getAttribute('data-index');
301
+ const item = this._context().data.get(parseInt(index));
302
+ const value = item && item.text;
303
+ if (value) {
304
+ if (inputElement) {
305
+ inputElement.value = value;
306
+ }
307
+ this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
308
+ if (!event.defaultPrevented) {
309
+ this._submit(value);
310
+ }
311
+ }
312
+ this.close();
313
+ return;
219
314
  }
220
- const value = suggestion.text || suggestion;
221
- if (inputElement) {
222
- inputElement.value = value;
315
+ if (element.matches('[data-role="product-item"]')) {
316
+ this._view._events.emit('select', Object.freeze({ value: item, index, domEvent: event }));
317
+ return;
223
318
  }
224
- this._submit(value);
225
- return;
226
319
  }
227
320
  }
228
321
 
@@ -230,6 +323,7 @@ export default class SearchBoxLayout extends TemplateBasedLayout {
230
323
  if (!value) {
231
324
  return;
232
325
  }
326
+ this.close();
233
327
  // TODO: q -> value
234
328
  this._view.hub.trigger(fields.query(), { q: value });
235
329
  }
@@ -245,6 +339,7 @@ class Context {
245
339
  constructor(element) {
246
340
  this._element = element;
247
341
  this._cache = {};
342
+ this._data = new Map();
248
343
  }
249
344
 
250
345
  get inputElement() {
@@ -255,25 +350,27 @@ class Context {
255
350
  return this._get('autocomplete');
256
351
  }
257
352
 
258
- get suggestionListElement() {
259
- return this._get('suggestion-list');
353
+ get data() {
354
+ return this._data;
260
355
  }
261
356
 
262
- setupAutocomplete() {
263
- if (!this._element || this._autocompleteSetup) {
264
- return;
265
- }
266
- this._autocompleteSetup = true;
267
- const { inputElement, autocompleteElement } = this;
268
- if (inputElement && autocompleteElement) {
269
- // TODO: should we clean up
270
- inputElement.addEventListener('focus', () => {
271
- autocompleteElement.classList.add('open');
272
- });
273
- inputElement.addEventListener('blur', () => {
274
- autocompleteElement.classList.remove('open');
275
- });
357
+ trackItems(completions) {
358
+ let index = 0;
359
+ const data = this._data = new Map();
360
+ const items = [];
361
+ for (const field in completions) {
362
+ for (const item of completions[field]) {
363
+ if (item.product && !item.product.url) {
364
+ continue; // product without URL is not clickable
365
+ }
366
+ item._index = index;
367
+ item._field = field;
368
+ data.set(index, item);
369
+ items.push(item);
370
+ index++;
371
+ }
276
372
  }
373
+ return items;
277
374
  }
278
375
 
279
376
  focus() {
@@ -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 } 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 } = {}) {
@@ -57,6 +55,19 @@ const DEFAULT_TEMPLATES = Object.freeze({
57
55
  ordered: false,
58
56
  items,
59
57
  item,
58
+ productInfoBlock,
59
+ articleInfoBlock,
60
+ brandBlock,
61
+ titleBlock,
62
+ descriptionBlock,
63
+ dateBlock,
64
+ priceBlock,
65
+ discountRateText,
66
+ ctaBlock,
67
+ cta,
68
+ imageBlock,
69
+ indexBlock,
70
+ helpers,
60
71
  });
61
72
 
62
73
  export default class CollectionLayout extends TemplateBasedLayout {
@@ -75,18 +86,13 @@ export default class CollectionLayout extends TemplateBasedLayout {
75
86
  itemType,
76
87
  ...options,
77
88
  });
78
- defineValues(this, {
79
- bindings: Object.freeze({
80
- list: this._listBindings.bind(this),
81
- }),
82
- });
83
89
  this._view = undefined;
90
+ this._initTrackable();
84
91
  }
85
92
 
86
93
  initialize(view) {
87
94
  this._view = view;
88
- const { proxyElement } = this._view = view;
89
- this._unsubscribes.push(proxyElement.on('click', this._onClick.bind(this)));
95
+ this._unsubscribes.push(view.proxyElement.on('click', this._onClick.bind(this)));
90
96
  }
91
97
 
92
98
  _preprocess({ state, rendered }) {
@@ -112,7 +118,7 @@ export default class CollectionLayout extends TemplateBasedLayout {
112
118
  _html(state, rendered, incremental) {
113
119
  if (incremental) {
114
120
  const offset = rendered.value.length;
115
- const values = state.value.slice(offset);
121
+ const values = (this._getItems(state) || []).slice(offset);
116
122
  return values.length > 0 ? this.templates.items(this, state, values, { offset }) : '';
117
123
  } else {
118
124
  return this.templates.root(this, state);
@@ -131,42 +137,23 @@ export default class CollectionLayout extends TemplateBasedLayout {
131
137
  } else {
132
138
  element.innerHTML = html;
133
139
  }
134
- this._syncValues(element, state);
140
+ this._syncBindings(element, state);
135
141
  }
136
142
 
137
- _syncValues(element, state) {
138
- if (!element) {
139
- return;
140
- }
141
- const values = state.value || [];
142
- let i = 0;
143
- for (const itemElement of this._listItemElements(element)) {
144
- itemElement[VALUE] = values[i++];
145
- }
143
+ _unrender() {
144
+ this._clearBindings();
146
145
  }
147
146
 
148
- _getListElement(element) {
149
- return element.querySelector(`.${this.className}__list`);
147
+ _getItems(state) {
148
+ return state.value;
150
149
  }
151
150
 
152
- _listItemElements(element) {
153
- return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
151
+ _getListElement(element) {
152
+ return element.querySelector('[data-role="list"]');
154
153
  }
155
154
 
156
- _listBindings(rootElement) {
157
- if (!rootElement) {
158
- return [];
159
- }
160
- const bindings = [];
161
- for (const element of this._listItemElements(rootElement)) {
162
- const value = element[VALUE];
163
- if (!value) {
164
- continue;
165
- }
166
- const key = this.options.itemType === 'product' ? value.product_id : value;
167
- bindings.push({ element, key, value });
168
- }
169
- return bindings;
155
+ _getItemElements(element) {
156
+ return element ? Array.from(element.querySelectorAll(`[data-role="item"]`)) : [];
170
157
  }
171
158
 
172
159
  _onClick(event) {
@@ -174,14 +161,26 @@ export default class CollectionLayout extends TemplateBasedLayout {
174
161
  if (!element) {
175
162
  return;
176
163
  }
177
- 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) {
178
173
  const { session } = this._view._state;
174
+ const { value, element } = binding;
179
175
  this._view._events.emit('click', { session, value, element, domEvent: event });
180
176
  }
181
177
 
182
178
  destroy() {
179
+ this._destroyTrackable();
183
180
  this._view = undefined;
184
181
  super.destroy();
185
182
  }
186
183
 
187
184
  }
185
+
186
+ makeTrackable(CollectionLayout.prototype);