I’m going to explore some new challenges and options for Remake.js, but before I do, let me quickly talk about the overall structure of a Remake.js application (so far, there’s only been one, but it’s in production).
The main challenge that Remake.js helps a developer like me overcome is organizing data on the fly. When you’re building a web application, you often want to plan things ahead, but with Remake.js you can let that naturally evolve more than in other frameworks.
Why is that?
With some frameworks, you have to put data into components and then attach behaviors to them. It’s relatively easy to move things around, but it still takes some time to think through it.
With Remake.js, components arise naturally after you attach enough functionality and data to them, but they’re not set in stone. You can easily move the data or behavior to a new element and then start working from there.
Let’s take a quick look at what a component in Remake.js might look like:
<div class="page-data" data-switches="disableTimelineQuickTip(no-auto) pageColorBackgroundPicker selectButtonColorPicker" data-i-choices="hideStars hideMasthead" data-o-save="pageData" data-o-type="object" data-o-key-username="david" data-o-key-email="email@example.com" data-o-key-email-confirmed="" data-o-key-hide-stars="" data-o-key-hide-masthead="true" data-o-default-username="david" > </div>
This is the top-level component in a real application. I know this is a lot to take in, but without trying to understand it on your own, just read this:
- Keep track of whether a few components are turned on or not (including the timeline quick tip the page color background picker, and the select button background picker)
- Allow the user to select among multiple choices inside this element regarding whether stars are hidden or not and whether the masthead is hidden or not
- When data on this element is changed, save the data using a function called
- Attach data to this element in the form of an object
- Add 5 keys to the object that this element represents, including: username, email, whether the email is confirmed, whether stars are hidden or not, and whether the masthead is hidden or not
- If the user tries to change their username to an unsupported value, change it back to the default value of “david”
Now, if you want, you can move this data around simply by moving a couple of these attributes.
That’s the power of Remake.js. It’s easy to move things around / experiment / and attach powerful functionality with a single data attribute.
These are just a few of the very powerful data attributes that Remake.js has.
Now, the thing I want to talk about in this post is how to make where the data shows up more flexible.
First, here’s how it works most of the time:
<div data-o-type="object" data-o-key-name="David"></div>
That tells Remake.js that this DOM node contains information, and, when parsed, that information will look like this object:
Another way to store this information in the DOM is like this:
<div data-o-type="object" data-l-key-name>David</div>
data-l attribute is different from the
data-o attribute in that in points somewhere else, instead of its attribute value. The
l stands for location. In the above case, it’s using the default location: the innerText of the current element.
However, you can also specify another location, like this:
<div data-o-type="object" data-l-key-name=".name"> <div class="name">David</div> </div>
.name means: “search my child elements for a class
.name and, when you find it, get the innerText of that element.
So, remember when I told you components end up arising naturally in Remake.js from the way the data is organized?
Well, let’s go through one more example of a high-level component…
<div class="bundle" data-switches="bundleDetails(no-auto) bundleTimeline(no-auto) bundleLegend(no-auto) deleteBundleConfirmation(auto) bundleRevisionsChoice(auto, inlineEdit)" data-o-save-deep="singleBundle" data-i-choices="majorRevisionsCount minorRevisionsCount" data-o-type="object" data-o-key-bundle-id="abc123" data-o-key-bundle-price="300" data-o-key-major-revisions-count="2" data-o-key-major-revisions-hours="1" data-o-key-minor-revisions-count="2" data-o-key-minor-revisions-hours=".5" data-o-default-bundle-price="100" data-o-default-major-revisions-count="0" data-o-default-major-revisions-hours="1" data-o-default-minor-revisions-count="0" data-o-default-minor-revisions-hours="1" > </div>
This one is similar to the
.page-data one in that it contains a lot of information. However, it’s different because instead of global preferences, it’s storing data for the component.
Let’s go through what the attributes above are doing:
- Keep track of whether the following toggles are activated or deactivated: bundle details, bundle timeline, bundle legend, the delete confirmation, and a choice between which revision type to edit.
- When the data on or inside this element is changed, serialize all of the this element’s data AND nested data and save it using a function called
- Allow the user to select among multiple choices inside this element regarding whether the major revisions section is shown or not and whether the minor revisions section is shown or not.
- Parse the data on this element as an object
- Store the following keys inside the parsed object: bundleId, bundlePrice, majorRevisionsCount, majorRevisionsHours, minorRevisionsCount, and minorRevisionsHours.
- If there’s an invalid value for any of the above keys, set a default value for them.
And that’s it!
Now, earlier we mentioned the use of
data-l (location) keys that keep track of data, but store that data inside the DOM — like in the innerText — instead of as the attribute value.
How come we’re not doing that here?
Well, we were at first, but then it turned we needed to do some funky things, including:
- Summing the major and minor revisions count to get the total number of revisions
- Formatting the price so that if it’s above 1000, it gets a comma, like so: 1,000
- Showing the price in more than one place
data-l attributes, we had no way of modifying the data before it was displayed. The displayed data was simply the data and that’s it. There’s no in between.
In order to get around this problem, we store the values in regular
data-o attributes and then use
data-w (watch) attributes to detect changes and modify values whenever anything changes.
So, if the price changes on this
.bundle element, for example, a child nested inside the
.bundle element might have a
data-w (watch) attribute that looks like this:
This means: “Hey, if any of my parents data changes and that change happened to a key called
bundlePrice, then please call my function
formatPrice() will use the new value for
bundlePrice, format it like a currency, and — in this case — insert it into the element with the
data-w-key-bundle-price attribute on it.
Pretty neat, right?
For adding the revisions counts together, we do something similar, but attaching BOTH the
data-w-key-minor-revisions-count attributes to it and using a
sumRevisions looks at the top-level revisions counts and adds them together, using the sum as the value of the current element.
It’s easy once you get the hang of it.
So, now let’s move on to the main problem I want to address with this post: Theming.
So, to make a theme, we have a few options:
- (easiest) Simply remove the existing styles from whatever you want to theme, and then write new styles, using the same DOM structure.
- Do the same as step #1, but add a few new nodes to the DOM structure and hide others, so you can have an easier time styling things because they’re arranged exactly how you want them.
- Render a totally different DOM structure based on the theme.
The first option is nice, but it’s a little hard to work with because you might have to add some tricky CSS here or there because the structure of the elements doesn’t match how you would’ve built it if you were starting from scratch.
The second option is perfect, in my view, because it involves changing only a few things — most of the DOM structure is probably fine — and by duplicating things you’re not really causing too much of an issue. You’ll be namespacing the theme using a global class anyways, so it should be relatively easy to hide certain elements depending on which global class is present.
The third option is nice… but it’s a lot harder to maintain. Now, every time you want to change the structure of the original DOM, you have to think about whether you ALSO want to change the other themes’ structures — and this goes for EVERY theme. The problem in this case isn’t really going through every theme, although that’s certainly annoying. It’s that it’s hard to remember. When you’re in the moment, changing something, it’s really hard to remember that the code exists elsewhere and was just kind of duplicated.
Also, another problem with the third option is: what about the case where you don’t need to change the DOM structure? Do you still duplicate all the code? Or do you use a combination of option one AND option two? Things get complicated fast.
Okay, so, with that in mind, I chose option two. It feels a tiny bit hacky because there’ll be a lot of CSS and HTML overlapping each other, but as long as 90-100% of the CSS is namespaced and doesn’t affect the other themes, I think it should be fine and the easiest to work with long term.
So, now we’re talking about duplicating elements inside a parent component and then conditionally hiding or showing them.
Here’s the main question: How do we get the data these elements need to be inside of them?
Well, we just saw and used
data-w (watch) attributes for this and it was pretty easy.
All we’d need to do is attach a
data-w attribute to each of the elements that need its value and then everything should work fine.
I think, now that I’m here talking about it, that works pretty well, and I don’t mind it as a solution. However, I’d like to explore one other solution:
These are new attributes, not yet introduced into the code, and this post is meant to help me think through if they make sense at all.
The idea would be:
data-f(format) attributes would compute formatted values based on one or more other values. They’d be kind of like
data-wattributes, but have less power. While
data-wattributes can do anything,
data-fattributes’ purpose is only to provide an intermediate value.
data-c(copy) attributes would copy data from
data-fattributes into themselves. Their ONLY purpose would be to copy data — that’s it.
Now, I’m a bit skeptical about inventing new data attributes. The ones I have already do a lot and work well enough. I’d prefer to keep this framework from expanding further if possible. However, the question here is whether these new attributes would add enough value in terms of simplicity to be worth the extra intial confusion when learning the framework — and provide much needed helpers to more experienced users.
The #1 good thing about them is they simplify what most
data-w (watch) attributes are already doing into two explicit & simple steps.
data-w attributes aren’t doing anything crazy, they’re copying data or computing a new value from existing data and then copying that into themselves.
However, when you, as a developer, are looking at a
data-w attribute, you have no idea what it’s doing. It could be using data from the current component, from the global scope, or even from an API or other source.
When you look at the top level of a component though and you see a bunch of
data-f attributes, you would know that those computed values are being used inside the element somewhere. And nothing extra is happening. Also, you could pretty safely ignore all
data-c attributes. They’re just copying data into themselves — not modifying the DOM, switching things on or off, adding classes, or anything else JS can do. Even though you can, of course, establish some best practices around how your team uses
data-w attributes, it’s safer to just know what can and cannot be happening just by looking at the page.
Even if we add these new attributes, we’ll still need
data-w attributes for some more complex cases, but if they handle 90% of data copying/formatting cases, then I think they’ll end up making things clearer than they are now.
The other minor benefit (and I say minor because this framework certainly wasn’t designed with performance as the #1 goal) is that you won’t have duplicate
data-w attributes with duplicate the same functions inside them, which will each need to be called separately. You’ll compute the data inside the
data-f attribute AND THEN pass the intermediate value down into the component. So, it will only be computed once. That’s kind of nice.
So, how would it look?
Let’s use a simple example (first, using
<div data-o-type="object" data-o-key-price1="100" data-o-key-price2="200" > <div data-w-key-price1="sumPrices" data-w-key-price2="sumPrices" >300</div> </div>
In the above example, we’d store both prices at the top level, and then use
data-w attributes to see when either changed. When they did, we’d sum them and add them to the current element’s innerText.
Now, to rework this using
data-f (format) and
data-c (copy) attributes:
<div data-o-type="object" data-o-key-price1="100" data-o-key-price2="200" data-f-key-total-price="price1 price2 | sum" > <div data-c-key-total-price >300</div> </div>
This is pretty similar, however it’s kind of nice to see ALL the data that’s being passed down into the component, like the total price, upfront — instead of having to look inside the (possibly messy) component and hunt for the place where the total price is being computed.
The thing I don’t like the most is the syntax of the
data-f attribute. I like that it creates a new key, but I don’t like its value’s syntax.
I suppose there are other options, like these:
In this first option, we’d pass the keys directly to a function, which I guess feels more natural.
And in the second option we’d automatically generate a function to be called named after the new key… something like
computeTotalPrice(). The function name wouldn’t be specified directly in this case. That would be kind of a bummer because it would make what’s happening slightly less explicit.
Anyways… the other thing I don’t like is all the new attributes. They make sense to me, but I worry that introducing them adds an extra layer of complication. It’s nice for more advanced use cases and more experienced users, but for beginners, it’d probably be hard to understand why you’d these new attributes. And, if possible, it’d be great to only have a few attributes to learn to get started and be productive with this framework.
This latter point can be address by simply hiding these attributes from new users and only introducing them long after they’ve learned about the more powerful
I’m not sure if it’s worth it.
From my perspective, as a developer, I think I slightly prefer the new
data-c attributes. It’s really nice to be able to see ALL the data inside your component with just a single glance. Having deeply nested
data-w attributes doesn’t really provide this. However, as “extra” data that isn’t saved when the element is serialized, I’m not sure how much that data matters.
So, for now, let’s just say this syntax is my favorite for
data-f-key-total-price="sum(price1, price2)" and I think
data-c attributes are barely worthly of being included in the main library. I’m not sure when or if I’ll add them, but it was fun to think through them and figure out a new way of doing things.
Thanks for reading!