• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yext / answers-search-ui / 16886612290

11 Aug 2025 04:53PM UTC coverage: 61.442% (+0.001%) from 61.441%
16886612290

Pull #1952

github

mkouzel-yext
Update extracted translations

Ran `npm run extract-translations`
Pull Request #1952: Update click event listeners v1.15

1965 of 3348 branches covered (58.69%)

Branch coverage included in aggregate %.

4 of 10 new or added lines in 2 files covered. (40.0%)

43 existing lines in 2 files now uncovered.

3378 of 5348 relevant lines covered (63.16%)

26.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

84.08
/src/ui/components/component.js
1
/** @module Component */
2

3
import cloneDeep from 'lodash.clonedeep';
4

5
import { Renderers } from '../rendering/const';
6

7
import DOM from '../dom/dom';
8
import State from './state';
9
import { AnalyticsReporter } from '../../core'; // eslint-disable-line no-unused-vars
10
import AnalyticsEvent from '../../core/analytics/analyticsevent';
11
import { AnswersComponentError } from '../../core/errors/errors';
12

13
/**
14
 * Component is an abstraction that encapsulates state, behavior,
15
 * and view for a particular chunk of functionality on the page.
16
 *
17
 * The API exposes event life cycle hooks for when things are rendered,
18
 * mounted, created, etc.
19
 */
20
export default class Component {
21
  constructor (config = {}, systemConfig = {}) {
×
22
    this.moduleId = null;
242✔
23

24
    /**
25
     * A unique id number for the component.
26
     * @type {number}
27
     */
28
    this.uniqueId = systemConfig.uniqueId;
242✔
29

30
    /**
31
     * Name of this component instance.
32
     * @type {String}
33
     */
34
    this.name = config.name || this.constructor.type;
242✔
35

36
    /**
37
     * Cache the options so that we can propogate properly to child components
38
     * @type {Object}
39
     */
40
    this._config = config;
242✔
41

42
    /**
43
     * An identifier used to classify the type of component.
44
     * The component manager uses this information in order to persist and organize components
45
     * @type {string|ComponentType}
46
     */
47
    this._type = this.constructor.name;
242✔
48

49
    /**
50
     * A local reference to the parent component, if exists
51
     * @type {Component}
52
     */
53
    this._parentContainer = config.parentContainer || null;
242✔
54

55
    /**
56
     * A container for all the child components
57
     * @type {Component[]}
58
     */
59
    this._children = [];
242✔
60

61
    /**
62
     * The state (data) of the component to be provided to the template for rendering
63
     * @type {object}
64
     */
65
    this._state = new State(config.state);
242✔
66

67
    /**
68
     * TODO(billy) This should be 'services'
69
     */
70
    this.core = systemConfig.core || null;
242!
71

72
    /**
73
     * A local reference to the component manager, which contains all of the component classes
74
     * eligible to be created
75
     * @type {ComponentManager}
76
     */
77
    this.componentManager = systemConfig.componentManager || null;
242✔
78

79
    /**
80
     * A local reference to the analytics reporter, used to report events for this component
81
     * @type {AnalyticsReporter}
82
     */
83
    this.analyticsReporter = systemConfig.analyticsReporter || null;
242✔
84

85
    /**
86
     * Options to include with all analytic events sent by this component
87
     * @type {object}
88
     * @private
89
     */
90
    this._analyticsOptions = config.analyticsOptions || {};
242✔
91

92
    /**
93
     * Allows the main thread to regain control while rendering child components
94
     * @type {boolean}
95
     */
96
    this._progressivelyRenderChildren = config.progressivelyRenderChildren;
242✔
97

98
    /**
99
     * A reference to the DOM node that the component will be appended to when mounted/rendered.
100
     * @type {HTMLElement}
101
     */
102
    if (this._parentContainer === null) {
242✔
103
      if (typeof config.container === 'string') {
187✔
104
        this._container = DOM.query(config.container) || null;
149!
105
        if (this._container === null) {
149!
106
          throw new Error('Cannot find container DOM node: ' + config.container);
×
107
        }
108
      }
109
    } else {
110
      this._container = DOM.query(this._parentContainer, config.container);
55✔
111

112
      // If we have a parent, and the container is missing from the DOM,
113
      // we construct the container and append it to the parent
114
      if (this._container === null) {
55✔
115
        this._container = DOM.createEl('div', {
1✔
116
          class: config.container.substring(1, config.container.length)
117
        });
118
        DOM.append(this._parentContainer, this._container);
1✔
119
      }
120
    }
121

122
    /**
123
     * A custom class to be applied to {this._container} node. Note that the class
124
     * 'yxt-Answers-component' will be included as well.
125
     * @type {string}
126
     */
127
    this._className = config.class || 'component';
242✔
128

129
    /**
130
     * A custom render function to be used instead of using the default renderer
131
     * @type {Renderer}
132
     */
133
    this._render = config.render || null;
242✔
134

135
    /**
136
     * A local reference to the default {Renderer} that will be used for rendering the template
137
     * @type {Renderer}
138
     */
139
    this._renderer = systemConfig.renderer || Renderers.Handlebars;
242✔
140

141
    /**
142
     * The template string to use for rendering the component
143
     * If this is left empty, we lookup the template the base templates using the templateName
144
     * @type {string}
145
     */
146
    this._template = config.template ? this._renderer.compile(config.template) : null;
242✔
147

148
    /**
149
     * The templateName to use for rendering the component.
150
     * This is only used if _template is empty.
151
     * @type {string}
152
     */
153
    this._templateName = config.templateName || this.constructor.defaultTemplateName(config);
242✔
154

155
    /**
156
     * An internal state indicating whether or not the component has been mounted to the DOM
157
     * @type {boolean}
158
     */
159
    this._isMounted = false;
242✔
160

161
    /**
162
     * A local reference to the callback, thats used to transform the internal data
163
     * models of the components, before it gets applied to the component state.
164
     * By default, no transformation happens.
165
     * @type {function}
166
     */
167
    this.transformData = config.transformData;
242✔
168

169
    /**
170
     * A local reference to the callback that will be invoked when a component is created.
171
     * @type {function}
172
     */
173
    this.onCreate = config.onCreateOverride || this.onCreate || function () {};
242!
174
    this.onCreate = this.onCreate.bind(this);
242✔
175

176
    /**
177
     * A local reference to the callback that will be invoked before a component is mounted.
178
     * @type {function}
179
     */
180
    this.beforeMount = config.beforeMountOverride || this.beforeMount || function () {};
242!
181
    this.beforeMount = this.beforeMount.bind(this);
242✔
182

183
    /**
184
     * A local reference to the callback that will be invoked when a component is mounted.
185
     * @type {function}
186
     */
187
    this.onMount = config.onMountOverride || this.onMount || function () {};
242!
188
    this.onMount = this.onMount.bind(this);
242✔
189

190
    /**
191
     * A local reference to the callback that will be invoked when a components state is updated.
192
     * @type {function}
193
     */
194
    this.onUpdate = config.onUpdateOverride || this.onUpdate || function () { };
242!
195
    this.onUpdate = this.onUpdate.bind(this);
242✔
196

197
    /**
198
     * A user provided onCreate callback
199
     * @type {function}
200
     */
201
    this.userOnCreate = config.onCreate || function () {};
242✔
202

203
    /**
204
     * A user provided onMount callback
205
     * @type {function}
206
     */
207
    this.userOnMount = config.onMount || function () {};
242✔
208

209
    /**
210
     * A user provided onUpdate callback
211
     * @type {function}
212
     */
213
    this.userOnUpdate = config.onUpdate || function () {};
242✔
214
  }
215

216
  /**
217
   * The template to render
218
   * @returns {string}
219
   * @override
220
   */
221
  static defaultTemplateName (config) {
222
    return 'default';
9✔
223
  }
224

225
  static get type () {
226
    return 'Component';
33✔
227
  }
228

229
  static areDuplicateNamesAllowed () {
230
    return false;
1✔
231
  }
232

233
  init (opts) {
234
    try {
236✔
235
      this.setState(opts.data || opts.state || {});
236✔
236
      this.onCreate();
236✔
237
      this.userOnCreate();
236✔
238
    } catch (e) {
239
      throw new AnswersComponentError(
×
240
        'Error initializing component',
241
        this.constructor.type,
242
        e);
243
    }
244

245
    this._state.on('update', () => {
236✔
246
      try {
121✔
247
        this.onUpdate();
121✔
248
        this.userOnUpdate();
121✔
249
        this.unMount();
121✔
250
        this.mount();
121✔
251
      } catch (e) {
252
        throw new AnswersComponentError(
×
253
          'Error updating component',
254
          this.constructor.type,
255
          e);
256
      }
257
    });
258

259
    DOM.addClass(this._container, this._className);
236✔
260
    DOM.addClass(this._container, 'yxt-Answers-component');
236✔
261
    return this;
236✔
262
  }
263

264
  /**
265
   * Adds a class to the container of the component.
266
   * @param {string} className A comma separated value of classes
267
   */
268
  addContainerClass (className) {
269
    DOM.addClass(this._container, className);
73✔
270
  }
271

272
  /**
273
   * Removes the specified classes from the container of the component
274
   * @param {string} className A comma separated value of classes
275
   */
276
  removeContainerClass (className) {
277
    DOM.removeClass(this._container, className);
219✔
278
  }
279

280
  setState (data) {
281
    const newState = Object.assign({}, { _config: this._config }, data);
357✔
282
    this._state.set(newState);
357✔
283
    return this;
357✔
284
  }
285

286
  getState (prop) {
287
    return this._state.get(prop);
49✔
288
  }
289

290
  hasState (prop) {
291
    return this._state.has(prop);
4✔
292
  }
293

294
  addChild (data, type, opts) {
295
    const childComponent = this.componentManager.create(
2✔
296
      type,
297
      Object.assign({
298
        name: data.name,
299
        parentContainer: this._container,
300
        data: data
301
      }, opts || {}, {
2!
302
        _parentOpts: this._config
303
      })
304
    );
305

306
    this._children.push(childComponent);
2✔
307
    return childComponent;
2✔
308
  }
309

310
  /**
311
   * Unmount and remove this component and its children from the list
312
   * of active components
313
   */
314
  remove () {
315
    this._children.forEach(c => c.remove());
10✔
316
    this.componentManager.remove(this);
10✔
317
  }
318

319
  /**
320
   * Set the render method to be used for rendering the component
321
   * @param {Function} render
322
   * @return {string}
323
   */
324
  setRender (render) {
325
    this._render = render;
×
326
    return this;
×
327
  }
328

329
  /**
330
   * Set the renderer for the component
331
   * @param {RendererType} renderer
332
   */
333
  setRenderer (renderer) {
334
    this._renderer = Renderers[renderer];
×
335
    return this;
×
336
  }
337

338
  /**
339
   * Sets the template for the component to use when rendering
340
   * @param {string} template
341
   */
342
  setTemplate (template) {
343
    this._template = this._renderer.compile(template);
5✔
344
  }
345

346
  unMount () {
347
    if (!this._container) {
121✔
348
      return this;
15✔
349
    }
350

351
    this._children.forEach(child => {
106✔
352
      child.unMount();
×
353
    });
354

355
    DOM.empty(this._container);
106✔
356
    this._children.forEach(c => c.remove());
106✔
357
    this._children = [];
106✔
358
    this.onUnMount();
106✔
359
  }
360

361
  mount (container) {
362
    if (container) {
285✔
363
      this._container = container;
106✔
364
    }
365

366
    if (!this._container) {
285✔
367
      return this;
16✔
368
    }
369

370
    if (this.beforeMount() === false) {
269✔
371
      return this;
3✔
372
    }
373

374
    DOM.append(this._container, this.render(this._state.asJSON()));
266✔
375

376
    // Process the DOM to determine if we should create
377
    // in-memory sub-components for rendering
378
    const domComponents = DOM.queryAll(this._container, '[data-component]:not([data-is-component-mounted])');
266✔
379
    let data;
380
    try {
266✔
381
      data = this.transformData
266!
382
        ? this.transformData(cloneDeep(this._state.get()))
383
        : this._state.get();
384
    } catch (e) {
385
      console.error(`The following problem occurred while transforming data for sub-components of ${this.name}: `, e);
×
386
    }
387
    domComponents.forEach(c => {
266✔
388
      try {
3✔
389
        this._createSubcomponent(c, data);
3✔
390
      } catch (e) {
391
        console.error('The following problem occurred while initializing sub-component: ', c, e);
×
392
      }
393
    });
394
    if (this._progressivelyRenderChildren) {
266!
395
      this._children.forEach(child => {
×
396
        setTimeout(() => {
×
397
          try {
×
398
            child.mount();
×
399
          } catch (e) {
400
            console.error('The following problem occurred while mounting sub-component: ', child, e);
×
401
          }
402
        });
403
      });
404
    } else {
405
      this._children.forEach(child => {
266✔
406
        try {
2✔
407
          child.mount();
2✔
408
        } catch (e) {
409
          console.error('The following problem occurred while mounting sub-component: ', child, e);
×
410
        }
411
      });
412
    }
413

414
    // Attach analytics hooks as necessary
415
    if (this.analyticsReporter) {
266!
416
      const domHooks = DOM.queryAll(this._container, '[data-eventtype]:not([data-is-analytics-attached])');
266✔
417
      domHooks.forEach(this._createAnalyticsHook.bind(this));
266✔
418
    }
419

420
    this._isMounted = true;
266✔
421
    this.onMount(this);
266✔
422
    this.userOnMount(this);
266✔
423

424
    return this;
266✔
425
  }
426

427
  /**
428
   * render the template using the {Renderer} with the current state and template of the component
429
   * @returns {string}
430
   */
431
  render (data = this._state.get()) {
2✔
432
    this.beforeRender();
268✔
433
    // Temporary fix for passing immutable data to transformData().
434
    data = this.transformData
268!
435
      ? this.transformData(cloneDeep(data))
436
      : data;
437

438
    let html = '';
268✔
439
    // Use either the custom render function or the internal renderer
440
    // dependant on the component configuration
441
    if (typeof this._render === 'function') {
268!
442
      html = this._render(data);
×
443
      if (typeof html !== 'string') {
×
444
        throw new Error('Render method must return HTML as type {string}');
×
445
      }
446
    } else {
447
      // Render the existing templates as a string
448
      html = this._renderer.render({
268✔
449
        template: this._template,
450
        templateName: this._templateName
451
      }, data);
452
    }
453

454
    this.afterRender();
268✔
455
    return html;
268✔
456
  }
457

458
  _createSubcomponent (domComponent, data) {
459
    domComponent.dataset.isComponentMounted = true;
3✔
460
    const dataset = domComponent.dataset;
3✔
461
    const type = dataset.component;
3✔
462
    const prop = dataset.prop;
3✔
463
    let opts = dataset.opts ? JSON.parse(dataset.opts) : {};
3✔
464

465
    const childData = data[prop] || {};
3✔
466

467
    opts = {
3✔
468
      ...opts,
469
      container: domComponent
470
    };
471

472
    // TODO(billy) Right now, if we provide an array as the data prop,
473
    // the behavior is to create many components for each item in the array.
474
    // THAT interface SHOULD change to use a different property that defines
475
    // whether to array data should be used for a single component or
476
    // to create many components for each item.
477
    // Overloading and having this side effect is unintuitive and WRONG
478
    if (!Array.isArray(childData)) {
3✔
479
      // Rendering a sub component should be within the context,
480
      // of the node that we processed it from
481
      this.addChild(childData, type, opts);
2✔
482
      return;
2✔
483
    }
484

485
    childData.reverse();
1✔
486
    childData.forEach(data => {
1✔
487
      this.addChild(data, type, opts);
×
488
    });
489
  }
490

491
  _createAnalyticsHook (domComponent) {
492
    domComponent.dataset.isAnalyticsAttached = true;
101✔
493
    const dataset = domComponent.dataset;
101✔
494
    const type = dataset.eventtype;
101✔
495
    const label = dataset.eventlabel;
101✔
496
    const middleclick = dataset.middleclick;
101✔
497
    const options = dataset.eventoptions ? JSON.parse(dataset.eventoptions) : {};
101✔
498
    const handleClick = (e) => {
101✔
499
      if (e.button === 0 || (middleclick && e.button === 1)) {
12!
500
        const event = new AnalyticsEvent(type, label);
4✔
501
        event.addOptions(this._analyticsOptions);
4✔
502
        event.addOptions(options);
4✔
503
        this.analyticsReporter.report(event);
4✔
504
      }
505
    };
506

507
    DOM.on(domComponent, 'click', e => {
101✔
508
      handleClick(e);
12✔
509
    });
510
    DOM.on(domComponent, 'auxclick', e => {
101✔
NEW
511
      handleClick(e);
×
512
    });
513
  }
514

515
  /**
516
   * onCreate is triggered when the component is constructed
517
   * @param {function} the callback to invoke upon emit
518
   */
519
  onCreate (cb) {
520

521
  }
522

523
  /**
524
   * onUpdate is triggered when the state of the component changes
525
   * @param {function} the callback to invoke upon emit
526
   */
527
  onUpdate (cb) {
528

529
  }
530

531
  /**
532
   * beforeRender event is triggered before the component is rendered
533
   * @param {function} the callback to invoke upon emit
534
   */
535
  beforeRender (cb) {
536

537
  }
538

539
  /**
540
   * afterRender event is triggered after the component is rendered
541
   * @param {function} the callback to invoke upon emit
542
   */
543
  afterRender (cb) {
544

545
  }
546

547
  /**
548
   * onMount is triggered when the component is appended to the DOM
549
   * @param {function} the callback to invoke upon emit
550
   */
551
  onMount (cb) {
552

553
  }
554

555
  /**
556
   * onUnMount is triggered when the component is removed from the DOM
557
   * @param {function} the callback to invoke upon emit
558
   */
559
  onUnMount (cb) {
560

561
  }
562

563
  /**
564
   * beforeMount is triggered before the component is mounted to the DOM
565
   * @param {function} the callback to invoke upon emit
566
   */
567
  beforeMount (cb) {
568

569
  }
570

571
  /**
572
   * onDestroy is triggered when the component is destroyed
573
   * @param {function} the callback to invoke upon emit
574
   */
575
  onDestroy (cb) {
576

577
  }
578
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc