Repository: vaadin/web-components
Pull request #9107: refactor: message-list dynamic markdown import
Author: login: tomivirkki
Labels: name: prerelease version for Vaadin 24.8.0
name: Released with Vaadin 24.8.0.alpha7
Created: 2025-05-09T12:58:12Z | Merged: 2025-05-12T08:32:01Z

## Description

Make `<vaadin-message-list>` dynamically import Markdown dependencies when `markdown` mode is switched on. This is to avoid the excess payload (of `marked` and `dompurify`) when the component is used in the default mode.

## Type of change

Refactor


Diff:
diff --git a/packages/markdown/theme/lumo/vaadin-markdown-styles.js b/packages/markdown/theme/lumo/vaadin-markdown-styles.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/markdown/theme/lumo/vaadin-markdown.js b/packages/markdown/theme/lumo/vaadin-markdown.js
index f3e579b8520..bd68ff2c60c 100644
--- a/packages/markdown/theme/lumo/vaadin-markdown.js
+++ b/packages/markdown/theme/lumo/vaadin-markdown.js
@@ -1 +1,2 @@
+import './vaadin-markdown-styles.js';
 import '../../src/vaadin-markdown.js';
diff --git a/packages/markdown/theme/material/vaadin-markdown-styles.js b/packages/markdown/theme/material/vaadin-markdown-styles.js
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/packages/markdown/theme/material/vaadin-markdown.js b/packages/markdown/theme/material/vaadin-markdown.js
index f3e579b8520..bd68ff2c60c 100644
--- a/packages/markdown/theme/material/vaadin-markdown.js
+++ b/packages/markdown/theme/material/vaadin-markdown.js
@@ -1 +1,2 @@
+import './vaadin-markdown-styles.js';
 import '../../src/vaadin-markdown.js';
diff --git a/packages/message-list/src/vaadin-message-list-mixin.js b/packages/message-list/src/vaadin-message-list-mixin.js
index c24061ee2ef..5134d99b722 100644
--- a/packages/message-list/src/vaadin-message-list-mixin.js
+++ b/packages/message-list/src/vaadin-message-list-mixin.js
@@ -3,7 +3,6 @@
  * Copyright (c) 2021 - 2025 Vaadin Ltd.
  * This program is available under Apache License Version 2.0, available at https://vaadin.com/license/
  */
-import '@vaadin/markdown/src/vaadin-markdown.js';
 import { html, render } from 'lit';
 import { ifDefined } from 'lit/directives/if-defined.js';
 import { KeyboardDirectionMixin } from '@vaadin/a11y-base/src/keyboard-direction-mixin.js';
@@ -98,12 +97,23 @@ export const MessageListMixin = (superClass) =>
     }
 
     /** @private */
-    __markdownChanged(_markdown) {
+    __markdownChanged(markdown) {
+      if (markdown && !customElements.get('vaadin-markdown')) {
+        // Dynamically import the markdown component
+        import('@vaadin/markdown/src/vaadin-markdown.js')
+          // Wait until the component is defined
+          .then(() => customElements.whenDefined('vaadin-markdown'))
+          // Render the messages again
+          .then(() => this._renderMessages(this.items));
+      }
       this._renderMessages(this.items);
     }
 
     /** @private */
     _renderMessages(items) {
+      // Check if markdown component is still loading
+      const loadingMarkdown = this.markdown && !customElements.get('vaadin-markdown');
+
       render(
         html`
           ${items.map(
@@ -118,6 +128,7 @@ export const MessageListMixin = (superClass) =>
                 theme="${ifDefined(item.theme)}"
                 class="${ifDefined(item.className)}"
                 @focusin="${this._onMessageFocusIn}"
+                style="${ifDefined(loadingMarkdown ? 'visibility: hidden' : undefined)}"
                 >${this.markdown
                   ? html`<vaadin-markdown .content=${item.text}></vaadin-markdown>`
                   : item.text}<vaadin-avatar slot="avatar"></vaadin-avatar
diff --git a/packages/message-list/test/message-list-markdown-import.test.js b/packages/message-list/test/message-list-markdown-import.test.js
new file mode 100644
index 00000000000..ea9547ccd3f
--- /dev/null
+++ b/packages/message-list/test/message-list-markdown-import.test.js
@@ -0,0 +1,39 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import '../src/vaadin-message-list.js';
+
+async function until(predicate) {
+  while (!predicate()) {
+    await new Promise((r) => {
+      setTimeout(r, 10);
+    });
+  }
+  return predicate();
+}
+
+describe('message-list-markdown dynamic import', () => {
+  let messageList;
+  const messages = [
+    {
+      text: 'This is a **bold text** in Markdown',
+      time: '10:00 AM',
+      userName: 'Markdown User',
+      userAbbr: 'MU',
+    },
+  ];
+
+  beforeEach(() => {
+    messageList = fixtureSync('<vaadin-message-list></vaadin-message-list>');
+  });
+
+  it('should not render incorrect content during import', async () => {
+    messageList.markdown = true;
+    messageList.items = messages;
+
+    // Expect the markdown to not exist in DOM as such
+    expect(messageList.textContent).to.not.include('**bold text**');
+
+    // Expect the markdown to be rendered as HTML eventually
+    await until(() => messageList.querySelector('vaadin-message strong')?.textContent === 'bold text');
+  });
+});
diff --git a/packages/message-list/test/message-list-markdown.test.js b/packages/message-list/test/message-list-markdown.test.js
index b1587541c0c..dd23bf39460 100644
--- a/packages/message-list/test/message-list-markdown.test.js
+++ b/packages/message-list/test/message-list-markdown.test.js
@@ -1,5 +1,5 @@
 import { expect } from '@vaadin/chai-plugins';
-import { fixtureSync, nextUpdate } from '@vaadin/testing-helpers';
+import { fixtureSync, nextFrame, nextUpdate } from '@vaadin/testing-helpers';
 import '../src/vaadin-message-list.js';
 
 describe('message-list-markdown', () => {
@@ -16,7 +16,8 @@ describe('message-list-markdown', () => {
   beforeEach(async () => {
     messageList = fixtureSync('<vaadin-message-list markdown></vaadin-message-list>');
     messageList.items = messages;
-    await nextUpdate(messageList);
+    await customElements.whenDefined('vaadin-markdown');
+    await nextFrame();
   });
 
   it('should render the message items as markdown', () => {
diff --git a/packages/message-list/test/visual/lumo/message-list.test.js b/packages/message-list/test/visual/lumo/message-list.test.js
index 46b5fa605a5..a908b8f4d5e 100644
--- a/packages/message-list/test/visual/lumo/message-list.test.js
+++ b/packages/message-list/test/visual/lumo/message-list.test.js
@@ -1,5 +1,5 @@
 import { sendKeys } from '@vaadin/test-runner-commands';
-import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
+import { fixtureSync, nextFrame, nextRender } from '@vaadin/testing-helpers';
 import { visualDiff } from '@web/test-runner-visual-regression';
 import '../../../theme/lumo/vaadin-message-list.js';
 
@@ -64,6 +64,8 @@ describe('message-list', () => {
           element.items[0].text = 'This is a **bold text** in Markdown';
           element.items = [...element.items];
           element.markdown = true;
+          await customElements.whenDefined('vaadin-markdown');
+          await nextFrame();
           await visualDiff(div, `${dir}-markdown`);
         });
       });
diff --git a/packages/message-list/test/visual/material/message-list.test.js b/packages/message-list/test/visual/material/message-list.test.js
index 5789b5ad2e6..979c354aa26 100644
--- a/packages/message-list/test/visual/material/message-list.test.js
+++ b/packages/message-list/test/visual/material/message-list.test.js
@@ -1,5 +1,5 @@
 import { sendKeys } from '@vaadin/test-runner-commands';
-import { fixtureSync, nextRender } from '@vaadin/testing-helpers';
+import { fixtureSync, nextFrame, nextRender } from '@vaadin/testing-helpers';
 import { visualDiff } from '@web/test-runner-visual-regression';
 import '../../../theme/material/vaadin-message-list.js';
 
@@ -64,6 +64,8 @@ describe('message-list', () => {
           element.items[0].text = 'This is a **bold text** in Markdown';
           element.items = [...element.items];
           element.markdown = true;
+          await customElements.whenDefined('vaadin-markdown');
+          await nextFrame();
           await visualDiff(div, `${dir}-markdown`);
         });
       });
diff --git a/packages/message-list/theme/lumo/vaadin-message-list.js b/packages/message-list/theme/lumo/vaadin-message-list.js
index 42b10a9050c..1c9f4111103 100644
--- a/packages/message-list/theme/lumo/vaadin-message-list.js
+++ b/packages/message-list/theme/lumo/vaadin-message-list.js
@@ -1,3 +1,3 @@
-import '@vaadin/markdown/theme/lumo/vaadin-markdown.js';
+import '@vaadin/markdown/theme/lumo/vaadin-markdown-styles.js';
 import './vaadin-message-list-styles.js';
 import '../../src/vaadin-message-list.js';
diff --git a/packages/message-list/theme/material/vaadin-message-list.js b/packages/message-list/theme/material/vaadin-message-list.js
index d2f65635b0a..ffeea275423 100644
--- a/packages/message-list/theme/material/vaadin-message-list.js
+++ b/packages/message-list/theme/material/vaadin-message-list.js
@@ -1,3 +1,3 @@
-import '@vaadin/markdown/theme/material/vaadin-markdown.js';
+import '@vaadin/markdown/theme/material/vaadin-markdown-styles.js';
 import './vaadin-message-list-styles.js';
 import '../../src/vaadin-message-list.js';


Reviews:
user: login: sissbruecker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T07:38:02Z
user: login: tomivirkki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T08:15:36Z
user: login: tomivirkki
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-05-12T08:16:22Z
user: login: sissbruecker
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T08:27:04Z
user: login: sissbruecker
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-12T08:31:47Z

Review comments:
diff_hunk: @@ -0,0 +1,39 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import '../src/vaadin-message-list.js';
+
+async function until(predicate) {
+  while (!predicate()) {
+    await new Promise((r) => {
+      setTimeout(r, 10);
+    });
+  }
+  return predicate();
+}
+
+describe('message-list-markdown dynamic import', () => {
+  let messageList;
+  const messages = [
+    {
+      text: 'This is a **bold text** in Markdown',
+      time: '10:00 AM',
+      userName: 'Markdown User',
+      userAbbr: 'MU',
+    },
+  ];
+
+  beforeEach(() => {
+    messageList = fixtureSync('<vaadin-message-list></vaadin-message-list>');
+  });
+
+  it('should not render incorrect content during import', async () => {
+    messageList.markdown = true;
+    messageList.items = messages;
+
+    // Expect the markdown to not exist in DOM as such
+    expect(messageList.textContent).to.not.include('**bold text**');
path: packages/message-list/test/message-list-markdown-import.test.js
user: login: sissbruecker
body: Could also test that the respective message is hidden?
created_at: 2025-05-12T07:37:10Z
author_association: CONTRIBUTOR
path: packages/markdown/theme/lumo/vaadin-markdown-styles.js
user: login: sissbruecker
body: I did not get why adding theme modules would be necessary?
created_at: 2025-05-12T07:37:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,39 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import '../src/vaadin-message-list.js';
+
+async function until(predicate) {
+  while (!predicate()) {
+    await new Promise((r) => {
+      setTimeout(r, 10);
+    });
+  }
+  return predicate();
+}
+
+describe('message-list-markdown dynamic import', () => {
+  let messageList;
+  const messages = [
+    {
+      text: 'This is a **bold text** in Markdown',
+      time: '10:00 AM',
+      userName: 'Markdown User',
+      userAbbr: 'MU',
+    },
+  ];
+
+  beforeEach(() => {
+    messageList = fixtureSync('<vaadin-message-list></vaadin-message-list>');
+  });
+
+  it('should not render incorrect content during import', async () => {
+    messageList.markdown = true;
+    messageList.items = messages;
+
+    // Expect the markdown to not exist in DOM as such
+    expect(messageList.textContent).to.not.include('**bold text**');
path: packages/message-list/test/message-list-markdown-import.test.js
user: login: tomivirkki
body: I was considering it but it kind of feels like testing a technical detail. Having the messages hidden at any point isn't exactly an expected state of the component, but rather a workaround. Hiding the host would be as correct as hiding the individual messages but it would conflict with the test. If something, a visual test could make sense here but timing it might become flaky (and effectively it would just be a white box).
created_at: 2025-05-12T08:15:36Z
author_association: MEMBER
path: packages/markdown/theme/lumo/vaadin-markdown-styles.js
user: login: tomivirkki
body: We can't (?) dynamically import the component with a specific theme, but instead we import the src version. For the Markdown to have the correct theme-specific styles (none currently thought), we need to have them (statically) imported, without actually importing the component.

I also considered an alternative where `<vaadin-markdown>` wouldn't be used as such by the `<vaadin-message-list>` but it would define a `<vaadin-message-list-markdown>` which would intrinsically have no styling at all. You can find the revision in the first commit of this PR.
created_at: 2025-05-12T08:16:22Z
author_association: MEMBER
diff_hunk: @@ -0,0 +1,39 @@
+import { expect } from '@vaadin/chai-plugins';
+import { fixtureSync } from '@vaadin/testing-helpers';
+import '../src/vaadin-message-list.js';
+
+async function until(predicate) {
+  while (!predicate()) {
+    await new Promise((r) => {
+      setTimeout(r, 10);
+    });
+  }
+  return predicate();
+}
+
+describe('message-list-markdown dynamic import', () => {
+  let messageList;
+  const messages = [
+    {
+      text: 'This is a **bold text** in Markdown',
+      time: '10:00 AM',
+      userName: 'Markdown User',
+      userAbbr: 'MU',
+    },
+  ];
+
+  beforeEach(() => {
+    messageList = fixtureSync('<vaadin-message-list></vaadin-message-list>');
+  });
+
+  it('should not render incorrect content during import', async () => {
+    messageList.markdown = true;
+    messageList.items = messages;
+
+    // Expect the markdown to not exist in DOM as such
+    expect(messageList.textContent).to.not.include('**bold text**');
path: packages/message-list/test/message-list-markdown-import.test.js
user: login: sissbruecker
body: OK, let's keep as is for now.
created_at: 2025-05-12T08:27:04Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-05-09T12:59:04Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=vaadin_web-components&pullRequest=9107) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=vaadin_web-components&pullRequest=9107&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=vaadin_web-components&pullRequest=9107&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=vaadin_web-components&pullRequest=9107&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=vaadin_web-components&pullRequest=9107&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=vaadin_web-components&pullRequest=9107&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=vaadin_web-components&pullRequest=9107)


author_association: NONE
user: login: vaadin-bot
created_at: 2025-05-14T17:57:11Z
body: This ticket/PR has been released with Vaadin 24.8.0.alpha7 and is also targeting the upcoming stable 24.8.0 version.
author_association: COLLABORATOR