How to lean on the DOM to reduce JavaScript bloat and frameworrk addiction
February 08, 2021
No notes available for this episode.
Transcribed by Rugo Obi
Something I find myself needing to do in my no-framework framework is to ensure that my JavaScript download size is not too large.
An easy way to do this is to split up the JavaScript that you need on all pages from the JavaScript that you need on only some pages.
For example, I have some PayPal JavaScript that is relatively large, or in fact very large. I think it's a megabyte of dependencies, thanks to Paypal.com.
And I don't want this on every page of my website because that would slow it down a ton. I only want to pull this JavaScript in whenever I'm on that payment page.
All in all, I have three entry points to JavaScript. My general JavaScript in application.js
, payment.js
, which is this file here - all it does is just require
this PayPal file you were looking at a second ago - and then admin.js
with admin specific stuff. I think this is what-you-can-see-is-what-you-get editor and some Rail stuff.
Then, on the pages that need those particular JavaScript packs, I require
them.
So we're opening up the orders/edit
page and you can see here there's a bit of code, javaScript_pack_tag
, for the payment
file, that’s the payment.js
one. And you can assume there's an equivalent thing in my admin area layout pulling in the admin code, and that's how I separate out the JavaScript in the no-framework framework.
The next thing I'd like to demonstrate is how to pass a bunch of data from the backend to JavaScript without necessarily using API calls.
So, I'm still within the orders/edit
file right here, and I'm going to look for the bit for actually paying. So, "Click Below To Pay Securely", that looks about right, and then there's this div
tag generated via Rails, that's unimportant. What's important here is all the data attributes that are created.
There are five different data attributes: payment
, finalize_url
, order_number
, change_state_order_url
, and locale
.
This @payment
is an instance variable passed from the controller. Let's have a look at what that is.
I've opened up the orders_controller
and I'm in the edit
action here. If we go down nine lines, we see this payment
variable, and what's happening here is, it calls PayPalGateway.new
, passes in order
and then generates the payment_json
.
I'll go into that file briefly and take a look. It’s not on the path, I don't know why, and PaypalGateway
, yeah, here we go.
And this generates a big old block of JSON and then calls to JSON there to ensure there are no encoding issues.
You want to be careful to use good methods to transform your objects in your backend language to JSON, otherwise, there can be issues with single quotes, that kind of thing.
Anyway, I generate this glob of JSON in the controller here, and then pass it to the view
, via the data
attribute, and then this gets passed to my actual JavaScript here.
You can see on this line here, paymentDetails
, that I use the JSON.parse
functionality with paypalButton.dataset.payment
. So, paypalButton
grabs by ID of #paypal_button
, a particular element.
If we go into the edit
file again here, you can see there is the div
with the id paypal_button
, so they match.
And then there was this function, dataset
. This grabs all the data attributes, so there were five of them as you saw over in the edit
file here, and it grabs them all.
You'll also notice that I pass in URLs to my JavaScript world, that's because I like to keep all the knowledge about what particular URLs are within my backend codebase and then have the minimal amount of knowledge within the JavaScript side.
That's not fully possible because sometimes these URLs require parameters, and if I change the parameters they require, then the JavaScript world has to change. But at least if I modify what the URL points to, the code will still work in the client-side.
Another thing I like to do in my no-framework framework, is to define very simple reusable functions like get
that have no external dependencies.
Let's have a look at this particular function. I'm going to go to the definition here, yeah, and you can see that it just uses the raw API given in JavaScript for doing a HTTP request.
Then I have variants like getJSON
and so on, and they tend to bring these functions back and forth between codebase and keep improving them and so on and this is a basic library that I use to make writing code a bit faster.
Now I'd like to move on to another codebase, this one in PHP Laravel, but mostly in JavaScript, and show how the no-framework framework was applied here.
So, this has a bunch of tax calculators -about 10 of them, I can't remember- and you can see some of them in action here.
You choose a year, you choose your income, and then your particular county, then it gives you what your tax is and you can calculate it again.
There are a bunch of other ones, some of them are more complicated. I think the income tax one is fairly involved, but essentially they all have a similar kind of structure.
The main entry point for this particular codebase was the resources/js/init.js
file. You can see that at the bottom of the screen.
And the structure is similar to in the last video, in the sense that there's polyfills included first, then I include some regular JS files, like stripeIntegration
one here, and then I include a separate file for each particular calculator.
The architecture however differs to my previous codebase.
Let me show you how by going into one of these files. This one is for the church tax, that's the calculator we saw on screen a moment ago. And let's go down 11 lines and look at this key piece of architecture.
What I'm doing is attaching to the window
object and the browser, and a key called kirchenRechner
, and then that has a function called calculate
, which is mapped to a function in terms of this file, calculateKirchenSteuer
.
So, this does pollute the global namespace in the sense that if there's another piece of code that uses KirchenSteuer
as a name, then it will clash. But I find that to be very unlikely given that this is in German, that's quite a rare word, but this is a risk that this particular piece of code takes.
Perhaps a safer way to have done this would have been to name the app first, 'xyz', and then make this a property, a sub-property of 'xyz'.
However, and this brings me to a more general point, I tend to think the fear of namespace clashing is a little bit overwrought for small codebases. And the architectural complexity that I'd have to add in order to avoid that, didn't seem justified given that there were junior programmers in the team who mightn’t be able to understand it and so on.
So, therefore, I just went with the simplest thing that would work, and there were no issues. This isn't mission-critical code either, so I'm not particularly worried about an error case causing damage.
The next thing to look at is how this particular piece of functionality, i.e, the calculation of the tax, gets connected to the frontend.
So I'm going to search for that particular object and then you'll see that there are some view
files. For example, resources/views/calculator/kirchen.blade
, and then I have a form
, and then I simply attach to the onsubmit
attribute, KirchenRechner.calculate(event)
.
So, what is the flow of input to output within this particular function for doing the calculator action?
Let me scroll this to the top of the screen, and you can see there's just 22 lines here because it calls out to other functions which do the work. So it calls event.preventDefault
, it resetsResultAreas
.
What I mean by that is that there are divs
with the results of previous calculations, and it just resets them between each calculation. This is an external function because it's shared between all the calculators.
Then it grabs the form that this was called on from event.target
, and then there are two functions here but the more important one is the inner one. There’s a function I have called getInputFromForm
, and I pass it the form
variable.
Let's have a look at that particular function.
What this function does, as you can see in the documentation here, is it takes a HTML form as input and outputs a JavaScript object that has as its keys, the names of every form input and its values.
The key way it does that is using the form data API to create that object.
The advantage of this overall function is that I don't have to manually wire up each form to JavaScript, rather, this just happens automatically.
So let's find the form element here and look at the various types of input.
So we have a select
input here with name=tax_year
. This will become a property tax_year
within the results, and will have as its value, the option
that's selected.
And then for this next input, checkbox
, with the name income _time_frequency
, this will also appear as a key and value in the results. And even if the form differs or whatever, it will still work, and this is really nice because I don't have to do a lot of the boilerplate.
To put it in more abstract engineering terms, there is an absolute minimal amount of coupling between the varying HTML forms and my JavaScript, and as an engineering principle, I like that a lot.
Let me go through the rest of this now.
So we have this function here, doRawCalculation
that takes this userInput
object generated from the form data. And doRawCalculation
calls a third-party API, so it takes a while for the results to come back.
This is of course extremely common in the JavaScript world. Once the results come back, it makes a bit of a modification by moving the key. Once the results come back, it makes a modification by just pulling out the only results it needs -church_tax
- from the big glob of results.
This creates a more lean input and leads to less issues.
Then there's a function I have addMonthlyResults
, which basically translates yearly results into monthly results. Then I have localizeValues
into the German way of showing decimals, which is with a comma instead of with a decimal point like we do around here in the English-speaking world.
And down here, we have a final bit to displayResultsOnPage
, and if there's an error to .catch
, and then call the displayErrorOnPage
once that error is caught.
Let's take a look at the function definition of displayResultsOnPage
.
The main thing to look at here is assignValuesToElementsNamedByKeys
.
It takes a bunch of results
, in JavaScript objects I presume, and a prefix here.
I'm going to go into the definition. The documentation here describes it pretty well. So you can see some imaginary input that's passed to it, and what it does is finds the elements on page with ids: pension_output
.
pension
being the key here, and output
being the... not prefix -- but suffix, and then it puts a value inside that particular id. So it'll put '40' there, and '20' into insurance_output
, and so on.
It also gives some warnings in developer mode about when there are issues, such as an id not being found.
Big picture, why do I like this function?
Again, it's to do with coupling and division of resources in a team.
Because it simply requires simple HTML ids on the page, for example, pension_output
or insurance_output
, a fairly junior frontend programmer can just create the appropriate HTML based off of the JavaScript object, and then your calculator will work with that HTML without you having to do any more work.
Let me show you that in action real quick.
So we have the kirchen.blade
thing, and we have this church_tax_result
field, essentially it's just a span
, and the results of my calculation will get inserted in there when the results come in. It's as simple as that.
If there were 20 or 30 fields on the page, which there are for much more complicated types of tax, like income tax, then very little code is required to populate the entire page with results.
This is something else I really like.
Returning to the displayResultsOnPage
function, we see something else.
There's heavy use of a hide
and an unhide
function. This just manipulates the CSS and either shows certain large elements or hides them from view.
For example, when we're showing results on screen, we hide the calculator_input
, i.e, the form where the user inputs the information to be calculated, but we unhide the results
.
The inverse thing happens when we resetResultsAreas
or something like the inverse, it's a bit more complicated.
This code obviously isn't perfect, and it does not scale very far, but it's sufficient for the purposes required.
I don't believe in calling in a framework for every single codebase, sometimes things are more easily and more quickly done by going back to basics and I believe that this project was a case where this was justified.