Introduction
Fluent APIs are a programming paradigm that is centered around how we develop modules, components, and functions/methods. The goal is to make our code more human readable. We do achieve this by using a more functional approach in our code. This doesn't mean that we're using only a functional paradigm. Let's see an example to get started with this process.
Example
Let's say we're going to create an anchor HTML element (<a>Link!</a>
), give it a couple classes, add a custom attribute, and give it text content. The code for that without some sort of a library would be a bit confusing.
// Create element
const newAnchor = document.createElement('a')
// Add classes
newAnchor.classList.add('btn')
newAnchor.classList.add('btn--blue')
newAnchor.classList.add('btn--large')
// Add attribute
newAnchor.setAttribute('href', 'https://some.location.com')
// Add text content
newAnchor.textContent = 'Click here for more info!'
This code will run doing everything that we expect it to do. The issue becomes more evident when we start making multiple elements like this. Imagine if we had even two more anchor elements that we were creating like this. Keeping track of names and added elements would become convoluted and much more error prone. This is where we can build out a Fluent API library to execute these types of repetitive tasks. What if, for example, our code looked like this instead:
const newAnchor = Html()
.create('a')
.addClasses(['btn', 'btn--blue', 'btn--large'])
.attribute('href', 'https://come.location.com')
.text('Click here for more info!')
So. Much. Better!
But these methods are all undefined! Good news! Not for long. Let's go make them.
Html
module
We're going to make a module that handles all of the business logic of rendering, manipulating, altering, or removing HTML elements through JS.
export default () => new Html()
class Html {
addClasses(classesToAdd = undefined) {
if (classesToAdd === undefined) {
return this.element.classList
} else if (typeof classesToAdd === 'string') {
this.element.classList.add(classesToAdd)
} else {
classesToAdd.forEach(classToAdd => this.element.classList.add(classToAdd))
}
return this
},
attribute(attribute, value = undefined) {
if (value === undefined) {
return this.element.getAttribute(attribute)
} else {
this.element.setAttribute(attribute, value)
}
return this
},
create(element) {
this.element = document.createElement(element)
return this
},
render() {
return this.element
},
text(textToAdd = undefined) {
if (textToAdd === undefined) {
return this.element.textContent
} else {
this.element.textContent = textToAdd
}
return this
}
}
The first thing to note about this modules methods are the return values. Each of them is returning this
by default. That behavior is what allows us to chain methods together the way we are. That's because each time we use one of those methods, we're getting the current instance of the Html
object that we created. This instance has all of those methods we're defining on it. The next thing to note is the value we're altering with each of these methods.
this.element
is going to always refer to the HTML element we are creating or otherwise referencing. That becomes a property of the larger object.
More testable
The other benefit that we get from creating an abstraction like this is that we can easily test this behavior since we're operating with an object. This is the benefit of JavaScript is that we can use OOP for organizing our code and FP for simplifying that code.
describe('create', () => {
test('creates a new HTMLElement', () => {
const underTest = Html().create('p')
expect(underTest.render() instanceof HTMLElement).toBeTruthy()
})
})
here we can see how we can pass assertions on our abstraction layer because our methods are now more deterministic. They either do or do not do what we expect.
Conclusion
The benefit of creating small modules that do specific things and do them well should be evident. This is just keeping to the Single Responsibility Principle. Writing them in this manner where we're able to chain these behaviors in a way that makes them more readable to us (and other devs) is what makes this so powerful. Writing code your computer can understand is easy. Writing code that people can understand is the art.