What happened was, Brian and I were chatting about W3C GitHub issues and Brian mentioned how really long issues are annoying to search and read, because GitHub has this thing where if there are too many comments on an issue, it snips out the middle with a “Load more…” button that’s very tastefully designed and pretty easy to miss if you’re quick-scrolling to try to catch up. The squiggle-line would be a good marker, if it weren’t so tasteful as to blend into the background in a way that makes the Baby WCAG cry.
And what’s worse, from this perspective, is that if the issue has been discussed to a very particular kind of death, the “Load more…” button can have more “Load more…” buttons hiding within. So even if you know there was an interesting comment, and you remember a word or two of it, page-searching in your browser will do no good if the comment in question is buried one or more XMLHTTPRequest calls deep.
“I really wish GitHub had an ‘expand all comments’ button at the top or something,” Brian said (or words to that effect).
Well, it was a Friday afternoon and I was feeling code-hacky, so I wrote a bookmarklet. Here it is in easy-to-save hyperlink form:
It waits half a second after you activate it to find all the buttons on the page (in my test runs, usually six hundred of them). Then it looks through all the buttons to find the ones that have a textContent of “Load more…” and dispatches a click event to each one. With that done, it waits five seconds and does it all again, waits five seconds to do it again, and so on. Once it finds there are zero buttons with the “Load more…” textContent, it exits. And, if five seconds is too quick due to slow loading times, you can always invoke the bookmarklet again should you come across a “Load more…” button.
If you want this ability for yourself, just drag the link above into your bookmark toolbar or bookmarks menu, and whenever you load up a mega-thread GitHub issue, fire the bookmarklet to load all the comments. I imagine there may be cleaner ways to do this, but I was able to codeslam this in about 15 minutes using ViolentMonkey on live GitHub pages, and it does the thing.
I did consider complexifying the ViolentMonkey script so that any GitHub page is scanned for the “Load more…” button, and if one is present, then a “Load all comments” button is plopped into the top of the page, but I knew that would take at least another 15 minutes and my codeslam window was closing. Also, it would require anyone using it to run ViolentMonkey (or equivalent) all the time, whereas the bookmarlet has zero impact unless the user invokes it. If you want to extend this into something more than it is and share your solution with the world, by all means feel free.
The point of all this being, if you too wish GitHub had an easy way to load all the comments without you having to search for the “Load more…” button yourself, now there’s a bookmarklet made just for you. Enjoy!
For a while now, Web Components (which I’m not going to capitalize again, you’re welcome) have been one of those things that pop up in the general web conversation, seem intriguing, and then fade into the background again.
I freely admit a lot of this experience is due to me, who is not all that thrilled with the Shadow DOM in general and all the shenanigans required to cross from the Light Side to the Dark Side in particular. I like the Light DOM. It’s designed to work together pretty well. This whole high-fantasy-flavored Shadowlands of the DOM thing just doesn’t sit right with me.
If they do for you, that’s great! Rock on with your bad self. I say all this mostly to set the stage for why I only recently had a breakthrough using web components, and now I quite like them. But not the shadow kind. I’m talking about Fully Light-DOM Components here.
It started with a one-two punch: first, I read Jim Nielsen’s “Using Web Components on My Icon Galleries Websites”, which I didn’t really get the first few times I read it, but I could tell there was something new (to me) there. Very shortly thereafter, I saw Dave Rupert’s <fit-vids> CodePen, and that’s when the Light DOM Bulb went off in my head. You just take some normal HTML markup, wrap it with a custom element, and then write some JS to add capabilities which you can then style with regular CSS! Everything’s of the Light Side of the Web. No need to pierce the Vale of Shadows or whatever.
Kindly permit me to illustrate at great length and in some depth, using a thing I created while developing a tool for internal use at Igalia as the basis. Suppose you have some range inputs, just some happy little slider controls on your page, ready to change some values, like this:
The idea here is that you use the slider to change the font size of an element of some kind. Using HTML’s built-in attributes for range inputs, I set a minimum, maximum, and initial value, the step size permitted for value changes, and an ID so a <label> can be associated with it. Dirt-standard HTML stuff, in other words. Given that this markup exists in the page, then, it needs to be hooked up to the thing it’s supposed to change.
In Ye Olden Days, you’d need to write a function to go through the entire DOM looking for these controls (maybe you’d add a specific class to the ones you need to find), figure out how to associate them with the element they’re supposed to affect (a title, in this case), add listeners, and so on. It might go something like:
let sliders = document.querySelectorAll('input[id]');
for (i = 0; i < sliders.length; i++) {
let slider = sliders[i];
// …add event listeners
// …target element to control
// …set behaviors, maybe call external functions
// …etc., etc., etc.
}
Then you’d have to stuff all that into a window.onload observer or otherwise defer the script until the document is finished loading.
To be clear, you can absolutely still do it that way. Sometimes, it’s even the most sensible choice! But fully-light-DOM components can make a lot of this easier, more reusable, and robust. We can add some custom elements to the page and use those as a foundation for scripting advanced behavior.
Now, if you’re like me (and I know I am), you might think of converting everything into a completely bespoke element and then forcing all the things you want to do with it into its attributes, like this:
<super-slider type="range" min="0.5" max="4" step="0.1" value="2"
unit="em" target=".preview h1">
Title font size
</super-slider>
Don’t do this. If you do, then you end up having to reconstruct the HTML you want to exist out of the data you stuck on the custom element. As in, you have to read off the type, min, max, step, and value attributes of the <super-slider> element, then create an <input> element and add the attributes and their values you just read off <super-slider>, create a <label> and insert the <super-slider>’s text content into the label’s text content, and why? Why did I do this to myse — uh, I mean, why do this to yourself?
This is the pattern I got from <fit-vids>, and the moment that really broke down the barrier I’d had to understanding what makes web components so valuable. By taking this approach, you get everything HTML gives you with the <label> and <input> elements for free, and you can add things on top of it. It’s pure progressive enhancement.
To figure out how all this goes together, I found MDN’s page “Using custom elements” really quite valuable. That’s where I internalized the reality that instead of having to scrape the DOM for custom elements and then run through a loop, I could extend HTML itself:
class superSlider extends HTMLElement {
connectedCallback() {
//
// the magic happens here!
//
}
}
customElements.define("super-slider",superSlider);
What that last line does is tell the browser, “any <super-slider> element is of the superSlider JavaScript class”. Which means, any time the browser sees <super-slider>, it does the stuff that’s defined by class superSlider in the script. Which is the thing in the previous code block! So let’s talk about how it works, with concrete examples.
It’s the class structure that holds the real power. Inside there, connectedCallback() is invoked whenever a <super-slider> is connected; that is, whenever one is encountered in the page by the browser as it parses the markup, or when one is added to the page later on. It’s an auto-startup callback. (What’s a callback? I’ve never truly understood that, but it turns out I don’t have to!) So in there, I write something like:
connectedCallback() {
let targetEl = document.querySelector(this.getAttribute('target'));
let unit = this.getAttribute('unit');
let slider = this.querySelector('input[type="range"]');
}
So far, all I’ve done here is:
Used the value of the target attribute on <super-slider> to find the element that the range slider should affect using a CSS-esque query.
The unit attribute’s value to know what CSS unit I’ll be using later in the code.
Grabbed the range input itself by running a querySelector() within the <super-slider> element.
With all those things defined, I can add an event listener to the range input:
slider.addEventListener("input",(e) => {
let value = slider.value + unit;
targetEl.style.setProperty('font-size',value);
});
…and really, that’s it. Put all together:
class superSlider extends HTMLElement {
connectedCallback() {
let targetEl = document.querySelector(this.getAttribute('target'));
let unit = this.getAttribute('unit');
let slider = this.querySelector('input[type="range"]');
slider.addEventListener("input",(e) => {
targetEl.style.setProperty('font-size',slider.value + unit);
});
}
}
customElements.define("super-slider",superSlider);
<span>See the Pen <a href="https://codepen.io/meyerweb/pen/oNmXJRX">
WebCOLD 01</a> by Eric A. Meyer (<a href="https://codepen.io/meyerweb">@meyerweb</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
As I said earlier, you can get to essentially the same result by running document.querySelectorAll('super-slider') and then looping through the collection to find all the bits and bobs and add the event listeners and so on. In a sense, that’s what I’ve done above, except I didn’t have to do the scraping and looping and waiting until the document has loaded — using web components abstracts all of that away. I’m also registering all the components with the browser via customElements.define(), so there’s that too. Overall, somehow, it just feels cleaner.
One thing that sets customElements.define() apart from the collect-and-loop-after-page-load approach is that custom elements fire all that connection callback code on themselves whenever they’re added to the document, all nice and encapsulated. Imagine for a moment an application where custom elements are added well after page load, perhaps as the result of user input. No problem! There isn’t the need to repeat the collect-and-loop code, which would likely have to have special handling to figure out which are the new elements and which already existed. It’s incredibly handy and much easier to work with.
But that’s not all! Suppose we want to add a “reset” button — a control that lets you set the slider back to its starting value. Adding some code to the connectedCallback() can make that happen. There’s probably a bunch of different ways to do this, so what follows likely isn’t the most clever or re-usable way. It is, instead, the way that made sense to me at the time.
With that code added into the connection callback, a button gets added right after the slider, and it shows a little circle-arrow to convey the concept of resetting. You could just as easily make its text “Reset”. When said button is clicked or keyboard-activated ("click" handles both, it seems), the slider is reset to the stored initial value, and then an input event is fired at the slider so the target element’s style will also be updated. This is probably an ugly, ugly way to do this! I did it anyway.
<span>See the Pen <a href="https://codepen.io/meyerweb/pen/jOdPdyQ">
WebCOLD 02</a> by Eric A. Meyer (<a href="https://codepen.io/meyerweb">@meyerweb</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
Okay, so now that I can reset the value, maybe I’d also like to see what the value is, at any given moment in time? Say, by inserting a classed <span> right after the label and making its text content show the current combination of value and unit?
let label = this.querySelector('label');
let readout = document.createElement('span');
readout.classList.add('readout');
readout.textContent = slider.value + unit;
label.after(readout);
Plus, I’ll need to add the same text content update thing to the slider’s handling of input events:
I imagine I could have made this readout-updating thing a little more generic (less DRY, if you like) by creating some kind of getter/setter things on the JS class, which is totally possible to do, but that felt like a little much for this particular situation. Or I could have broken the readout update into its own function, either within the class or external to it, and passed in the readout and slider and reset value and unit to cause the update. That seems awfully clumsy, though. Maybe figuring out how to make the span a thing that observes slider changes and updates automatically? I dunno, just writing the same thing in two places seemed a lot easier, so that’s how I did it.
So, at this point, here’s the entirety of the script, with a CodePen example of the same thing immediately after.
class superSlider extends HTMLElement {
connectedCallback() {
let targetEl = document.querySelector(this.getAttribute("target"));
let unit = this.getAttribute("unit");
let slider = this.querySelector('input[type="range"]');
slider.addEventListener("input", (e) => {
targetEl.style.setProperty("font-size", slider.value + unit);
readout.textContent = slider.value + unit;
});
let reset = slider.getAttribute("value");
let resetter = document.createElement("button");
resetter.textContent = "↺";
resetter.setAttribute("title", reset + unit);
resetter.addEventListener("click", (e) => {
slider.value = reset;
slider.dispatchEvent(
new MouseEvent("input", { view: window, bubbles: false })
);
});
slider.after(resetter);
let label = this.querySelector("label");
let readout = document.createElement("span");
readout.classList.add("readout");
readout.textContent = slider.value + unit;
label.after(readout);
}
}
customElements.define("super-slider", superSlider);
<span>See the Pen <a href="https://codepen.io/meyerweb/pen/NWoGbWX">
WebCOLD 03</a> by Eric A. Meyer (<a href="https://codepen.io/meyerweb">@meyerweb</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
Anything you can imagine JS would let you do to the HTML and CSS, you can do in here. Add a class to the slider when it has a value other than its default value so you can style the reset button to fade in or be given a red outline, for example.
Or maybe do what I did, and add some structural-fix-up code. For example, suppose I were to write:
In that bit of markup, I left off the id on the <input> and the for on the <label>, which means they have no structural association with each other. (You should never do this, but sometimes it happens.) To handle this sort of failing, I threw some code into the connection callback to detect and fix those kinds of authoring errors, because why not? It goes a little something like this:
if (!label.getAttribute('for') && slider.getAttribute('id')) {
label.setAttribute('for',slider.getAttribute('id'));
}
if (label.getAttribute('for') && !slider.getAttribute('id')) {
slider.setAttribute('id',label.getAttribute('for'));
}
if (!label.getAttribute('for') && !slider.getAttribute('id')) {
let connector = label.textContent.replace(' ','_');
label.setAttribute('for',connector);
slider.setAttribute('id',connector);
}
Once more, this is probably the ugliest way to do this in JS, but also again, it works. Now I’m making sure labels and inputs have association even when the author forgot to explicitly define it, which I count as a win. If I were feeling particularly spicy, I’d have the code pop an alert chastising me for screwing up, so that I’d fix it instead of being a lazy author.
It also occurs to me, as I review this for publication, that I didn’t try to do anything in situations where both the for and id attributes are present, but their values don’t match. That feels like something I should auto-fix, since I can’t imagine a scenario where they would need to intentionally be different. It’s possible my imagination is lacking, of course.
So now, here’s all just-over-40 lines of the script that makes all this work, followed by a CodePen demonstrating it.
class superSlider extends HTMLElement {
connectedCallback() {
let targetEl = document.querySelector(this.getAttribute("target"));
let unit = this.getAttribute("unit");
let slider = this.querySelector('input[type="range"]');
slider.addEventListener("input", (e) => {
targetEl.style.setProperty("font-size", slider.value + unit);
readout.textContent = slider.value + unit;
});
let reset = slider.getAttribute("value");
let resetter = document.createElement("button");
resetter.textContent = "↺";
resetter.setAttribute("title", reset + unit);
resetter.addEventListener("click", (e) => {
slider.value = reset;
slider.dispatchEvent(
new MouseEvent("input", { view: window, bubbles: false })
);
});
slider.after(resetter);
let label = this.querySelector("label");
let readout = document.createElement("span");
readout.classList.add("readout");
readout.textContent = slider.value + unit;
label.after(readout);
if (!label.getAttribute("for") && slider.getAttribute("id")) {
label.setAttribute("for", slider.getAttribute("id"));
}
if (label.getAttribute("for") && !slider.getAttribute("id")) {
slider.setAttribute("id", label.getAttribute("for"));
}
if (!label.getAttribute("for") && !slider.getAttribute("id")) {
let connector = label.textContent.replace(" ", "_");
label.setAttribute("for", connector);
slider.setAttribute("id", connector);
}
}
}
customElements.define("super-slider", superSlider);
<span>See the Pen <a href="https://codepen.io/meyerweb/pen/PoVPbzK">
WebCOLD 04</a> by Eric A. Meyer (<a href="https://codepen.io/meyerweb">@meyerweb</a>)
on <a href="https://codepen.io">CodePen</a>.</span>
There are doubtless cleaner/more elegant/more clever ways to do pretty much everything I did above, considering I’m not much better than an experienced amateur when it comes to JavaScript. Don’t focus so much on the specifics of what I wrote, and more on the overall concepts at play.
I will say that I ended up using this custom element to affect more than just font sizes. In some places I wanted to alter margins; in others, the hue angle of colors. There are a couple of ways to do this. The first is what I did, which is to use a bunch of CSS variables and change their values. So the markup and relevant bits of the JS looked more like this:
I’ll leave the associated JS as an exercise for the reader. I can think of reasons to do either of those approaches.
But wait! There’s more! Not more in-depth JS coding (even though we could absolutely keep going, and in the tool I built, I absolutely did), but there are some things to talk about before wrapping up.
First, if you need to invoke the class’s constructor for whatever reason — I’m sure there are reasons, whatever they may be — you have to do it with a super() up top. Why? I don’t know. Why would you need to? I don’t know. If I read the intro to the super page correctly, I think it has something to do with class prototypes, but the rest went so far over my head the FAA issued a NOTAM. Apparently I didn’t do anything that depends on the constructor in this article, so I didn’t bother including it.
Second, basically all the JS I wrote in this article went into the connectedCallback() structure. This is only one of four built-in callbacks! The others are:
disconnectedCallback(), which is fired whenever a custom element of this type is removed from the page. This seems useful if you have things that can be added or subtracted dynamically, and you want to update other parts of the DOM when they’re subtracted.
adoptedCallback(), which is (to quote MDN) “called each time the element is moved to a new document.” I have
no idea what that means. I understand all the words; it’s just that particular combination of them that confuses me.
attributeChangedCallback(), which is fired when attributes of the custom element change. I thought about trying to use this for my super-sliders, but in the end, nothing I was doing made sense (to me) to bubble up to the custom element just to monitor and act upon. A use case that does suggest itself: if I allowed users to change the sizing unit, say from
em to
vh, I’d want to change other things, like the
min,
max,
step, and default
value attributes of the sliders. So, since I’d have to change the value of the
unit attribute anyway, it might make sense to use
attributeChangedCallback() to watch for that sort of thing and then take action. Maybe!
Third, I didn’t really talk about styling any of this. Well, because all of this stuff is in the Light DOM, I don’t have to worry about Shadow Walls or whatever, I can style everything the normal way. Here’s a part of the CSS I use in the CodePens, just to make things look a little nicer:
Hopefully that all makes sense, but if not, let me know in the comments and I’ll clarify.
A thing I didn’t do was use the :defined pseudo-class to style custom elements that are defined, or rather, to style those that are not defined. Remember the last line of the script, where customElements.define() is called to define the custom elements? Because they are defined that way, I could add some CSS like this:
super-slider:not(:defined) {
display: none;
}
In other words, if a <super-slider> for some reason isn’t defined, make it and everything inside it just… go away. Once it becomes defined, the selector will no longer match, and the display: none will be peeled away. You could use visibility or opacity instead of display; really, it’s up to you. Heck, you could tile red warning icons in the whole background of the custom element if it hasn’t been defined yet, just to drive the point home.
The beauty of all this is, you don’t have to mess with Shadow DOM selectors like ::part() or ::slotted(). You can just style elements the way you always style them, whether they’re built into HTML or special hyphenated elements you made up for your situation and then, like the Boiling Isles’ most powerful witch, called into being.
That said, there’s a “fourth” here, which is that Shadow DOM does offer one very powerful capability that fully Light DOM custom elements lack: the ability to create a structural template with <slot> elements, and then drop your Light-DOM elements into those slots. This slotting ability does make Shadowy web components a lot more robust and easier to share around, because as long as the slot names stay the same, the template can be changed without breaking anything. This is a level of robustness that the approach I explored above lacks, and it’s built in. It’s the one thing I actually do like about Shadow DOM.
It’s true that in a case like I’ve written about here, that’s not a huge issue: I was quickly building a web component for a single tool that I could re-use within the context of that tool. It works fine in that context. It isn’t portable, in the sense of being a thing I could turn into an npm package for others to use, or probably even share around my organization for other teams to use. But then, I only put 40-50 lines worth of coding into it, and was able to rapidly iterate to create something that met my needs perfectly. I’m a lot more inclined to take this approach in the future, when the need arises, which will be a very powerful addition to my web development toolbox.
I’d love to see the templating/slotting capabilities of Shadow DOM brought into the fully Light-DOM component world. Maybe that’s what Declarative Shadow DOM is? Or maybe not! My eyes still go cross-glazed whenever I try to read articles about Shadow DOM, almost like a trickster demon lurking in the shadows casts a Spell of Confusion at me.
So there you have it: a few thousand words on my journey through coming to understand and work with these fully-Light-DOM web components, otherwise known as custom elements. Now all they need is a catchy name, so we can draw more people to the Light Side of the Web. If you have any ideas, please drop ’em in the comments!
I’ve posted a followup to this post which you should read before you read this post, because you might decide there’s no need to read this one. If not, please note that what’s documented below was a hack to overcome a bug that was quickly fixed, in a part of CSS that wasn’t enabled in stable Firefox at the time I wrote the post. Thus, what follows isn’t really useful, and leaves more than one wrong impression. I apologize for this. For a more detailed breakdown of my errors, please see the followup post.
I’ve been doing some development recently on a tool that lets me quickly produce social-media banners for my work at Igalia. It started out using a vanilla JS script to snarfle up collections of HTML elements like all the range inputs, stick listeners and stuff on them, and then alter CSS variables when the inputs change. Then I had a conceptual breakthrough and refactored the entire thing to use fully light-DOM web components (FLDWCs), which let me rapidly and radically increase the tool’s capabilities, and I kind of love the FLDWCs even as I struggle to figure out the best practices.
With luck, I’ll write about all that soon, but for today, I wanted to share a little hack I developed to make Firefox a tiny bit more capable.
One of the things I do in the tool’s CSS is check to see if an element (represented here by a <div> for simplicity’s sake) has an image whose src attribute is a base64 string instead of a URI, and when it is, add some generated content. (It makes sense in context. Or at least it makes sense to me.) The CSS rule looks very much like this:
div:has(img[src*=";data64,"])::before {
[…generated content styles go here…]
}
This works fine in WebKit and Chromium. Firefox, at least as of the day I’m writing this, often fails to notice the change, which means the selector doesn’t match, even in the Nightly builds, and so the generated content isn’t generated. It has problems correlating DOM updates and :has(), is what it comes down to.
There is a way to prod it into awareness, though! What I found during my development was that if I clicked or tabbed into a contenteditable element, the :has() would suddenly match and the generated content would appear. The editable element didn’t even have to be a child of the div bearing the :has(), which seemed weird to me for no distinct reason, but it made me think that maybe any content editing would work.
I tried adding contenteditable to a nearby element and then immediately removing it via JS, and that didn’t work. But then I added a tiny delay to removing the contenteditable, and that worked! I feel like I might have seen a similar tactic proposed by someone on social media or a blog or something, but if so, I can’t find it now, so my apologies if I ganked your idea without attribution.
My one concern was that if I wasn’t careful, I might accidentally pick an element that was supposed to be editable, and then remove the editing state it’s supposed to have. Instead of doing detection of the attribute during selection, I asked myself, “Self, what’s an element that is assured to be present but almost certainly not ever set to be editable?”
Well, there will always be a root element. Usually that will be <html> but you never know, maybe it will be something else, what with web components and all that. Or you could be styling your RSS feed, which is in fact a thing one can do. At any rate, where I landed was to add the following right after the part of my script where I set an image’s src to use a base64 URI:
let ffHack = document.querySelector(':root');
ffHack.setAttribute('contenteditable','true');
setTimeout(function(){
ffHack.removeAttribute('contenteditable');
},7);
Literally all this does is grab the page’s root element, set it to be contenteditable, and then seven milliseconds later, remove the contenteditable. That’s about a millisecond less than the lifetime of a rendering frame at 120fps, so ideally, the browser won’t draw a frame where the root element is actually editable… or, if there is such a frame, it will be replaced by the next frame so quickly that the odds of accidentally editing the root are very, very, very small.
At the moment, I’m not doing any browser sniffing to figure out if the hack needs to be applied, so every browser gets to do this shuffle on Firefox’s behalf. Lazy, I suppose, but I’m going to wave my hands and intone “browsers are very fast now” while studiously ignoring all the inner voices complaining about inefficiency and inelegance. I feel like using this hack means it’s too late for all those concerns anyway.
I don’t know how many people out there will need to prod Firefox like this, but for however many there are, I hope this helps. And if you have an even better approach, please let us know in the comments!
Over the past few weeks, I’ve been writing a remake of/homage to a game I last played somewhere around 1990: Gravity Wars. I gave this personal project a few hours here and there each day, or more usually each evening, slowly resurrecting a little piece of my past and putting my own spin on it. You can check it out for yourself, if you like: Gravity Wars Redux.
There are a few things I changed from the version I played all those years ago, besides the change of colors. First of all, instead of taking turns, in my version the ships fire simultaneously, meaning a no-survivors tie is possible. Second, the shot preview paths weren’t part of the original game. Third, the “Gravimetric display” wasn’t a thing in the original game either. That and the shot previews are leftovers from my development testing that I decided to keep around, either because I thought they added something to the game or because I just liked them too much to dump.
In fact, most of the code in there is accreted leftovers from the experimentation and development process. Looking at it now, I can see all the things I should have done. It just got to a point, as with most coding projects, where I could add another scoop or two of spaghetti to the existing mound and ship it, or I could start over and try to make the lasagna properly this time. I decided to ship it.
I have to admit the point of all this wasn’t actually to recreate Gravity Wars, as much as I like the game and am glad to have a working copy of it. It was instead to teach myself about the canvas API, and also to try to get a handle on some JS features that have never quite made sense to me. I think I did okay on the both fronts, in my own idiosyncratic way.
At this point in a JS-related post, I usually throw in a disclaimer about being a JS newb whose code should never be inspected by anybody. That’s probably still true, but I know I’ve advanced quite a bit from where I was, which pleases me. I can see that not just in that the code I’m publishing today, which is convoluted and clumsy but still better than what I’ve written in the past. I can also tell I’ve gotten better simply because I can see better approaches, as a result of what I learned along the way.
This may be where I end the project, or I may go back and take another crack at making lasagna. We’ll see. The carrot for me in doing that is it would let me add some other fun features and useful improvements pretty cheaply, not to mention I could fix some things that aren’t quite what they should be (like the placement of planets and ships, which should be a little less random). The stick is of course having to rewrite code I already wrote. I mean, lasagna is nice and all, but I like spaghetti too.
Even though it turned out that there is no simple solution for the math problem I posted, I learned a fair amount from the fantastic responses—thanks, everyone!—and eventually came up with a solution that worked for me. (I’d like to say it was one of the iterative approaches posted, but none of them worked for me. In the end, I brute-forced it.) I’m hoping for a different outcome with my next question, which is about JavaScript.
Consider the following structure, which is a much-edited-down version of part of the HYDEsim code:
There are two things I’ve tried and failed to do. And tried, and tried, and tried; and failed, and failed, and failed.
Eliminate the redundant calculations of radii. Note how I define a radius property in each case? And then have to not use it when I create the overlay? It seems like there must be a way to just define the value once for each subsection and then use it as many times as needed within that context. How?
How do I make it so that all those properties and overlays and such automatically recalculate any time one of the “upper-level” terms changes? As in, once I’ve created a new Detonation object det, how can I set things up so declaring det.yield = 250; will trigger recalculation of all the other pieces of the object? At present, I’m just blowing away the existing det and creating a whole new one, which just seems clumsy and silly. I have to believe there’s a better way. I just can’t find it.
Please note: tossing off comments like “oh, just instantiate a mixin constructor with an extra closure” will be of no help at all, because I don’t understand what those terms mean. Hell, I’m not even sure I used the words “object” and “property” correctly in my explanation above. Similarly, pointing me to tutorials that use those terms liberally is unlikely to be of help, since the text will just confuse me. Sample code (whether posted or in a tutorial) will help a great deal, because it will give me something to poke and prod and dissect. That’s how I’ve always learned to program. Actually, it’s how I’ve always learned anything.
As well, I’m absolutely willing to believe that there are much, much better ways to structure the object, but right now I really need to learn how those two things are accomplished in the context of what I already have. Once I get familiar with those and finish up some other work, I can start thinking about more fundamental overhauls of the code (which needs it, but not now).
I really appreciate any concrete help you can give me.
Addendum: if you leave code in a comment, please just wrap it in a code element and use whatever indentation you like. The indentation won’t show up when the post goes up, but I’ll go in after and wrap the code in a pre and then everything will be fine. Sorry to those who’ve already gone to the effort of posting with indents or nbsp entities to try to preserve indentation! As soon as I can dig up the right preference panel or plugin to allow pre in comments, I’ll do that, but for now I’ll manually edit in the needed pres as comments are added. THanks, and again, apologies to those who posted before I made this clear!
A while back, I woke up one morning thinking, John Resig’s got some great CSS3 support in jQuery but it’s all forced into JS statements. I should ask him if he could set things up like Dean Edwards‘ IE7 script so that the JS scans the author’s CSS, finds the advanced selectors, does any necessary backend juggling, and makes CSS3 selector support Transparently Just Work. And then he could put that back into jQuery.
And then, after breakfast, I fired up my feed reader and saw Simon Willison‘s link to John Resig’s nascent Sizzle project.
I swear to Ged this is how it happened.
Personally, I can’t wait for Sizzle to be finished, because I’m absolutely going to use it and recommend its use far and wide. As far as I’m concerned, though, it’s a first step into a larger world.
Think about it: most of the browser development work these days seems to be going into JavaScript performance. Those engines are being overhauled and souped up and tuned and re-tuned to the point that performance is improving by orders of magnitude. Scanning the DOM tree and doing things to it, which used to be slow and difficult, is becoming lightning-fast and easy.
So why not write JS to implement multiple background-image support in all browsers? All that’s needed is to scan the CSS, find instances of multiple-image backgrounds, and then dynamically add divs, one per extra background image, to get the intended effect.
Just like that, you’ve used the browser’s JS to extend its CSS support. This approach advances standards support in browsers from the ground up, instead of waiting for the browser teams to do it for us.
I suspect that not quite everything in CSS3 will be amenable to this approach, but you might be surprised. Seems to me that you could do background sizing with some div-and-positioning tricks, and text-shadow could be supportable using a sIFR-like technique, though line breaks would be a bear to handle. RGBa and HSLa colors could be simulated with creative element reworking and opacity, and HSL itself could be (mostly?) supported in IE with HSL-to-RGB calculations. And so on.
There are two primary benefits here. The first is obvious: we can stop waiting around for browser makers to give us what we want, thanks to their efforts on JS engines, and start using the advanced CSS we’ve been hearing about for years. The second is that the process of finding out which parts of the spec work in the real world, and which fall down, will be greatly accelerated. If it turns out nobody uses (say) background-clip, even given its availability via a CSS/JS library, then that’s worth knowing.
What I wonder is whether the W3C could be convinced that two JavaScript libraries supporting a given CSS module would constitute “interoperable implementations”, and thus allow the specification to move forward on the process track. Or heck, what about considering a single library getting consistent support in two or more browsers as interoperable? There’s a chance here to jump-start the entire process, front to back.
It is true that browsers without JavaScript will not get the advanced CSS effects, but older browsers don’t get our current CSS, and we use it anyway. (Still older browsers don’t understand any CSS at all.) It’s the same problem we’ve always faced, and everyone will face it differently.
We don’t have to restrict this to CSS, either. As I showed with my href-anywhere demo, it’s possible to extend markup using JS. (No, not without breaking validation: you’d need a custom DTD for that. Hmmm.) So it would be possible to use JS to, say, add audio and video support to currently-available browsers, and even older browsers. All you’d have to do is convert the HTML5 element into HTML4 elements, dynamically writing out the needed attributes and so forth. It might not be a perfect 1:1 translation, but it would likely be serviceable—and would tear down some of the highest barriers to adoption.
There’s more to consider, as well: the ability to create our very own “standards”. Maybe you’ve always wanted a text-shake property, which jiggles the letters up and down randomly to look like the element got shaken up a bit. Call it -myCSS-text-shake or something else with a proper “vendor” prefix—we’re all vendors now, baby!—and go to town. Who knows? If a property or markup element or attribute suddenly takes off like wildfire, it might well make it into a specification. After all, the HTML 5 Working Group is now explicitly set up to prefer things which are implemented over things that are not. Perhaps the CSS Working Group would move in a similar direction, given a world where we were all experimenting with our own ideas and seeing the best ideas gain widespread adoption.
In the end, as I said in Chicago last week, the triumph of standards (specifically, the DOM standard) will permit us to push standards support forward now, and save some standards that are currently dying on the vine. All we have to do now is start pushing. Sizzle is a start. Who will take the next step, and the step after that?
In support of the still-to-be-finished proposal for allowing most HTML 5 elements to become hyperlinks, I’ve written a quick proof-of-concept demo for your perusal. Basically, it’s a page with some JavaScript that captures the whole document tree, looks for any elements with an href attribute, and then sprinkles some events on those elements in order to make them act like hyperlinks. There’s also some CSS that applies old-school link presentation to said elements (blue and underlined, baby!). I’m using href because it was the easiest thing to do.
I’m sure I could have written a more elegant script (and yes, I know, your favorite JS framework would done it in half the lines and seventeen times the page weight) and I suspect there are some things I’m missing. I’ll be interested to hear what those may be. Meanwhile, if you want to try out your own arbitrary-element linking, grab a copy of the demo and edit the markup to your heart’s content. Or you could suck out the JS and apply it to your own test pages. Your call.
The demo works fine in Firefox 2, Camino 1.5, Safari 2, and Opera 9.2. I didn’t test it in anything else. It may well fail spectacularly in every other browser known to man and dog. That’s not really an issue, though. The goal here is to have a working demonstration, not a universal solution. (The latter may come later.) It’s a handy way to show people how browsers should behave in an arbitrary-link world.
The one thing that didn’t go right is the status-bar URL handling when hovering over a linked element (other than an a element) that descends from another linked element. For some reason the descendant’s URL never shows up in the status bar. I’m sure there’s an easy fix. I regard this as a minor issue. [Update 7/23: this has been fixed thanks to Allwyn Fernandez.]
The biggest thing that’s missing is simulating “visited” styles on non-a elements; in this case, turning them purple. That would require mining the history and dynamically adding classes and, well, all kinds of stuff. I’m sure it’s possible. I’m also sure that I don’t have the time right now to figure out how to do it well. Besides, ship early, ship often, right?
As I said before, I’m very interested to know what people think of the demonstrated behavior and how it might be improved. And hey, if anyone wants to contribute improvements to the JS, I’ll do my best to keep up.