I'm not going to dive into implementation details. I'm going to talk about why we decided on this stack and go in-depth on the structure of the application code, bundle sizes, and choosing external dependencies.
We're distributing Mailcoach as a Composer package that you can install into any existing Laravel application. The Mailcoach interface doesn't have a vast amount of screens, but a fundamental design principle of the package is that it needs to be easily extensible by package users.
Alternatively, we would need to carry the burden of adding all sorts of abstractions to make our interface extensible. Laravel Nova proves this is possible, but it's hard to get right and a lot of work to maintain.
We want it to be dead easy for eager developers to tinker with the Mailcoach interface. By keeping the stack as small as possible, we impose the least amount of required knowledge as possible.
The file structure is pretty straightforward.
components/ ...util/ ...app.js
There's one entry point:
app.js, which imports modules from
components. Some examples are
datepicker.js. These get mounted using
A small dropdown example:
<div class="dropdown" data-dropdown> <button class="icon-button" data-dropdown-trigger> <i class="fas fa-ellipsis-v | dropdown-trigger-rotate"></i> </button> <ul class="dropdown-list dropdown-list-left | hidden" data-dropdown-list > <!-- Dropdown items --> </ul></div>
util contains all sorts of utility functions, which are used by the components. Examples include a
debounce function, a helper to trap focus (used in modals), and wrappers around DOM APIs like
app.js weighs in at about 45 KB (minified and gzipped). About 42 KB of this is spent on external libraries. The remaining 3 KB is the custom application code, as explained above.
We're picky about our dependencies. Every piece of third party code that ends up in our application bundle must meet the following criteria:
First, it solves a hard problem OR enables us to simplify parts of our code significantly.
Second, it's worth its cost in KBs. If a small problem requires a large dependency to solve, there's probably a better solution lurking around the corner.
Our heaviest dependencies are
flatpickr.js. Choices is used for autocompleted multi-selects to manage tags, and Flatpickr is a date picker to schedule a campaign. We'd have a hard time solving those interface problems better than a respected npm package, so deciding to pull these in was a natural choice.
Turbolinks makes the UI significantly more snappy. It also enables us to build datatables without providing separate AJAX endpoints. Turbolinks improves the user experience and simplifies our backend code.
We use morphdom to patch bits of HTML with fresh chunks from the server (server fetched partials). We're using morphdom instead of manually setting
innerHTML with the native DOM API because it enables us to animate transitions with CSS. Since it's a small library with a simple API and we could drop it without significantly degrading the user experience, it's allowed in the bundle.
Alpine wasn't available yet when we were knee-deep into building Mailcoach, but we might give it a spin in a future refactor or our next big project.
Fruits of a smaller stack
When building new screens, I often didn't even need to run
yarn watch unless I needed to write some custom script for the page. Going back to building things without spinning up a build process in the background was a breath of fresh air. It makes you realize how much complexity we've been shoving into our client-side code.