Declarative enhancement for HTML: simple, composable, lean. Only 8KB .min.gz. That name comes from innovative dual ignition engines from Alfa Romeo — idea is that you work on client-side fluidity from both server- and client-side.
TwinSpark was heavily influenced by Intercooler (predecessor of htmx) and was written since Intercooler's extensibility wasn't accomodating enough for our needs: we needed a way to integrate analytics and a "batch" request — when a lot of elements on page want to query same API.
Now it's a battle-tested technology used in a several companies. On a technical side, it got some nice properties:
id="[unique-id]"
.See at Github.
This document has tests embedded in examples, to run them, press:
The core feature of TwinSpark is it's ability to update fragments of the page using AJAX requests to server without writing JavaScript.
To simplify inner logic you have one limitation - you can only return single element from the server. If you return more, they will be ignored. How to deal with that and why it is so - read on.
Usually requests are triggered on natural interrupts: submit on forms and clicks elsewhere, but sometimes you want more, like triggering on being seen or hovered:
If you need to do some custom things around your request, you can use
ts-req-before
and ts-req-after
. Those use same
syntax as ts-action
, so it's pretty
flexible. One additional thing is that if last (or the only) command of
ts-req-before
returns falsy value (false
, or
an empty string, or a zero) - this stops that request.
Sometimes collecting the data you need to send in templates is really
annoying: you'll have to teach lower-level components/partials/etc some
knowledge about higher levels. That is something TwinSpark can prevent: when
you send a request, it iterates through element parents and searches for
ts-data
attributes, parses their content as query string or JSON
and merges it into a single object. Note how values with same keys are aggregated
into an array (like normal query string or form data). You can override that by
specifying key with null or empty string (see demo source).
Often you don't want to replace clicked element, but some other part of the
page, like if you hit "Refresh" and need to update big part of the page. This
can be done with ts-target
attribute, which takes a CSS
selector.
I'm waiting... Do it!
Interestingly, this seems like the most common pattern - when a button needs
to update an element around itself. Add modifier parent
before
selector and element.closest(selector)
method will be used to
find a parent. This will help a lot to minimize amount of ids.
There are a few other selector modifiers - please read sources of the function
_findSingleTarget
until we get proper documentation. They are here
because regular CSS selectors start at the top of the document, but in case of
TwinSpark you're starting from a concrete element. DOM has various APIs to do
that, which TwinSpark maps to modifiers: inherit
,
parent
, child
, sibling
.
Wanna read text behind me? Do it!
Most twinspark commands and extensions operate directly on the current
target element. However, some of them might require a pair of elements
(e.g. a command that copies data from one input to another). This means
supplying a twinspark selector as an argument. To point directly to
the current target element, use the target
keyword.
Click here or here
to copy the value here:
Click one of these buttons to rename it with the value above: or
Another useful target is the element that created the event. The simplest
way to access it is to create a twinspark extension that sets
target
to event.target
:
URLs are a fundament of the Web. Changing URLs in line with activity makes your app reloadable, browseable with backward/forward button and overall a good citizen of the Web.
NOTE: twinspark is loaded with limit of 3 items in history storage (which uses IndexedDB), so if you click all of those links and then go back you'll see than it'll only restore HTML for the first 3 clicks
It's really irritating when you click a link and nothing happens for some time. Luckily
TwinSpark makes it really easy: it adds class ts-active
to an element, enhanced with ts-req
.
Sometimes you don't need to go to server to do something. Closing popup or hiding an element can be done without network round trip.
Default actions (those are defined in TwinSpark without you doing anything):
stop
: stops propagation of an event which triggered this action pipelineprevent
: prevents default reaction of an event which triggered this action pipelinedelay [Ns]
: delays execution of an action pipeline by N secondstarget ['selector']
: selects another element (based on current one, so you can use all selector modifiers)remove
: removes target element (current one by default)wait [event-name]
: waits for an event to happen on a target elementclass+ [class-name]
: adds a class to a target elementclass- [class-name]
: removes a class from a target elementclass^ [class-name]
: toggles a class on a target elementnot [command-name arg1 arg2]
: negates return value of a given commandlog [what ever you want]
: logs whatever you pass it, along with an input value if anyremove
Hey! I'm here!
delay
Remove with timeout
wait
(waiting for an event to happen)Remove after transition
animate
(no Safari here)
This is going to be removed
And this only after transition
Doing something when element is almost visible makes it possible to implement lazy loading and various analytical events
You'll probably see this text after around 5 seconds or so. Click "Reset" to see loader again.
This sentence will log some message when it becomes invisible (moves out of browser viewport, and, actually, on load as well).
Popups, modals, menus and some other elements can make use of click
happened outside
. It could be done with markup and underlying element,
but why bother if you have straightforward trigger.
This trigger is ideally used with modifier once
, since you're
probably going to remove that modal you calling it on - using once
will clean up your listeners so you won't get memory leaks.
TwinSpark usually deals with single element being target (ts-target
)
and single element being replacement (ts-req-selector
) - this is
much more controlled behavior than multiple elements. But if you look how
endless scrolling is implemented in HTML, it's usually a long list of elements
inside some other element - so you have to deal with several elements being appended
to a parent. For this and similar use cases there is a modifier children
in ts-req-selector
.
Element 1
Element 2
This is a thing which is not immediately obvious, but is one of the reasons why TwinSpark appeared. It is an important optimization. Our use case for it was following: page renders for an anonymous user (for efficient caching on CDN) and then status of wishlisted products is checked. It's a pattern we use often and we really wanted it to be a first-class feature.
Span 1
Span 2
Span 3
Sometimes you need to update more than a single point in your DOM tree.
Maybe there is a basket item counter should be incremented, or a notification
incoming. To perform that you need to add another tag in your HTML reply and
indicate where it should go. There are two ways to do that: either by a
selector in ts-swap-push
attribute or by a `ts-swap-push` header.
The latter has a form
[replace strategy]: [selector-in-document] <= [selector-in-response]
Update me!
And me!
Also waiting here.
Form validation is a common task, and TwinSpark allows to consolidate validation logic on the server. Surprisingly, it could be difficult, but `ts-swap="morph"` strategy allows us to just return whole new form with errors and not mess up with focus.
Important bits: input `id`s and that form submit appends
<input type="submit">
's value - this way you can distinguish between
validation and submission. Notice how `keyup` updates form on every character
input and it feels natural.
Solve this problem with odd numbers
It is useful to do something when node is removed (especially if that's some child or even non-related node triggering that removal). It's possible, but not recommended to use often since performance characteristics of the code are not well understood.
When this paragraph is removed by clicking button, it will resurrect itself.
Actions pipe their return values into next action as o.input
, check
the source of the next example to see how it works.
script
elements in
responsesSetting innerHTML
to a value which contains script
element does not execute JavaScript inside that element. TwinSpark handles
that for you, check it out.
Autocomplete is interesting because it executes many things at once. Just look at the source, the interesting part is trigger modifiers - it does something only if user typed something (rather than just navigated field with cursor keys) and then stopped for 200 ms.
When autocomplete triggers a time-consuming operation (e.g. full-text search),
the implementation above triggers numerous requests if the user types slow enough.
If requests finish at different durations, an older request can override the
latest. To avoid this, we need to abort the XHR using ts-req-strategy="last"
.
Possible values for ts-req-strategy
are:
first
prevent triggering new requests until the active one finishes
(useful for forms)last
abort active request when a new one is triggeredqueue
(default) send requests as they are triggeredEvery other request is slow
Filtering on ecommerce sites is a complex task. On one side you want it to be crawlable by Google, on the other if a user selected two filters one by one you'd like to see products, filtered by both. Naïve implementation will filter only one of them if a pause between clicks was short enough. It seems like the best way is form, full of links (so that Google/no-js envs can still use it), which toggle checkboxes when JS is enabled and auto-submit form.
Selected filters: