The Slower Way — Statically Import All the Things
Take a look at this example main entry file.
- Static Imports
- Generic Selectors
All the selectors in the example are valid ways to query the DOM; but, mixing arbitrary class, tag, and id selectors in this way tells us nothing regarding their function or context. Our styling is combined with our interactivity. And the selectors are so generic that we could easily cause one of the modules to initialize on a component that we did NOT intend.
To see why this is an issue check out the example code below; we're initializing the
foobar() module on a few different elements using a variety selectors.
Now imagine what would happen when we are asked to make updates to the markup and styles for
.callout? We may see that
.item has undesirable styles applied for the purposes of the update, so we remove it—now whatever
was going to do has been completely broken.
Maybe generic selectors are less of an issue for a solo developer who is familiar with the code base. But what about for a new Jr. Developer on your team who's been asked to make these changes? We’ve set them up for failure and made the task more likely to go over budget. In the long run, code organization issues like Static Imports and Generic Selectors make a project more difficult to debug, maintain, and work on as a team.
The Faster Way — Conditionally Import Some of the Things
Let's see how we can resolve these issues and improve our entry file.
The two major changes are:
- Dynamic Imports
- Data Attribute Selectors
We’ve also moved all of our modules into a separate
init.js file which exports a single function to only load the modules we need on-demand within an explicit scope, in this case the
document. (There’s another reason for moving this to a separate file that we’ll get to in a moment.)
Data Attribute Selectors
All (except one) of our selectors have been changed to
data- attributes. We've found this is the most helpful convention for understanding code long term, for both new and experienced developers alike.
Why leave the one
.intro selector as a class? With any convention comes at least some exceptions for the sake of simplicity—sometimes a class will always and only be for a single specific purpose that is associated with both a specific set of styles and functionality. Always remember, conventions exist to make our lives easier—if a convention ultimately feels "over-engineered" for a specific use case use your best judgment!
And finally, it’s incredibly easy to pass along additional data to our JS modules with additional data attributes.
This refactored approach using Dynamic Imports and Data Attribute Selectors immediately gives us a number of benefits for long-term, scalable maintenance.
Going a Little Further
This is all well and good when it comes to vanilla JS utility modules. But what if we need a more complex UI? When writing a traditional Multi-Page Application (MPA) we don’t necessarily have all the bells and whistles of a front-end UI framework such as React or Vue in all areas of the site.
We could implement a React or Vue runtime on top of our server-side rendered site to intercept user interaction, or we could serve HTML over the wire in specific areas of our pages with tools such as Livewire. But we like to use Svelte (almost) exclusively for creating dynamic and engaging front-end UIs
Svelte gives us the best of both worlds:
- Our front-end is lightweight and performant.
- And our developer experience is simple and flexible.
Dynamically Importing Svelte Components
Let’s apply what we've done above and create a Svelte adapter for our HTML. (We are aware that Svelte can be compiled into web components, however, we do not take this approach.)
In our template, we’re going to define a new, custom HTML element,
x-svelte and set its display to
This will give us a very explicit and clear hook in our markup for adding Svelte components while also allowing us to pass along properties and slots to our component. And by setting display: contents we can essentially ignore the x-svelte element in our document flow.
Let’s make a small addition to our
init.js file by adding to the list of modules:
We now have a dedicated Svelte file that will ONLY be loaded when a page has an
x-svelte element on the page.
There’s a lot happening in this file, however most of it simply allows us to pass along markup to our Svelte components’ slots and to pass along any data attributes as component properties. For the purposes of this article, we will not go into detail about this, but the general organizational approach is exactly the same as our original
Once in our Svelte component, we can use our slots and properties however we need.
Remember how we put all of our modules in a separate
src/js/init.js file earlier? Here’s why it is so helpful. There’s no guarantee (and in fact, it is extremely unlikely) that your Svelte components will be mounted and rendered before your other modules query their targets and are initialized. However, because we can now initialize these modules whenever we want for whatever scope we require, we can initialize any modules within our Svelte component quickly and dependably.
Let’s take our original
[data-example] module and use it in a Svelte component’s slot.