REST allows client functionality to be extended by downloading and executing code in the form of applets or scripts. This simplifies clients by reducing the number of features required to be pre-implemented.
Is Scripting Allowed?
A common criticism of the web is that it’s being misused. There is a narrative that WWW was created as a delivery system for “documents”, and only came to be used for “applications” by way of an accident or bizarre circumstances.
However, the concept of hypermedia challenges the split of document and application. Hypermedia systems like HyperCard, which preceded the web, featured rich capabilities for active and interactive experiences, including scripting.
HTML, as specified and implemented, does lack affordances needed to build highly interactive applications. This doesn’t mean, however, that hypermedia’s purpose is “documents” over “applications.”
Scripting has been a massive force multiplier for the web. Using scripting, web application developers are not only able to enhance their HTML websites, but also create full-fledged client-side applications that can often compete with native, thick client applications.
Scripting for Hypermedia
Borrowing from Roy Fielding’s notion of “constraints” defining REST, we offer two constraints of hypermedia-friendly scripting. You are scripting in an HDA-compatible manner if the following two constraints are adhered to:
- The main data format exchanged between server and client must be hypermedia, the same as it would be without scripting.
- Client-side state, outside the DOM itself, is kept to a minimum.
The goal of these constraints is to confine scripting to where it shines best and where nothing else comes close: interaction design. Business logic and presentation logic are the responsibility of the server, where we can pick whichever languages or tools are appropriate for our business domain.
The Hypermedia-Driven Application cannot as comfortably fall back on this tradition. This chapter is our contribution to the development of a new style and best practices for what we are calling Hypermedia-Driven Applications.
Unfortunately, simply listing “best practices” is rarely convincing or edifying. To be honest, it’s boring.
Instead, we will demonstrate these best practices by implementing client-side features in Contact.app. To cover different aspects of hypermedia-friendly scripting, we will implement three different features:
- An overflow menu to hold the Edit, View and Delete actions, to clean up visual clutter in our list of contacts.
- An improved interface for bulk deletion.
- A keyboard shortcut for focusing the search box.
The important takeaway in the implementation of each of these features is that, while they are implemented entirely on the client-side using scripting, they don’t exchange information with the server via a non-hypermedia format, such as JSON, and that they don’t store a significant amount of state outside of the DOM itself.
Scripting Tools for the Web
We will instead focus on three client-side scripting technologies that are hypermedia-friendly:
Let’s take a quick look at each of these scripting options, so we know what we are dealing with.
Note that, as with CSS, we are going to show you just enough of each of these options to give a flavor of how they work and, we hope, spark your interest in looking into any of them more extensively.
No code is faster than no code.
A quote from the website http://vanilla-js.com, which is well worth visiting even though it’s slightly out of date, captures the situation well:
VanillaJS is the lowest-overhead, most comprehensive framework I’ve ever used.
- Client-side routing
- An abstraction over DOM manipulation (i.e., templates that automatically update when referenced variables change)
- Server side rendering 
- Attaching dynamic behavior to server-rendered tags on load (i.e., “hydration”)
- Network requests
One of the best things about VanillaJS is how you install it: you don’t have to!
- Being as established as it is, it has accreted a lot of features and warts.
- It has a complicated and confusing set of features for working with asynchronous code.
- Working with events is surprisingly difficult.
- DOM APIs (a large portion of which were originally designed for Java, yes Java) are verbose and don’t have a habit of making common functionality easy to use.
A Simple Counter
Our counter widget will be very simple: it will have a number, shown as text, and a button that increments the number.
An inline implementation
HTML. When the button is clicked, we will look up the
output element holding the number, and increment the number
contained within it.
Not too bad.
It’s not the most beautiful code, and can be irritating especially if you aren’t used to the DOM APIs.
It’s a little annoying that we needed to add an
id to the
output element. The
is a bit verbose compared with, say, the
$ function, as provided by jQuery.
So that’s the simple, inline approach with VanillaJS.
Separating our scripting out
While the inline implementation is simple in some sense, a more standard way to write this code would be to move the code
<script src> tag or
placed into an inline
<script> tag by a build process.
For quite some time, this Separation of Concerns was considered the “orthodox” way to build web applications.
A stated goal of Separation of Concerns is that we should be able to modify and evolve each concern independently, with confidence that we won’t break any of the other concerns.
- Code reuse is difficult.
- The code ends up wildly disorganized and “flat”, with lots of unrelated event handlers mixed together.
So, you can see that the notion of Separation of Concerns doesn’t always work out as well as promised: our concerns end up intertwined or coupled pretty deeply, even when we separate them into different files.
To show that it isn’t just naming between concerns that can get you into trouble, consider another small change to our HTML
that demonstrates the problems with our separation of concerns: imagine that we decide to change the number field from
<output> tag to an
The fix for this issue is simple enough (we would need to change the
.textContent property to
.value property), but
it demonstrates the burden of synchronizing markup changes and code changes across multiple files. Keeping everything
in sync can become increasingly difficult as your application size increases.
In Contact.app we are not concerned with “structure”, “styling” or “behavior”; we are concerned with collecting contact info and presenting it to users. SoC, in the way it’s formulated in web development orthodoxy, is not really an inviolate architectural guideline, but rather a stylistic choice that, as we can see, can even become a hindrance.
It turns out that there is a burgeoning reaction against the Separation of Concerns design principle. Consider the following web technologies and techniques:
- Single-File Components
- Filesystem based routing
Each of these technologies colocate code in various languages that address a single feature (typically a UI widget).
All of them mix implementation concerns together in order to present a unified abstraction to the end-user. Separating technical detail concerns just isn’t as much of an, ahem, concern.
In a Hypermedia-Driven Application, we feel that the Locality of Behaviour design principle is often more important than the more traditional Separation of Concerns design principle.
What to do with our counter?
So, should we go back to the
onclick attribute way of doing things? That approach certainly wins in Locality of
Behavior, and has the additional benefit that it is baked into HTML.
Unfortunately, however, the
- They don’t support custom events.
- There is no good mechanism for associating long-lasting variables with an element — all variables are discarded when an event listener completes executing.
- If you have multiple instances of an element, you will need to repeat the listener code on each, or use something more clever like event delegation.
- An element cannot listen for events on another element.
Consider this common situation: you have a popup, and you want it to be dismissed when a user clicks outside of it. The listener will need to be on the body element in this situation, far away from the actual popup markup. This means that the body element would need to have listeners attached to it that deal with many unrelated components. Some of these components may not even be on the page when it was first rendered, if they are added dynamically after the initial HTML page is rendered.
The situation is not hopeless, however: it’s important to understand that LoB does not require behavior to be implemented at a use site, but merely invoked there. That is, we don’t need to write all our code on a given element, we just need to make it clear that a given element is invoking some code, which can be located elsewhere.
We won’t reproduce all the RSJS guidelines here, but here are the ones most relevant for our counter widget:
- “One component per file” - the name of the file should match the data attribute so that it can be found easily, a win for LoB
that is, HTML attributes that begin with
data-, a standard feature of HTML, to indicate that our HTML is a counter
as the root element in our counter component and wires in the appropriate event handlers and logic. Additionally, let’s
rework the code to use
querySelectorAll() and add the counter functionality to all counter components found on the
page. (You never know how many counter’s you might want!)
Here is what our code looks like now:
Using RSJS solves, or at least alleviates, many of the problems we pointed out with our first, unstructured example of VanillaJS being split out to a separate file:
- The JS that attaches behavior to a given element is clear (though only through naming conventions).
- Reuse is easy — you can create another counter component on the page and it will just work.
- The code is well-organized — one behavior per file.
VanillaJS in Action: An Overflow Menu
Our homepage has “Edit”, “View” and “Delete” links for every contact in our table. This uses a lot of space and creates visual clutter. Let’s fix that by placing these actions inside a drop-down menu with a button to open it.
Let’s begin by sketching the markup we want for our dropdown menu. First, we need an element, we’ll use a
<div>, to enclose the
entire widget and mark it as a menu component. Within this div, we will have a standard
<button> that will function
as the mechanism that shows and hides our menu items. Finally, we’ll have another
<div> that holds the menu items
that we are going to show.
These menu items will be simple anchor tags, as they are in the current contacts table.
Here is what our updated, RSJS-structured HTML looks like:
The roles and ARIA attributes are based on the Menu and Menu Button patterns from the ARIA Authoring Practices Guide.
With this ARIA introduction out the way, let’s return to our VanillaJS drop down menu. We’ll begin with the RSJS boilerplate: query for all elements with some data attribute, iterate over them, get any relevant descendants.
Note that, below, we’ve modified the RSJS boilerplate a bit to integrate with htmx, in particular we load the overflow menu when htmx loads new content.
However, this approach has some drawback:
- We would need to keep the DOM in sync with the state (harder without a framework)
Instead of taking this approach, we will use the DOM to store our state. We’ll lean on the
hidden attribute on the
menu element to tell us it’s closed. If the HTML of the page is snapshotted and restored, the menu can be restored as
well by simply re-running the JS.
We’ll also make the menu items non-tabbable, so we can manage their focus ourselves.
Let’s also make the menu close when we click outside it, a nice behavior that mimics how native drop-down menus work. This will require an event listener on the whole window.
Note that we need to be careful with this kind of listener: you may find that listeners accumulate as components add listeners and fail to remove them when the component is removed from the DOM. This, unfortunately, leads to difficult to track down memory leaks.
MutationObserver API. A
MutationObserver is very useful, but the API is quite heavy and a bit arcane, so we
won’t be using it for our example.
Instead, we will use a simple pattern to avoid leaking event listeners: when our event listener runs, we will check if the attaching component is still in the DOM, and, if the element is no longer in the DOM, we will remove the listener and exit.
This is a somewhat hacky, manual form of garbage collection. As is (usually) the case with other garbage collection algorithms, our strategy removes listeners in a nondeterministic amount of time after they are no longer needed. Fortunately for us, With a frequent event like “the user clicks anywhere in the page” driving the collection, it should work well enough for our system.
Now, let’s move on to the keyboard interactions for our dropdown menu. The keyboard handlers turn out to all be pretty similar to one another and not particularly intricate, so let’s knock them all out in one go:
That should cover all our bases, and we’ll admit that that’s a lot of code. But, in fairness, it’s code that encodes a lot of behavior.
Now, our drop-down menu isn’t perfect, and it doesn’t handle a lot of things. For example, we don’t support submenus,
or menu items being added or removed dynamically to the menu. If we needed more menu features like this, it might make
more sense to use an off-the-shelf library such as, GitHub’s
But, for our relatively simple use case, this library does a fine job, and we got to explore ARIA and RSJS while implementing it.
much further than
Installing Alpine is very easy: it is a single file and is dependency-free, so you can simply include it via a CDN:
You can also install it via a package manager such as NPM, or vendor it from your own server.
Alpine provides a set of HTML attributes, all of which begin with the
x- prefix, the main one of which is
The content of
be access within the element that the
x-data attribute is located on.
To get a flavor of what AlpineJS looks like, let’s look at how to implement our counter example using it.
with one property,
count, in an
x-data attribute on the div for our counter:
This defines our state, that is, the data we are going to be using to drive dynamic updates to the DOM. With the state
declared like this, we can now use it within the div element it is declared on. Let’s add an
output element with
Next, we will bind the
x-text attribute to the
count attribute we declared in the
on the parent
div element. This will have the effect of setting the text of the
output element to whatever the
count is: if
count is updated, so will the text of the
output. This is “reactive” programming, in that
the DOM will “react” to changes to the backing data.
Next, we need to update the count, using a button. Alpine allows you to attach event listeners with the
To specify the event to listen for, you add a colon and then the event name after the
x-on attribute name. Then, the
on* attributes we discussed
earlier, but it turns out to be much more flexible.
We want to listen for a
click event, and we want to increment
count when a click occurs, so here is what the Alpine
code will look like:
And that’s all it takes. A simple component like a counter should be simple, and Alpine delivers.
“x-on:click” vs. “onclick”
As we said, the Alpine
x-on:click attribute (or its shorthand, the
@click attribute) is similar to the built-in
onclick attribute. However, it has additional features that make it significantly more useful:
You can listen for events from other elements. For example, the
.outsidemodifier lets you listen to any click event that is not within the element.
You can use other modifiers to:
- throttle or debounce event listeners,
- ignore events that are bubbled up from descendant elements, or
- attach passive listeners.
You can listen to custom events. For example, if you wanted to listen for the
htmx:after-requestevent you could write
Reactivity and Templating
We hope that you’ll agree that the AlpineJS version of the counter widget is better, in general, than the VanillaJS implementation, which was either somewhat hacky or spread out over multiple files.
A big part of the power of AlpineJS is that it supports a notion of “reactive” variables, allowing you to bind the count
div element to a variable that both the
output and the
button can reference, and properly updating all the
dependencies when a mutation occurs. Alpine allows for much more elaborate data bindings than what we have demonstrated
here, and it is an excellent general purpose client-side scripting library.
Alpine.js in Action: A Bulk Action Toolbar
Next, let’s implement a feature in Contact.app with Alpine. As it stands currently, Contact.app has a “Delete Selected Contacts” button at the very bottom of the page. This button has a long name, is not easy to find and takes up a lot of room. If we wanted to add additional “bulk” actions, this wouldn’t really scale very well visually.
In this section, we’ll replace this single button with a toolbar. Furthermore, the toolbar will only appear when the user starts selecting contacts. Finally, it will show how many contacts are selected and let you select all contacts in one go.
The first thing we will need to add is an
x-data attribute, to hold the state that we will use to determine if the
toolbar is visible or not. We will need to place this on a parent element of both the toolbar that we are going to
add, as well as the checkboxes, which will be updating the state when they are checked and unchecked. The best
option given our current HTML is to place the attribute on the
form element that surrounds the contacts table. We
will declare a property,
selected, which will be an array that holds the selected contact ids, based on the checkboxes
that are selected.
Here is what our form tag will look like:
Next, at the top of the contacts table, we are going to add a
template tag. A template tag is not rendered by a
browser, by default, so you might be surprised that we are using it. However, by adding an Alpine
we can tell Alpine: if a condition is true, show the HTML within this template.
Recall that we want to show the toolbar if and only if one or more contacts are selected. But we know that we will
have the ids of the selected contacts in the
selected property. Therefore, we can check the length of that array
to see if there are any selected contacts, quite easily:
The next step is to ensure that toggling a checkbox for a given contact adds (or removes) a given contact’s id from the
selected property. To do this, we will need to use a new Alpine attribute,
x-model attribute allows
you to bind a given element to some underlying data, or its "model".
In this case, we want to bind the value of the checkbox inputs to the
selected property. This is how we do this:
Now, when a checkbox is checked or unchecked, the
selected array will be updated with the given row’s contact id.
Furthermore, mutations we make to the
selected array will similarly be reflected in the checkboxes' state. This is
known as a two-way binding.
With this code written, we can make the toolbar appear and disappear, based on whether contact checkboxes are selected.
Now that we have the mechanics of showing and hiding the toolbar, let’s look at how to implement the buttons within the toolbar.
Let’s first implement the “Clear” button, because it is quite easy. All we need to do is, when the button is clicked,
clear out the
selected array. Because of the two-way binding that Alpine provides, this will uncheck all the selected
contacts (and then hide the toolbar)!
Here is the code:
For the Cancel button, our job is quite simple:
Once again, AlpineJS makes this very easy.
The “Delete” button, however, will be a bit more complicated. It will need to do two things: first it will confirm
if the user indeed intends to delete the contacts selected, and, if the user confirms the action, it will use the
Note that we are using the short-circuiting behavior of the
htmx.ajax() if the
confirm() call returns false.
htmx.ajax() function is just a way to access the normal, HTML-driven hypermedia exchange that htmx’s
Looking at how we call
htmx.ajax, we first pass in that we want to issue a
/contacts. We then pass in
two additional pieces of information:
source property is the element from which htmx will
collect data to include in the request. We set this to
$root, which is a special symbol in Alpine that will be
the element that has the
x-data attribute declared on it. In this case, it will be the form containing all of our
target, or where the response HTML will be placed, is just the entire document’s body, since the
DELETE handler returns a whole page when it completes.
Note that we are using Alpine here in a Hypermedia-Driven Application compatible manner. We could have issued an
AJAX request directly from Alpine and perhaps updated an
x-data property depending on the results of that request.
This is the key to scripting in a hypermedia-friendly manner within a Hypermedia-Driven Application.
So, with all of this in place, we now have a much improved experience for performing bulk actions on contacts: less visual clutter and the toolbar can be extended with more options without creating bloat in the main interface of our app.
The most noticeable thing about _hyperscript is that it resembles English prose more than it resembles other programming languages.
Like Alpine, _hyperscript is a modern jQuery replacement. Also like Alpine, _hyperscript allows you to write your scripting inline, in HTML.
We will give a small taste of what scripting in the _hyperscript language is like, so you can pursue the language in more depth later if you find it interesting.
Like htmx and AlpineJS, _hyperscript can be installed via a CDN or from npm (package name
_hyperscript uses the
\_ (underscore) attribute for putting scripting on DOM elements. You may also use the
data-script attributes, depending on your HTML validation needs.
Let’s look at how to implement the simple counter component we have been looking at using _hyperscript. We will place
output element and a
button inside of a
div. To implement the counter, we will need to add a small bit of
_hyperscript to the button. On a click, the button should increment the text of the previous
As you’ll see, that last sentence is close to the actual _hyperscript code:
Let’s go through each component of this script:
on clickThis is an event listener, telling the button to listen for a
clickevent and then executing the remaining code.
incrementThis is a “command” in _hyperscript that “increments” things, similar to the
- the “the” doesn’t have any semantic meaning _hyperscript, but can used to make scripts more readable.
a.b, meaning "Get the property
a.`" _hyperscript supports this syntax, but also supports the forms `b of aand
a’s b. Which one you use should depend on which one is most readable.
previousexpression in _hyperscript finds the previous element in the DOM that matches some condition.
<output />This is a query literal, which is a CSS selector wrapped between
In this code, the
previous keyword (and the accompanying
next keyword) is an example of how _hyperscript makes DOM operations
easier: there is no such native functionality to be found in the standard DOM API, and implementing this in VanillaJS is trickier
than you might think!
So, you can see, _hyperscript is very expressive, particularly when it comes to DOM manipulations. This makes it easier to embed scripts directly in HTML: since the scripting language is more powerful, scripts written in it tend to be shorter and easier to read.
_hyperscript in Action: A Keyboard Shortcut
While the counter demo is a good way to compare various approaches to scripting, the rubber meets the road when you try to actually implement a useful feature with an approach. For _hyperscript, let’s add a keyboard shortcut to Contact.app: when a user hits Shift-S in our app, we will focus the search field.
Since our keyboard shortcut focuses the search input, let’s put the code for it on that search input, satisfying locality.
Here is the original HTML for the search input:
We will add an event listener using the
on keydown syntax, which will fire whenever a keydown occurs. Further, we
can use an event filter syntax in _hyperscript using square brackets after the event. In the square brackets we
can place a filter expression that will filter out
keydown events we aren’t interested in. In our case, we only
want to consider events where the shift key is held down and where the “S” key is being pressed. We can create a
boolean expression that inspects the
shiftKey property (to see if it is
true) and the
code property (to see if
"KeyS") of the event to achieve this.
So far our _hyperscript looks like this:
Now, by default, _hyperscript will listen for a given event on the element it is declared on. So, in this case, with
the script we have so far, we would only get
keydown events if the search box is already focused. That’s not what
we want! We want to have this key work globally, no matter which element has focus.
Not a problem! We can listen for the
keyDown event elsewhere by using a
from clause in our event handler. In this
case we want to listen for the
keyDown from the window, and our code ends up looking, naturally, like this:
from clause, we can attach the listener to the window while, at the same time, keeping the code on the
element it logically relates to.
Now that we’ve picked out the event we want to use to focus the search box, let’s implement the actual focusing by
calling the standard
Here is the entire script, embedded in HTML:
Given all the functionality, this is surprisingly terse, and, as an English-like programming language, pretty easy to read.
Why a New Programming Language?
- Async transparency
In _hyperscript, asynchronous functions (i.e., functions that return
asyncannotations (which can’t be mixed with synchronous code).
- Array property access
In _hyperscript, accessing a property on an array (other than
lengthor a number) will return an array of the values of property on each member of that array, making array property access act like a flat-map operation. jQuery has a similar feature, but only for its own data structure.
- Native CSS Syntax
- Deep Event Support
Again we wish to stress that, in this example, we are not stepping outside the lines of a Hypermedia-Driven Application: we are only adding frontend, client-side functionality with our scripting. We are not creating and managing a large amount of state outside of the DOM itself, or communicating with the server in a non-hypermedia exchange.
Additionally, since _hyperscript embeds so well in HTML, it keeps the focus on the hypermedia, rather than on the scripting logic.
Taken all together, given a certain style of scripting and certain scripting needs, _hyperscript can provide an excellent scripting experience for your Hypermedia-Driven Application. It is a small and obscure programming language, so we won’t blame you if you decide to pass on it, but it is worth a look to understand what it is trying to achieve.
Using Off-the-Shelf Components
- Mutate the DOM but don’t communicate with a server over JSON
Respect HTML norms (e.g., using
inputelements to store values)
- Trigger many custom events, as the library updates things
The last point, triggering many custom events (over the alternative of using lots of methods and callbacks) is especially important, as these custom events can be dispatched or listened to without additional glue code written in a scripting language.
To make things concrete, let’s implement a better confirmation dialog for the
DELETE button we created in Alpine in the
previous section. In the original example we used the
SweetAlert2, that shows a much nicer looking confirmation dialog. Unlink the
confirm() function, which blocks
and returns a boolean (
true if the user confirmed,
false otherwise), SweetAlert2 returns a
Promise object, which
or deny an action) completes.
Integrating using callbacks
With SweetAlert2 installed as a library, you have access to the
Swal object, which has a
fire() function on it to
trigger showing an alert. You can pass in arguments to the
fire() method to configure exactly what the buttons
on the confirmation dialog look like, what the title of the dialog is, and so forth. We won’t get into these details
too much, but you will see what a dialog looks like in a bit.
So, given we have installed the SweetAlert2 library, we can swap it in place of the
confirm() function call. We then
need to restructure the code to pass a callback to the
then() method on the
Swal.fire() returns. A
deep dive into Promises is beyond the scope of this chapter, but suffice to say that this callback will be called when
a user confirms or denies the action. If the user confirmed the action, then the
result.isConfirmed property will be
Given all that, our updated code will look like this:
And now, when this button is clicked, we get a nice looking dialog in our web application:
Much nicer than the system confirmation dialog. Still, this feels a little wrong. This is a lot of code to write
just to trigger a slightly nicer
awkward. It would be more natural to move the htmx out to attributes on the button, as we have been doing, and then
trigger the request via events.
So let’s take a different approach and see how that looks.
Integrating using events
To clean this code up, we will pull the
sweetConfirm() will take the dialog options that are passed into the
fire() method, as well as
the element that is confirming an action. The big difference between the code we already have and
sweetConfirm(), rather than calling some htmx directly, will, instead, trigger a
confirmed event on the
button when the user confirms they wish to delete.
With this method available, we can now tighten up our delete button quite a bit. We can remove all the SweetAlert2
code that we had in the
@click Alpine attribute, and simply call this new
sweetConfirm() method, passing in the
$el, which is the Alpine syntax for getting `"the current element`" that the script is on, and then
the exact configuration we want for our dialog.
If the user confirms the action, a
confirmed event will be triggered on the button. This means that we can go back
to using our trusty htmx attributes! Namely, we can move
DELETE to an
hx-delete attribute, and we can we can use
hx-target to target the body. And then, and here is the crucial step, we can use the
confirmed event that is
triggered in the
sweetConfirm() function, to trigger the request, but adding an
hx-trigger for it.
Here is what our code looks like:
As you can see, this event-based code is much cleaner and certainly more “HTML-ish.” The key to this cleaner
implementation is that our new
sweetConfirm() function fires an event that htmx is able to listen for.
This is why a rich event model is important to look for when choosing a library to work with, both with htmx and with Hypermedia-Driven Applications in general.
In case of conflict, consider users over authors over implementors over specifiers over theoretical purity.
We have looked at several tools and techniques for scripting in a Hypermedia-Driven Application. How should you pick between them? The sad truth is that there will never be a single, always correct answer to this question.
Are you a bit more bold in your technical choices? Maybe _hyperscript would be worth taking a look at. (We certainly think so.)
Sometimes you might even consider picking two (or more) of these approaches within an application. Each has its own strengths and weaknesses, and all of them are relatively small and self-contained, so picking the right tool for the job at hand might be the best approach.
In general, we encourage a pragmatic approach to scripting: whatever feels right is probably right (or, at least, right enough) for you. Rather than being concerned about which particular approach is taken for your scripting, we would focus on these more general concerns:
- Avoid communicating with the server via JSON data APIs.
- Avoid storing large amounts of state outside of the DOM.
- Favor using events, rather than hard-coded callbacks or method calls.
And even on these topics, sometimes a web developer has to do what a web developer has to do. If the perfect widget for your application exists but uses a JSON data API? That’s OK.
Just don’t make it a habit.