@financial-times/custom-code-component 2.0.22 → 2.0.25

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.
@@ -69,7 +69,12 @@ export class FTCustomCodeComponent extends HTMLElement {
69
69
  * Logger instance. Set log level using `this.logger.setLogLevel(string|number|null)`.
70
70
  */
71
71
  logger: Logger;
72
-
72
+
73
+ /**
74
+ * Abort controller used to abort async connectedCallback functionality when disconnectedCallback is called`.
75
+ */
76
+ ac: AbortController | null = null;
77
+
73
78
  /**
74
79
  * IntersectionObserver that dispatches CCCViewportEvents
75
80
  */
@@ -82,7 +87,7 @@ export class FTCustomCodeComponent extends HTMLElement {
82
87
  source: this.source,
83
88
  intersecting: entry.isIntersecting,
84
89
  entry,
85
- })
90
+ }),
86
91
  );
87
92
  });
88
93
  });
@@ -96,7 +101,7 @@ export class FTCustomCodeComponent extends HTMLElement {
96
101
  constructor() {
97
102
  super();
98
103
  this.logger = new Logger();
99
-
104
+
100
105
  const supportsDeclarative =
101
106
  HTMLElement.prototype.hasOwnProperty("attachInternals");
102
107
 
@@ -120,10 +125,11 @@ export class FTCustomCodeComponent extends HTMLElement {
120
125
  this.logger.component = this.component;
121
126
  this.logger.setLogLevel(this.getAttribute("log"));
122
127
  this.logger.debug("connectedCallback");
123
-
124
- this.app = await this.load();
125
- await this.mount();
126
- await this.initTracking();
128
+ this.ac?.abort();
129
+ this.ac = new AbortController();
130
+ this.app = await this.load(this.ac.signal);
131
+ this.mount();
132
+ this.initTracking();
127
133
  } catch (e) {
128
134
  if (e instanceof Error) {
129
135
  requestAnimationFrame(() => {
@@ -150,7 +156,7 @@ export class FTCustomCodeComponent extends HTMLElement {
150
156
  composed: true,
151
157
  error,
152
158
  message: error.message,
153
- })
159
+ }),
154
160
  );
155
161
  } else {
156
162
  // Wrap original error with generic CCC error class
@@ -166,7 +172,7 @@ export class FTCustomCodeComponent extends HTMLElement {
166
172
  composed: true,
167
173
  error: wrappedError,
168
174
  message: wrappedError.message,
169
- })
175
+ }),
170
176
  );
171
177
  }
172
178
  }
@@ -177,8 +183,10 @@ export class FTCustomCodeComponent extends HTMLElement {
177
183
  * Called when a <custom-code-component> element is removed from DOM.
178
184
  */
179
185
  disconnectedCallback() {
186
+ this.ac?.abort();
180
187
  this.logger.debug("disconnectedCallback");
181
188
  const path = this.getAttribute("path");
189
+ this.destroyTracking();
182
190
  this.logger.info(`<custom-code-component:${path}> disconnected`);
183
191
  this.observer.disconnect();
184
192
  }
@@ -233,7 +241,7 @@ export class FTCustomCodeComponent extends HTMLElement {
233
241
  new CCCReadyEvent({
234
242
  component: this.component,
235
243
  source: this.source,
236
- })
244
+ }),
237
245
  );
238
246
 
239
247
  this.dataset.cccReady = "true";
@@ -274,7 +282,7 @@ export class FTCustomCodeComponent extends HTMLElement {
274
282
  *
275
283
  * @param prerendered
276
284
  */
277
- async mount(prerendered?: ShadowRoot | null) {
285
+ mount(prerendered?: ShadowRoot | null) {
278
286
  this.logger.debug("mount", prerendered);
279
287
  try {
280
288
  this.mode =
@@ -292,7 +300,7 @@ export class FTCustomCodeComponent extends HTMLElement {
292
300
  new CCCConnectedEvent({
293
301
  component: this.component,
294
302
  source: this.source,
295
- })
303
+ }),
296
304
  );
297
305
  // Add global CCC styles if not already present in shadow DOM.
298
306
  if (
@@ -322,9 +330,9 @@ export class FTCustomCodeComponent extends HTMLElement {
322
330
  .filter(
323
331
  (attribute) =>
324
332
  !this.RESERVED_ATTRS.has(attribute.name) &&
325
- !attribute.name.startsWith("data-")
333
+ !attribute.name.startsWith("data-"),
326
334
  )
327
- .map((attribute) => [attribute.name, attribute.value])
335
+ .map((attribute) => [attribute.name, attribute.value]),
328
336
  );
329
337
 
330
338
  // Create tracking instance
@@ -340,12 +348,12 @@ export class FTCustomCodeComponent extends HTMLElement {
340
348
  this.logger.warn(
341
349
  `CCC ${this.component.toString()}: passing component settings as webcomponent attributes is %cDEPRECATED`,
342
350
  "font-weight: bold",
343
- " and will be removed in v3."
351
+ " and will be removed in v3.",
344
352
  );
345
353
  this.logger.warn(
346
354
  `Please use the %cdata-component-props`,
347
355
  "text-decoration: underline;",
348
- " attribute instead."
356
+ " attribute instead.",
349
357
  );
350
358
  }
351
359
 
@@ -362,7 +370,7 @@ export class FTCustomCodeComponent extends HTMLElement {
362
370
  children: this.children,
363
371
  },
364
372
  ssr,
365
- this.onunmount.bind(this)
373
+ this.onunmount.bind(this),
366
374
  ) || {};
367
375
 
368
376
  if (onmessage) this.onmessage = onmessage;
@@ -371,7 +379,7 @@ export class FTCustomCodeComponent extends HTMLElement {
371
379
  }
372
380
  } catch (err) {
373
381
  this.logger.info(
374
- `<custom-code-component> uncaught error during mount from ${this.component?.toString()}`
382
+ `<custom-code-component> uncaught error during mount from ${this.component?.toString()}`,
375
383
  );
376
384
  this.logger.error(err);
377
385
 
@@ -391,7 +399,7 @@ export class FTCustomCodeComponent extends HTMLElement {
391
399
 
392
400
  const template =
393
401
  this.querySelector<HTMLTemplateElement>(
394
- "template[data-component-fallback]"
402
+ "template[data-component-fallback]",
395
403
  ) ?? this.querySelector<HTMLTemplateElement>("template");
396
404
 
397
405
  if (template) {
@@ -412,7 +420,7 @@ export class FTCustomCodeComponent extends HTMLElement {
412
420
  *
413
421
  * @returns Promise resolving to component renderer function
414
422
  */
415
- async load() {
423
+ async load(signal: AbortSignal) {
416
424
  this.logger.debug("load");
417
425
 
418
426
  if (!this.component.isValid) {
@@ -426,7 +434,7 @@ export class FTCustomCodeComponent extends HTMLElement {
426
434
  this.testUrl = assignTestURL(testEnv);
427
435
  const isTestEnv = await useComponentTestEnv(
428
436
  this.component.name,
429
- this.testUrl
437
+ this.testUrl,
430
438
  );
431
439
 
432
440
  // id querystring necessary to multiple allow components with the same source (same name and version number) to appear on the page correctly
@@ -446,13 +454,14 @@ export class FTCustomCodeComponent extends HTMLElement {
446
454
  try {
447
455
  return await new Promise<typeof BaseRenderer.prototype.render>(
448
456
  (resolve, reject) => {
457
+ signal.throwIfAborted();
449
458
  const to = setTimeout(() => {
450
459
  this.logger.error("CCC import timeout error");
451
460
  reject(
452
461
  new CCCTimeoutError({
453
462
  component: this.component!,
454
463
  source: this.source,
455
- })
464
+ }),
456
465
  );
457
466
  }, Number(timeout));
458
467
 
@@ -468,7 +477,7 @@ export class FTCustomCodeComponent extends HTMLElement {
468
477
  {
469
478
  component: this.component!,
470
479
  source: this.source,
471
- }
480
+ },
472
481
  );
473
482
  })
474
483
  .catch((e) => {
@@ -479,7 +488,7 @@ export class FTCustomCodeComponent extends HTMLElement {
479
488
  new CCCImportError(e.message, {
480
489
  component: this.component!,
481
490
  source: this.source,
482
- })
491
+ }),
483
492
  );
484
493
  } else {
485
494
  reject(e);
@@ -492,11 +501,21 @@ export class FTCustomCodeComponent extends HTMLElement {
492
501
  source: this.source,
493
502
  });
494
503
  }
495
- }
504
+
505
+ signal.addEventListener(
506
+ "abort",
507
+ () => {
508
+ // Stop the main operation
509
+ // Reject the promise with the abort reason.
510
+ reject(signal.reason);
511
+ },
512
+ { once: true },
513
+ );
514
+ },
496
515
  );
497
516
  } catch (err) {
498
517
  this.logger.error(
499
- `<custom-code-component> error during import from ${path}@${componentVersionRange}`
518
+ `<custom-code-component> error during import from ${path}@${componentVersionRange}`,
500
519
  );
501
520
 
502
521
  throw err;
@@ -506,7 +525,7 @@ export class FTCustomCodeComponent extends HTMLElement {
506
525
  /**
507
526
  * Initialises OTracking
508
527
  */
509
- initTracking = async () => {
528
+ initTracking = () => {
510
529
  this.logger.debug("initTracking", { cccId: this.id });
511
530
  try {
512
531
  this.tracking?.init(this.id);
@@ -514,7 +533,24 @@ export class FTCustomCodeComponent extends HTMLElement {
514
533
  const path = this.getAttribute("path");
515
534
  const componentVersionRange = this.getAttribute("version");
516
535
  this.logger.info(
517
- `Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`
536
+ `Error initialising tracking on <custom-code-component> ${path}@${componentVersionRange}`,
537
+ );
538
+ this.logger.error(e);
539
+ }
540
+ };
541
+
542
+ /**
543
+ * Destroys OTracking to avoid duplicate events
544
+ */
545
+ destroyTracking = () => {
546
+ this.logger.debug("destroyTracking", { cccId: this.id });
547
+ try {
548
+ this.tracking?.destroy();
549
+ } catch (e) {
550
+ const path = this.getAttribute("path");
551
+ const componentVersionRange = this.getAttribute("version");
552
+ this.logger.info(
553
+ `Error destroying tracking on <custom-code-component> ${path}@${componentVersionRange}`,
518
554
  );
519
555
  this.logger.error(e);
520
556
  }
package/src/logger.ts CHANGED
@@ -49,7 +49,7 @@ export class Logger {
49
49
  level: LogLevel.DEFAULT,
50
50
  }
51
51
  ) {
52
- this.level = localStorage.getItem("CCC_LOG_LEVEL")
52
+ this.level = localStorage?.getItem("CCC_LOG_LEVEL")
53
53
  ? convertStringLogLevel(localStorage.getItem("CCC_LOG_LEVEL"))
54
54
  : level;
55
55
  this.component = component;
@@ -82,7 +82,7 @@ export class Logger {
82
82
  }
83
83
 
84
84
  setLogLevel(level: string | number | null) {
85
- if (localStorage.getItem("CCC_LOG_LEVEL")) {
85
+ if (localStorage?.getItem("CCC_LOG_LEVEL")) {
86
86
  this.level = convertStringLogLevel(localStorage.getItem("CCC_LOG_LEVEL"));
87
87
  } else if (typeof level === "number") {
88
88
  this.level = level;
package/src/tracking.ts CHANGED
@@ -19,6 +19,7 @@ class Tracking {
19
19
  elements: string | string[];
20
20
  isInitialised: boolean;
21
21
  log: Logger;
22
+ shadowDelegate: typeof Delegate;
22
23
 
23
24
  constructor({
24
25
  id = "00000000-0000-0000-0000-000000000000",
@@ -68,7 +69,7 @@ class Tracking {
68
69
  // Controller for handling click events
69
70
  handleClickEvent(
70
71
  eventData: { action: string; category: string },
71
- root: Element
72
+ root: Element,
72
73
  ) {
73
74
  return (clickEvent: Event, clickElement: HTMLElement) => {
74
75
  const context: any = this.getEventProperties(clickEvent);
@@ -98,7 +99,7 @@ class Tracking {
98
99
  detail: eventData,
99
100
  bubbles: true,
100
101
  composed: true,
101
- })
102
+ }),
102
103
  );
103
104
  };
104
105
  }
@@ -125,7 +126,7 @@ class Tracking {
125
126
  detail: eventData,
126
127
  bubbles: true,
127
128
  composed: true,
128
- })
129
+ }),
129
130
  );
130
131
  }
131
132
 
@@ -142,16 +143,20 @@ class Tracking {
142
143
  const root = this.shadowRoot?.querySelector("[data-component-root]");
143
144
 
144
145
  if (root) {
145
- const shadowDelegate = new Delegate(root);
146
- shadowDelegate.on(
146
+ this.shadowDelegate = new Delegate(root);
147
+ this.shadowDelegate.on(
147
148
  "click",
148
149
  this.elements,
149
150
  this.handleClickEvent(eventData, root),
150
- true
151
+ true,
151
152
  );
152
153
  }
153
154
  }
154
155
  }
156
+
157
+ destroy() {
158
+ this.shadowDelegate?.off();
159
+ }
155
160
  }
156
161
 
157
162
  export default Tracking;