ππΈοΈ
7/16/2025 9:41AM
New digital garden theme :)
Start menu here...
Thanks for visiting my website! Or as I like to call it, my web desktop. This is a space that I play around with web technologies, share my thoughts and my half-baked projects. Feel free to explore by clicking around.
This is a continuation of my experiments using iframes as a modular way to build an app. You can start from the beginning here: Introduction Post.
This is what I’ll be building today: code, preview
This calendar will form the starting point of my productivity app. Now this app begins to toe the line of, why don’t you just use web components or one of the many many framework libraries? And if I were building something for my company or working with other developers… I would.
But I’m fond of this idea. And honestly, playing around with this approach, I like more and more. It keeps things modular without the complex overhead of a large JavaScript framework and doesn’t require any server-side processing. It’s also simple for the end-user to use as a building block.
Want to follow along? You are going to need some basic familiarity with JavaScript, html, css, you will need a code editor and have the files served from a webserver. Due to iframe restrictions, this won’t work locally.
So in my last post A basic framed* website… more experiments with disappearing iframes I showed a way to pull an iframe into a page and run any script tags that it contained. Some might have noticed that with anything complicated you’d probably run into scope issues. For instance, id
tags might clash. A call to document.querySelector
might return an unexpected element when it is run from the parent (because another element might be a match). So if I’m going to be serious using this, that’s going to need to be fixed.
Also, just some clean up. Script files are now in the script folder and styles are in the… you guessed it… styles folder. I put all the fragment pieces (or should I call them components… arg… naming things is hard) in a fragments folder. The colors I chose for the app come from catppuccin/catppuccin: πΈ Soothing pastel theme for the high-spirited! (github.com), the font stack is “Humanist” from Modern Font Stacks and the icons are from Bootstrap Icons Β· Official open source SVG icon library for Bootstrap (getbootstrap.com).
I rolled my own css taking advantage of css grids and flex-box. The main css file is only 65 lines long. Not too shabby. And the css in the ./fragments/_calendar.html
is an additional 20. Considering most website calendars are monsters of JavaScript and lots and lots of divs I’m quite happy.
The calendar I’m building is for letting the user navigate around and it boils down to a bunch of buttons and a couple of classes. The data-day
attribute helps keep the JavaScript clean and tidy so I don’t need to look at the button text to determine what day the button represents.
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<script src="/scripts/fragment.js" type="text/javascript"></script>
</head>
<body>
<section>
<div class="month_picker">
<b>
<span class="date_month"></span>
<span class="date_year"></span>
</b>
<button class="previous_month" type="button">Prev</button>
<button class="today_switch" type="button">Today</button>
<button class="next_month" type="button">Next</button>
</div>
<div class="weekday">
<div>Su</div>
<div>Mo</div>
<div>Tu</div>
<div>We</div>
<div>Th</div>
<div>Fr</div>
<div>Sa</div>
</div>
<div class="day_picker">
<button data-day="1" type="button">1</button>
<button data-day="2" type="button">2</button>
<button data-day="3" type="button">3</button>
<button data-day="4" type="button">4</button>
<button data-day="5" type="button">5</button>
<button data-day="6" type="button">6</button>
<button data-day="7" type="button">7</button>
<button data-day="8" type="button">8</button>
<button data-day="9" type="button">9</button>
<button data-day="10" type="button">10</button>
<button data-day="11" type="button">11</button>
<button data-day="12" type="button">12</button>
<button data-day="13" type="button">13</button>
<button data-day="14" type="button">14</button>
<button data-day="15" type="button">15</button>
<button data-day="16" type="button">16</button>
<button data-day="17" type="button">17</button>
<button data-day="18" type="button">18</button>
<button data-day="19" type="button">19</button>
<button data-day="20" type="button">20</button>
<button data-day="21" type="button">21</button>
<button data-day="22" type="button">22</button>
<button data-day="23" type="button">23</button>
<button data-day="24" type="button">24</button>
<button data-day="25" type="button">25</button>
<button data-day="26" type="button">26</button>
<button data-day="27" type="button">27</button>
<button data-day="28" type="button">28</button>
<button data-day="29" type="button">29</button>
<button data-day="30" type="button">30</button>
<button data-day="31" type="button">31</button>
</div>
</section>
</body>
</html>
The real workhorse here is the css. display: grid;
is where the magic happens.
.weekday, .day_picker {
display: grid;
grid-template-columns: repeat(7, 1fr);
text-align: center;
}
.month_picker {
text-align: center;
display: flex;
align-items: center;
width: 100%;
padding: 0.5em;
}
.month_picker > * {
flex: 1 1 auto;
text-decoration: none;
}
.day_picker button.hide-date {
display:none;
}
We have a couple of things JavaScript will take care of, showing the name of the month and the year. Highlighting today and what’s been selected (via classes) and most importantly hiding any extra days in a month and shifting over the day of the week the grid starts on (hint, css does this).
Next
of Prev
buttons the month shown should change accordinglyToday
button, the calendar should show the current month with today’s date selected.In all cases, the fragment should notify the parent about the interaction either that the date changed or the month did. It’s the parents' responsibility about what should happen because of that interaction. Perhaps it saves the information somewhere, perhaps it will notify a different fragment.
One thing that I need to fix is how to target just the element I’m interested in for these click events and nothing else. Which means I need to lock down the scope somehow. I’m envisioning a way to label an iframe, so that inside it I can use that as a selector and keep my JavaScript from being over eager. That sounds an awful lot like an id
attribute. Note: There is also an loading="lazy"
on the iframe which helps with performance.
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="description" content="a basic calendar and todo list app">
<title>Framed* Productivity App</title>
<script src="scripts/disappearingFrame.js" type="text/javascript"></script>
<link rel="stylesheet" href="./styles/style.css" />
</head>
<body>
<iframe aria-busy="true"
src="./fragments/_header.html"
loading="lazy"
onload="disappear(this)"></iframe>
<main>
<iframe aria-busy="true"
id="main_calendar"
src="./fragments/_calendar.html"
loading="lazy"
onload="disappear(this)"></iframe>
</main>
<footer aria-live="polite">
<iframe aria-busy="true"
src="./fragments/_nav.html"
loading="lazy"
onload="disappear(this)"></iframe>
</footer>
</body>
</html>
Here is the index.html. You can see the id="main_calendar"
on the middle iframe. Let’s write some code to grab it from inside. The ./fragments/_header.html
and ./fragments/_nav.html
are trivial so I won’t include the code here. If you’re interested they are up on glitch code.
There is a way to get the id element of the frame using window.frameElement
. You can actually get access to all the attributes that way. I’m going to alter the scripts/fragment.js
file a bit here. One, I’m going to make the IIFE asynchronous. Second, I’m going to add an event listener to see when everything is loaded. Then I’m going to get either the id of the frame or generate a random identifier. Lastly I’ll set it on every child of the iframe body with an attribute name of scope.
Why not use the id
? Because someone might put an id on an element and I don’t want to override that and they might have multiple children, thus we’d be giving id’s to multiple elements which is a basic no-no.
scripts/fragment.js
;(async () => {
// if we loaded this outside an iframe... bail.
if(window.self === window.top) {
window.location.replace("/");
}
})();
// when everything is loaded
// set up an identifier so we have something to scope off of.
window.addEventListener("DOMContentLoaded", (event) => {
let body = document.querySelector('body');
let uuid = window.frameElement.getAttribute('id') ?? self.crypto.randomUUID();
for (let child of body.children) {
child.setAttribute('data-scope', uuid);
}
});
and some changes to the disappear
function in scripts/disappearingFrame.js
to make sure our new script tag also has the data-scope
attribute
/* Copy the contents of the iframe into the DOM and execute scripts */
function disappear(frame) {
let body = (frame.contentDocument.body||frame.contentDocument);
let children = [...body.children];
let scope = body.children[0].getAttribute('data-scope');
for(let child of children) {
if(child.tagName === 'SCRIPT'){
let script = document.createElement("script");
script.setAttribute('data-scope', scope);
script.text = child.innerHTML;
document.head.appendChild( script ).parentNode.removeChild( script );
} else {
frame.before(child);
}
}
frame.remove();
}
When the iframe “disappears” I won’t have access to the window.frameElement. By copying this data-scope
attribute to every child I’ll have something to select off of. One other tweak. This works best if there is only one child with the data-scope
attribute. Otherwise if the fragment body has multiple children you’d need to start looping to cover all the elements. So I made the design decision that I would structure my fragment like so:
<!DOCTYPE html>
<html lang="en"><head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="robots" content="noindex, nofollow" />
<script src="/scripts/fragment.js" type="text/javascript"></script>
</head>
<body>
<style>
... my styles here
</style>
<section>
... my elements here
</section>
<script>
// * * * * *
// inside the iframe you can access attributes on the iframe element
// via the `window.frameElement`. Outside the iframe the attributes have
// been copied to the script tag `document.currentScript`
// * * * * *
;(async () => {
<!-- Executes only when copied into the parent DOM -->
if(window.top == window.self) {
const scope = document.currentScript.getAttribute('scope');
const fragment = document.querySelector(`x-fragment[scope='${scope}']`);
} // parent DOM code
})(); // end IIFE
</script>
</body>
</html>
The important thing is that it is one element that all the other DOM elements go within. Also, notice how the scope is grabbed. The script itself get’s tagged with the scope
so you can grab it using document.currentScript.getAttribute('scope')
. Then to be extra tricky I created a constant I called fragment that was set to document.querySelector(
section[scope='${scope}']);
. So now we can use fragment.querySelector
in our code and only get elements from this instance of the fragment. Fantastic!
The rest of the JavaScript code is a bit long for this post, so I’ll only call out a few items. But if you want me to go into more detail on the calendar functionality let me know below.
fragment.addEventListener("click", (event) => {
...
}); // end click event handlers
If you dispatch events with the scope…
document.dispatchEvent(new CustomEvent('calendar-date-selected', {
detail: {
date: event.target.getAttribute("data-date"),
scope: scope
}
}));
The parent can listen for it and react…
document.addEventListener('calendar-date-selected', function (event) {
console.log(event.detail.date, event.detail.scope);
// do something cool
});
If you create the event listener with the scope you can make sure the right fragment (in the case of multiples) get’s the right event. I’m using this to put the dots on the calendar.
/fragments/_calendar.html
// listen for custom event to render the dots on the calendar
document.addEventListener(`calendar-render-dots-${scope}`, function (event) {
// reset the buttons
let dayBtns = fragment.querySelectorAll('.day_picker button');
for (let btn of dayBtns) {
btn.innerHTML = `${btn.getAttribute("data-day")}<br/>`;
}
// add in the dots
for(let detail of event.detail) {
var date = new Date(parseInt(detail.date));
let btn = fragment.querySelector(`[data-day='${date.getDate()}']`);
if(btn.getAttribute("data-date") == detail.date)
{
btn.innerHTML += `<span style="color: ${detail.color}">β’</span> `;
}
}
});
index.html
<script>
/*
** calendar events
*/
document.addEventListener('calendar-date-selected', function (event) {
console.log('calendar-date-selected', event.detail.date, event.detail.scope);
});
document.addEventListener('calendar-ready', renderDots);
document.addEventListener('calendar-month-changed', renderDots);
function renderDots(event) {
if(event.detail.scope == "main_calendar"){
document.dispatchEvent(new CustomEvent(`calendar-render-dots-${event.detail.scope}`, {
detail: [{date: 1688875200000, color: 'yellow' },
{date: 1689480000000, color: 'green'},
{date: 1688616000000, color: 'aqua'}]
}));
}
}
</script>
It’s not perfect but we can get most of the way there. A good rule of thumb is to only have essential styles in the fragment. No need to make the buttons fancy, the parent can handle that. Once again we are going to lean on the disappear
function to help us out.
I’ll add a check for a style tag. We can get a list of all the styles by leaning on the browser. We can create a temporary document and a copy of the style tag. By appending to this temporary document, the browser will parse it. Then we can use the sheet.cssRules
that lists all the style elements. Basically I loop through and split the selector on a ,
and then for each selector prepend [data-scope]=scope
. Once I have all the changes, I replace the contents of the style tag with my changes and then append it.
/* Copy the contents of the iframe into the DOM and execute scripts */
function disappear(frame) {
let body = (frame.contentDocument.body||frame.contentDocument);
let children = [...body.children];
let scope = body.children[0].getAttribute('data-scope');
for(let child of children) {
if(child.tagName === 'SCRIPT'){
let script = document.createElement("script");
script.setAttribute('data-scope', scope);
script.text = child.innerHTML;
document.head.appendChild( script ).parentNode.removeChild( script );
}
if(child.tagName === 'STYLE'){
let doc = document.implementation.createHTMLDocument("");
let styleElement = document.createElement("style");
styleElement.textContent = child.innerHTML;
doc.body.appendChild(styleElement); // to compute the css rules
child.innerHTML = '';
// scope out the styles
for(var style of styleElement.sheet.cssRules) {
let selectors = style.selectorText;
let scoped = [];
for(var selector of selectors.split(',')){
scoped.push(`[data-scope='${scope}'] ${selector}`);
}
child.innerHTML += style.cssText.replace(selectors, scoped.join(', ')) + ' ';
}
frame.before(child);
}
else {
frame.before(child);
}
}
frame.remove();
}
This method is not perfect but covers most of the css selector scenarios. Not bad for about 16 lines.
The disappearing trick has graduated and now has a way to add scope to both the JavaScript and CSS contained within fragments. So we can use multiple of the same fragments on the same page. Also, with custom events we can interact with the parent page.
The best part is, with two small script files and the html file anyone can take the calendar I built and add it to their website. code, preview
The next step is to add an event list to our calendar. This will demonstrate how two fragments can be used together for more complex interactions. But first, I’m going on vacation So stay tuned for the next post in two weeks.
I have a lot of interests and this place is a catch all. If you prefer to only follow a couple topics then check out my feeds page for category specific RSS feeds.