I have a dialog element that is made up of HTML and styles obviously. I want to reuse this dialog across HTML pages. I think the best approach is to make the dialog an external HTML file with it’s own CSS. But I don’t know how to bring that HTML into my pages. Is there a way to do this?
Example:
// this
<dialog>
<span id="title">Title</span>
<div id="content"></div>
<button id="closeButton"></div>
</dialog>
<style>
// styles here
</style>
// or this:
<html>
<body>
<dialog>
<span id="title">Title</span>
<div id="content"></div>
<button id="closeButton"></div>
</dialog>
<style>
// styles here
</style>
</body>
</html>
I can use Javascript and I am using Typescript to compile to Javascript and Bun as a bundler but I’d rather rely on native methods if possible.
This question is not how to reuse HTML code across pages. It’s how to reuse a specific HTML Dialog element.
Update:
What I’m finding is recommendations to use Web Components and ES Modules developer.mozilla.org/en-US/docs/Web/API/Web_components.
The issue with that is that all the examples show HTML inline or adding each element line by line with JS.
My dialog is over 2000 lines of HTML and 2000 lines of CSS. That’s not going to work or be reusable if the HTML or CSS changes.
I could try and use fetch / xhr to pull in a HTML page and then read in the HTML and the CSS and see if that works. The CanIuse site says that ES modules are the replacement for HTML imports (see here). How? How does ES modules help?
I could also use an iframe and then get the dialog from that. The issue though is will the CSS and element names collide.
Update 2:
I added a template element to the page with my dialog element and then tried to show that dialog and all the styles were lost or not applied. I could be something I’m doing wrong.
Here’s the dialog:
<template id="dialogTemplate" mode="open">
<style>
#contentDialog {
position: absolute;
display: block;
width: 100%;
height: 100%;
background-color: rgba(255,255,255,1);
overflow: hidden;
background: transparent;
}
#abz {
position: absolute;
width: 100%;
height: 100%;
left: 0px;
top: 0px;
background-color: rgba(255,255,255,1);
border-radius: 8px 8px 0px 0px;
border: 1px solid rgba(112,112,112,1);
overflow: visible;
}
#acb {
fill: transparent;
stroke: rgba(112,112,112,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acb {
overflow: visible;
position: absolute;
height: 1px;
left: 0.5px;
right: 0px;
top: 24.5px;
transform: matrix(1,0,0,1,0,0);
border-top: 1px solid rgba(112,112,112,1);
}
#contentDialogTitle {
border: 0;
left: 8px;
top: 5px;
position: absolute;
overflow: visible;
white-space: nowrap;
text-align: left;
font-family: Helvetica Neue, Helvetica;
font-style: normal;
font-weight: normal;
font-size: 14px;
color: rgba(112,112,112,1);
}
#contentFrame {
border: 0;
box-sizing: border-box;
margin: 0;
padding: 0;
position: absolute;
height: 100%;
left: 0px;
right: 0px;
top: 25px;
bottom: 0px;
overflow: visible;
white-space: nowrap;
text-align: left;
font-family: Helvetica Neue, Helvetica;
font-style: normal;
font-weight: normal;
font-size: 14px;
color: rgba(112,112,112,1);
width: 100%;
}
#closeContentDialogButton {
position: absolute;
width: 18px;
height: 18px;
right: 5px;
top: 3px;
overflow: visible;
cursor: pointer;
}
#acc {
fill: rgba(147,147,147,1);
}
.acc {
position: absolute;
overflow: visible;
width: 18px;
height: 18px;
left: 0px;
top: 0px;
}
#acd {
border: 0;
left: 50%;
transform: translateX(-50%) translateY(-50%);
top: 50%;
position: absolute;
overflow: visible;
width: 9px;
white-space: nowrap;
text-align: center;
font-family: Helvetica Neue, Helvetica;
font-style: normal;
font-weight: bold;
font-size: 12px;
color: rgba(255,255,255,1);
user-select: none;
pointer-events: none;
}
#copyDialogURLtoClipboardIcon {
position: absolute;
width: 12.566px;
height: 16.149px;
right: 32.434px;
top: 4.925px;
overflow: visible;
cursor: pointer;
}
#ace {
opacity: 0;
fill: rgba(196,196,196,1);
}
.ace {
position: absolute;
overflow: visible;
width: 12.566px;
height: 16.149px;
left: 0px;
top: 0px;
}
#acf {
position: absolute;
width: 10.053px;
height: 13.238px;
left: 0px;
top: 0px;
overflow: visible;
}
#acg {
fill: rgba(247,247,247,1);
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acg {
overflow: visible;
position: absolute;
width: 10.052px;
height: 13.238px;
left: 0px;
top: 0px;
transform: matrix(1,0,0,1,0,0);
}
#ach {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.ach {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 3.351px;
transform: matrix(1,0,0,1,0,0);
}
#aci {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.aci {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 6.702px;
transform: matrix(1,0,0,1,0,0);
}
#acj {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acj {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 10.053px;
transform: matrix(1,0,0,1,0,0);
}
#ack {
position: absolute;
width: 10.053px;
height: 13.238px;
left: 1.759px;
top: 1.759px;
overflow: visible;
}
#acl {
fill: rgba(247,247,247,1);
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acl {
overflow: visible;
position: absolute;
width: 10.052px;
height: 13.238px;
left: 0px;
top: 0px;
transform: matrix(1,0,0,1,0,0);
}
#acm {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acm {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 3.351px;
transform: matrix(1,0,0,1,0,0);
}
#acn {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.acn {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 6.702px;
transform: matrix(1,0,0,1,0,0);
}
#aco {
fill: transparent;
stroke: rgba(196,196,196,1);
stroke-width: 1px;
stroke-linejoin: miter;
stroke-linecap: butt;
stroke-miterlimit: 4;
shape-rendering: auto;
}
.aco {
overflow: visible;
position: absolute;
width: 5.585px;
height: 1px;
left: 2.234px;
top: 10.053px;
transform: matrix(1,0,0,1,0,0);
}
#contentDialogMessage {
border: 0;
right: 54px;
top: 4px;
position: absolute;
overflow: visible;
white-space: nowrap;
text-align: right;
font-family: Helvetica Neue, Helvetica;
font-style: normal;
font-weight: normal;
font-size: 14px;
color: rgba(112,112,112,1);
}
</style>
<dialog id="contentDialog">
<div id="abz">
</div>
<hr class="acb" viewBox="0 0 449.5 1">
<path id="acb" d="M 0 0 L 449.5 0">
</path>
</hr>
<div id="contentDialogTitle">
<span>Title</span>
</div>
<iframe id="contentFrame"></iframe>
<div id="closeContentDialogButton">
<svg class="acc">
<rect id="acc" rx="9" ry="9" x="0" y="0" width="18" height="18">
</rect>
</svg>
<div id="acd">
<span>X</span>
</div>
</div>
<div id="copyDialogURLtoClipboardIcon">
<svg class="ace">
<rect id="ace" rx="1" ry="1" x="0" y="0" width="12.566" height="16.149">
</rect>
</svg>
<div id="acf">
<svg class="acg" viewBox="0 0 10.053 13.238">
<path id="acg" d="M 0 0 L 7.89845609664917 0 L 10.05258083343506 0 L 10.05258083343506 3.309466361999512 L 10.05258083343506 13.23786544799805 L 0 13.23786544799805 L 0 0 Z">
</path>
</svg>
<svg class="ach" viewBox="0 0 5.585 1">
<path id="ach" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
<svg class="aci" viewBox="0 0 5.585 1">
<path id="aci" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
<svg class="acj" viewBox="0 0 5.585 1">
<path id="acj" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
</div>
<div id="ack">
<svg class="acl" viewBox="0 0 10.053 13.238">
<path id="acl" d="M 0 0 L 7.89845609664917 0 L 10.05258083343506 0 L 10.05258083343506 3.309466361999512 L 10.05258083343506 13.23786544799805 L 0 13.23786544799805 L 0 0 Z">
</path>
</svg>
<svg class="acm" viewBox="0 0 5.585 1">
<path id="acm" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
<svg class="acn" viewBox="0 0 5.585 1">
<path id="acn" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
<svg class="aco" viewBox="0 0 5.585 1">
<path id="aco" d="M 0 0 L 5.58476734161377 0">
</path>
</svg>
</div>
</div>
<div id="contentDialogMessage">
<span></span>
</div>
</dialog>
</template>
I accessed the dialog:
var dialog = document.getElementById("dialogTemplate").content.querySelector("#contentDialog")
dialog.showModal(); // error not added to any document
document.body.append(document.getElementById("dialogTemplate").content.querySelector("#contentDialog")) // visible but no styles applied
dialog.showModal(); // visible but no styles applied
This isn’t an external HTML document but if templates can work then I can load in an external page or iframe content and do the same. Here is the guide I’m using. It says to clone the node to a shadow dom that is the web component class. I’m trying to do it manually on the document. I don’t know if shadow dom will work. More here.
Another option would be to assign the HTML to a export variable in Javascript and then assign it to the innerHTML property when needed.
// mypage.js
export const content = `multiline html using quotes`;
// mypage.ts or mypage.js
import * as mypage from "mypage.js"
element.innerHTML = myPage.content;
This would still be a hack and the page would always have to be wrapped in JavaScript quotes.
2
Answers
Here's what I came up with for displaying a dialog from an external HTML page using an HTML template (the code below also supports using an inline template). I'm using a template so I can have CSS and ids that I can prevent naming conflicts when no matter what page the dialog is on.
The issue with this is that sometimes the contentDocument or contentWindow of the iframe is null. So I'm guessing it's not loaded yet. I will probably try to use the iframe onload event to verify it's ready.
The other issue is that I have to hide the host div rather than the dialog. I tried creating a host element and using the body element as the host. Both seem to work but the escape key works with a host element by default but didn't seem to work with body as the host by default.
The final note is that showDialog() method uses a base class so some of the dialog showing code you will have to add yourself. It adds classes that display and center the dialog.
I did not create a web snippet.
So far it seems to work.
JS:
mypage.html:
common-dialog.html:
Since you don’t want to turn it into web component (and I get it, it’s pain to render everything bit by bit into DOM), I would suggest you to use Preact – it’s small and it’s JSX syntax (if you need one) is pretty similar to HTML. If you don’t want to compile JSX, you can take htm and wrap your HTML in template literal and render it with preact via
htm/preact
. This way you will get around 4kb of libraries on top of your huge component and will be able to render it anywhere anytime.Another way would be to write your own render function for
htm
, and use it without preact. That will cost you around 500bytes library code, and anything else will be your code.