mirror of
https://github.com/bigskysoftware/hypermedia-systems.git
synced 2025-12-01 00:02:56 -05:00
311 lines
15 KiB
Plaintext
311 lines
15 KiB
Plaintext
:sectnums:
|
||
:figure-caption: Figure {chapter}.
|
||
:listing-caption: Listing {chapter}.
|
||
:table-caption: Table {chapter}.
|
||
:sectnumoffset: 5
|
||
// line above: :sectnumoffset: 5 (chapter# minus 1)
|
||
:leveloffset: 1
|
||
:sourcedir: ../code/src
|
||
:source-language:
|
||
|
||
= Scripting Hypermedia Applications
|
||
|
||
This chapter covers
|
||
|
||
* The role of scripting in hypermedia applications
|
||
* The use of scripting for non-hypermedia applications on the Web
|
||
* Tools, guidelines and patterns for writing scripts in a hypermedia application
|
||
|
||
|
||
[partintro]
|
||
== Introduction: Do you just hate JavaScript?
|
||
|
||
This chapter is not about the merits of JavaScript as a programming language.
|
||
|
||
On the Web, the need for scripting is fulfilled by JavaScript. It is not the only language option. Some qualities that make a language good for hypermedia scripting are discussed, though we leave it to readers to decide whether they hold for JavaScript.
|
||
|
||
The discussion of scripting is framed around tools you can use for it (who doesn't like a new, shiny tool?) all of which are written in JavaScript. With each tool, we introduce archetypes, patterns/antipatterns, tips and resources.
|
||
|
||
|
||
== Quality Criteria
|
||
|
||
Local, restartable, generic.
|
||
|
||
"... implementations that do not simply run on the Web but that actually exist in the web ..."
|
||
-- Mike Amundsen, Building Hypermedia APIs with HTML5 and Node
|
||
|
||
Use ((progressive enhancement)).
|
||
|
||
The disadvantage is that scripting reduces ((visibility)), "in this case [...] the ability of a component to monitor or mediate the interaction between two other components" (Fielding).
|
||
|
||
Make scripts generic, and specify details in markup.
|
||
|
||
Scripts should be resilient to DOM modifications from other scripts.
|
||
|
||
A script should not impose a programming model upon the rest of the application. To the greatest practical extent, do not assume anything not in a Web specification. When more structure is needed, it should only be required in parts of the page that explicitly invoke the script. For instance, do not call `document.querySelector` with generic CSS selectors and attach event listeners, but query for classes or attributes which are explicitly placed by the page author to call your script.
|
||
|
||
Reuse HTML semantics, but do so carefully. For instance, a script that updates `<time>` elements to match the user's local timezone
|
||
|
||
|
||
== Case studies
|
||
|
||
* A-Frame
|
||
* GitHub's components
|
||
|
||
|
||
== Adding features to Contact.App
|
||
|
||
* optimistic whatever
|
||
* drag and drop reordering
|
||
|
||
|
||
[#web-as-app-delivery]
|
||
== Perils of interaction
|
||
|
||
The HTML, CSS, JS trio of languages have demonstrated their capability as a GUI toolkit so extensively, that to cite evidence seems inane. We even have tools like Electron for applications that may never be run in a browser context (although the excessive resource use from these kinds of applications is a pain point for users).
|
||
|
||
Web applications are not just cross-platform but _platform-independent_. The web browser provides a standard interface that abstracts many aspects of hardware and operating systems:
|
||
|
||
* Mode of input, i.e.: mouse, touchscreen, D-pad
|
||
* Executable formats and application packaging
|
||
* Filesystems
|
||
|
||
The ubiquity of the web means that:
|
||
|
||
* *Your application is destined to run in environments you have not tested them in.* The only way to minimize this is to minimize the amount of code running on the user's device.
|
||
* *You will need to create an appropriate user experience for different platforms in one codebase.* This is known as ((responsive web design)). You can try to work around this requirement by trying to detect different types of devices and delivering a different app accordingly. This technique may be frowned upon but it still finds use. For instance, at the time of writing, Twitter redirects mobile clients to `mobile.twitter.com`. There are, of course, devices that are neither desktop nor mobile. These devices will likely be neglected by this kind of application. Hypermedia-driven applications should instead shift as much of the burden of responsive web design as possible to the platform developer, who is tasked with providing a conformant Web browser.
|
||
|
||
[sidebar]
|
||
The existence of `mobile.twitter.com` is an annoyance to anyone who uses a browser that synchronizes history between devices --the address bar will often suggest mobile pages when searching on desktop.
|
||
|
||
|
||
== Pervasive scripting, localized scripting
|
||
|
||
There is sometimes a distinction made between ((high-JS)) and ((low-JS)) applications. Supposedly, a single-page application is high-JS, and the main purpose of adopting a hypermedia architecture is to reduce the amount of JavaScript you have to write. While some developers do avoid JavaScript this way, it's a reductive way of viewing hypermedia architectures and related tools.
|
||
|
||
The amount of JavaScript code (whether measured in SLoC or KB minified and gzipped) is not a good proxy for the paradigm its programmers used. Consider these two applications:
|
||
|
||
* _Artifice_ is a CMS, with a bespoke rich text editor that is implemented as a Web Component. Otherwise, it's not very interesting, using full page reloads for navigation.
|
||
* _Beholder_ is a Hacker News client, which fetches posts and comments from an API and presents them with little processing in between. It uses an SPA framework to provide a flashy experience to distinguish itself.
|
||
|
||
A good rich text editor takes a lot of consideration and code to implement properly, whereas a Hacker News client is a common exercise for newcomers to web development. Thus, it's quite likely that Artifice has more JavaScript than Beholder. However, it is much closer in architecture and programming style to the average _low-JS_ application.
|
||
|
||
This is why I find it much more useful to talk about ((pervasive-JS)) and ((localized-JS)). Artifice's rich text editor is localized, relegated to a specific part of the system (both in the codebase, and in the document). As a result, it would be trivial to use it in a different application. In comparison, most components (component in the sense used in SPA frameworks) of Beholder are only reusable within Beholder.
|
||
|
||
This is not a criticism of Beholder or SPAs. Non-reusable aspects are still there in Artifice, in the server-side controllers and templates. Rather, it's an illustration of a style of scripting hypermedia creates. Application-specific code lives in servers, while generic, interoperable code and data is transmitted to clients.
|
||
|
||
|
||
== Vanilla JavaScript
|
||
|
||
In SPAs, frameworks like React, Vue, Svelte, Angular, Solid... are almost mandatory. A framework can provide
|
||
|
||
|
||
Many of these requirements are
|
||
However,
|
||
|
||
=== RSJS
|
||
|
||
<https://ricostacruz.com/rsjs/>. Stands for "Reasonable System for JavaScript Structure".
|
||
|
||
A system for creating reusable behaviors in JavaScript. RSJS is a set of guidelines for writing client-side JavaScript for non-single-page applications.
|
||
|
||
* *"Think in component behaviors".* A behavior is a piece of code that only affects a select part of the document. Behaviors are linked to elements using HTML data attributes:
|
||
+
|
||
[source,html]
|
||
----
|
||
<div class='main-navbar' data-js-collapsible-nav>
|
||
<button class='expand' data-js-expand>Expand</button>
|
||
<a href='/'>Home</a>
|
||
<ul>...</ul>
|
||
</div>
|
||
----
|
||
+
|
||
This is Locality of Behavior in action. Note that LoB does not require that behavior is _implemented_ at the site of use, only _invoked_.
|
||
|
||
* *"One component per file".* Each .js file should be named after the data attribute it is invoked by. This is also important for locality: There should be a clear, deterministic path from invocation to definition.
|
||
|
||
* *"Load components in all pages".*
|
||
|
||
+
|
||
[quote]
|
||
--
|
||
Your main .js file should be a concatenation of all your behaviors.
|
||
|
||
It should be safe to load all behaviors for all pages. Since your behaviors are localized to their respective components, they will not have any effect unless the element it applies to is on the page.
|
||
--
|
||
|
||
* *"Use a data attribute".* "You can use ID’s and classes, but this can be confusing since it isn’t obvious which class names are for styles and which have JS behaviors bound to them." This is a case if Locality of Behavior and Separation of Concerns in unison.
|
||
|
||
* *"No inline scripts".* "By putting imperative logic outside your .js files (eg, JavaScript in your .html), it makes your application harder to test and they pose a significant maintenance burden". This is a departure from _hyperscript and Alpine practices. An important concern around testing. In hypermedia-driven applications, client scripting is usually reserved for interactions. Manual testing is often more appropriate for this kind of code. However, if your application requires a lot of logic to be computed client-side, this is a good principle to follow.
|
||
|
||
Remember that these are only a few of the guidelines RSJS comprises. We present the structural guidelines that are relevant to the topics we covered.
|
||
|
||
|
||
== _hyperscript
|
||
|
||
<https://hyperscript.org>. Stylized __hyperscript_.
|
||
|
||
Hyperscript is a language derived from ((HyperTalk)), the scripting language accompanying the early hypermedia system ((HyperCard)). This makes it a member of the ((xTalk)) family.
|
||
|
||
=== _Principle:_ Events
|
||
|
||
=== _Example:_ Draggable window
|
||
|
||
.Draggable._hs
|
||
[source,hyperscript]
|
||
-------------
|
||
behavior Draggable(dragHandle)
|
||
init
|
||
default dragHandle to me
|
||
end
|
||
on pointerdown(clientX, clientY) from dragHandle
|
||
halt the event
|
||
trigger draggable:start
|
||
measure my x, y
|
||
set xoff to clientX - x
|
||
set yoff to clientY - y
|
||
repeat until event pointerup from document
|
||
wait for pointermove(pageX, pageY) or
|
||
pointerup (pageX, pageY) from document
|
||
add { left: ${pageX - xoff}px; top: ${pageY - yoff}px; }
|
||
trigger draggable:move
|
||
end
|
||
trigger draggable:end
|
||
end
|
||
-------------
|
||
|
||
== Alpine.js
|
||
|
||
<https://alpinejs.dev/>.
|
||
|
||
Alpine.js is a library inspired by Vue.js. It emphasizes being lightweight.
|
||
|
||
.Sample code from the Alpine.js website
|
||
[source,html]
|
||
----
|
||
<div x-data="{ open: false }">
|
||
<button @click="open = true">Expand</button>
|
||
|
||
<span x-show="open">
|
||
Content...
|
||
</span>
|
||
</div>
|
||
----
|
||
|
||
Like _hyperscript, it allows you to invoke behavior directly from markup. Unlike _hyperscript, it uses JavaScript instead of a bespoke programming language.
|
||
|
||
In an Alpine application, code will rarely modify the document directly. Instead, Alpine uses a reactivity system borrowed from Vue. `x-data` is used to define JavaScript objects accessible from a given subtree of the document. Templating directives like `x-show` or `x-for` are used to modify elements based on those objects. Then, when that data is modified, Alpine will update the document accordingly.
|
||
|
||
In _hyperscript, one might write the above component as such:
|
||
|
||
[source,html]
|
||
----
|
||
<div id="disclosure">
|
||
<button _="on click toggle *display of #popup"
|
||
>Expand</button>
|
||
|
||
<span id="popup">
|
||
Content...
|
||
</span>
|
||
</div>
|
||
----
|
||
|
||
Compared to the original Alpine example, the _hyperscript version has a lot less indirection. That can be a disadvantage -- see how handling all document mutations in the framework allows Alpine to give us easy animations:
|
||
|
||
[source,html]
|
||
----
|
||
<div x-data="{ open: false }">
|
||
<button @click="open = true">Expand</button>
|
||
|
||
<span x-show="open" x-transition> <1>
|
||
Content...
|
||
</span>
|
||
</div>
|
||
----
|
||
<1> The `x-transition` attribute will add a fade animation by default. Examples of more intricate control over transitions can be found in the Alpine.js documentation.
|
||
|
||
When the page author directly modifies the document via code, it's a lot more difficult for tools to provide this kind of convenience because the transition logic needs to be sandwiched around the addition/removal of elements. Achieving the above in _hyperscript would look a bit like this:
|
||
|
||
[source,html]
|
||
----
|
||
<div id="disclosure">
|
||
<button _="on click
|
||
if #popup's *display is 'none'
|
||
show #popup
|
||
transition the #popup's opacity from 0 to 1
|
||
else
|
||
transition the #popup's opacity from 1 to 0
|
||
hide #popup
|
||
"
|
||
>Expand</button>
|
||
|
||
<span id="popup">
|
||
Content...
|
||
</span>
|
||
</div>
|
||
----
|
||
|
||
Of course, this code could be extracted as a reusable behavior:
|
||
|
||
[source,html]
|
||
----
|
||
<div _="install Disclosure">
|
||
<button _="on click trigger Disclosure:toggle"
|
||
>Expand</button>
|
||
|
||
<span>
|
||
Content...
|
||
</span>
|
||
</div>
|
||
----
|
||
|
||
|
||
=== _Principle:_ Locality
|
||
|
||
=== _Example:_ Search form
|
||
|
||
=== _Pattern:_ Sprinkle
|
||
|
||
=== _Pattern:_ Reusable Behavior
|
||
|
||
One form of script that goes well with hypermedia is similar to a polyfill; which extends the interaction capabilities of the client. Unlike a polyfill, however, they introduce capabilities not in any specification and their use may be completely unique to one application. The specific behavior to be used is not programmed but still encoded in the document, usually in an application-specific format. For example, one might implement a rich text input for forms which is instantiated by adding a class to an `<input>` tag, or enhance all anchor links with previews that appear when hovered.
|
||
|
||
The polyfill-style script, which can be called the _((behavior script))_, must be written with a great care for accessibility. Accessibility in web applications is an intricate dance between browsers, operating systems and assistive tools. The identities of these systems are all unknown to the application developer, but they still have to step in time. As one builds further beyond the features the user agent provides, the risk of stepping on someone's toes tends to increase.
|
||
|
||
=== _Principle:_ Element Lifecycle
|
||
|
||
== Summary
|
||
|
||
|
||
|
||
////
|
||
The last constraint of REST as described by Fielding is _((Code on Demand))_. Implementing servers enclose code in hypermedia documents and user agents execute them.
|
||
|
||
"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."
|
||
-- Roy Fielding, Architectural Styles and the Design of Network-based Software Architectures
|
||
|
||
By this explanation, the main role of scripts from the application developer's perspective is to extend the browser to the application's needs in the same way as browser extensions extend the browser to the user's needs. A _((polyfill))_ is an example of this kind of script. As we eill see later, however, there are other ways of using scripting that go well with hypermedia.
|
||
|
||
"Allowing features to be downloaded after deployment improves system extensibility."
|
||
-- (continued)
|
||
|
||
In the case of the Web, it improves system extensibility so much that the RESTful protocol can be used as a generic delivery mechanism for non-RESTful applications. The ubiquity and freedom of the Web, along with the fact that applications can be downloaded and used immediately without an installation phase, has made it an attractive medium for distributing applications. In particular, the Web is a compelling alternative to the app stores to which some newer consumer operating systems restrict users (i.e.: Android, iOS, to a lesser extent: Chrome OS).
|
||
|
||
The advantages of using the Web this way are not without peril. Some disadvantages are discussed in <<web-as-app-delivery>>.
|
||
|
||
"However, it also reduces visibility, and thus is only an optional constraint within REST."
|
||
-- (continued)
|
||
|
||
...
|
||
|
||
|
||
.continued:
|
||
____
|
||
The mobile agent style is an example where the lack of visibility may lead to security concerns.
|
||
____
|
||
|
||
[example]
|
||
Gemini is a modern hypermedia system, similar to the Web, that does not include scripting (along with many other features).
|
||
////
|
||
|