A library to handle your AJAX/SSE DOM contents and to automate your requests, using some contracts.
anticore
is a library to make a very simple bridge between your front-end and your back-end, using some contracts
associating a selector (to match some elements) and a task (a function to make something on the matching elements).
Each time anticore
loads a DOM content by an AJAX request or received as a Server-Sent Event, it calls the trigger(fragment)
on it.
It implies that: except for the elements already into the document, it acts before its insertion, to improve the performances & the UX (the browser only renders once per view, in opposition to a mutations-based system).
In fact, anticore
doesn't insert anything, you need to make some "tree" contracts dedicated to that task, like the generic @anticore-contracts/tree-view
or @anticore-contracts/tree-insert
Since it really often helps to understand the concept, you can try it yourself in a few seconds:
- Go to the Live demo, using another tab/window
- Replace the content of the Your contracts by
on('main.a', element => {
element.querySelector('h1').append('should be a: ', element.className)
})
on('main.b', element => {
element.querySelector('h1').append('should be b: ', element.className)
})
on('main.c', element => {
element.querySelector('h1').append('should be c: ', element.className)
})
on('main', element => {
element.querySelector('h1').append(' AND can be one of a || b || c || my-own: ', element.className)
})
// a very basic view switcher
on('.anticore > main', element => {
const main = document.querySelector('main')
main.replaceWith(element)
})
- Press the "play" button on its right
- Go to the Server responses
- Click on the "plus" button, type
a
into the prompt & press Enter - Click on the "plus" button, type
b
into the prompt & press Enter - Click on the "plus" button, type
c
into the prompt & press Enter - Finally, send them by clicking the "play" button on its right, in any order and check how it changes the Preview, and the Current tree.
Alternatively, you can also try this other demo, which includes an SSE example:
anticore-demo (also installable from its repository)
-
Content based, anticore is a utility used to easily interact with any DOM server-side rendered (SSR) element, even received in AJAX or SSE. Of course, it also supports the
ServiceWorker
rendered elements to work offline. -
Contracts oriented, just like an event listener, you can write some contracts to define some process on any element matching a selector when anticore receives it, regardless of how many (various) views needs them.
-
Automated on demand, by a single function call, during the script initialization, it avoids the need to write any AJAX request, they are automatically deduced by the anchor/form attributes, on the user interaction related event (
click
/submit
). -
Very low front-end, weighing only 4Ko minified without dependency, it provides a strategy to incite you to keep your front code lightest as possible, letting your rendering strategy, and the control to the server side.
The front code is only used as an unobstrusive overlay to improve the user experience and let the client resources available for any other operations.
-
Reusable, through projects, you have a lot of as generic contracts to cover most of your needs at once... you can also write any project-specific contracts if needed, just based on the selector precision.
-
Server-side/framework agnostic, no specific server configuration needed, just to receive some DOM contents (HTML, SVG, ...). It can be used in conjunction to your favorite libraries & frameworks.
-
Easy to maintain, it doesn't chain all the process, each contract, ideally following the single responsibility principle, is simply replaceable/removable without the need to check the entire project.
-
TrustedTypes support for your own trusted contents.
-
Lazy load support
Using NPM:
npm i anticore
Or using a CDN:
import anticore, { defaults, on, trigger } from 'https://unpkg.com/anticore@latest/index.js'
A contract is just a query selector associated function, a few like a routing route, where the selector is tested each time anticore receives a new content.
By default, once triggered, anticore triggers the contracts on any element matching a provided selector, event if that element is already in the document or received after.
import { on } from 'anticore'
// a contract example
on('a.query.selector', (matchingElement, serverResponseURL) => {
// processes to apply on the element
})
anticore loads the contents in a <body class="anticore">
(created internally), then you can use that class in your selector to target that
elements, specifically.
For example, we don't want to replace an already embedded element by itself.
import { on } from 'anticore'
// selects an element which is only received after the page load
on('.anticore a.query.selector', (matchingElement, serverResponseURL) => {
// processes to apply on the element
})
A useful selector prefix can also be body:not(.anticore)
to only target the elements initially loaded (non AJAX/SSE).
import { on } from 'anticore'
on('body:not(.anticore) a.query.selector', (matchingElement, serverResponseURL) => {
// processes to apply on a loaded element
})
Initialization (aka main.js
)
// your generic contracts
import './contracts/generic/index.js'
// your view contracts
import './contracts/view/index.js'
// register the default contracts, to handle your anchors/forms without target attribute
import 'anticore/defaults.js'
// your tree contracts
import './contracts/tree/index.js'
// applies the contracts on the current document elements
import 'anticore/trigger.js'
A very first contract (aka view-switcher.js
)
When you create an AJAX navigation, the most common operation is to replace the current contents by the user requested contents.
To do that, commonly...
- you need to create a lot of functions to build some AJAX (
XMLHttpRequest
/fetch
) requests - you need to check if the response is the expected response (e.g. 403, 404, 500, ...)
- you need to write some functions to treat the received contents
- you need to update the user's history
... requiring an abstraction to avoid code repetitions
With anticore, it's really shorter/simpler!
Since the <main>
& <title>
are unique in a page, you can easily write that process with a unique contract.
import { on } from 'anticore'
// matching any received <main> / <title>
on('.anticore > main, .anticore title', (element, url) => {
// creating a selector based on the element node name
const selector = element.nodeName.toLowerCase()
// retrieving the same embedded element in the document
const current = document.querySelector(selector)
// replacing the embedded element by the new one
current.replaceWith(element)
if (selector === 'title') {
// updating the history
history.pushState({}, element.innerHTML, url)
} else {
window.scrollTo(0, 0)
}
})
Voilà, import it in your main.js
and your AJAX navigation is resolved for all your pages, at once!.
Caution: to improve the user experience and the performances, it's recommended to import the "switchers" as very last
contracts, in your main.js
.
import { defaults, trigger } from 'anticore'
// non-switching contracts here
import './contracts.js'
// then the view-switcher
import './view-switcher.js'
defaults()
trigger()
Example:
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Initial title</title>
<script src="/assets/main.js" type="module"></script>
</head>
<body>
<main>
<h1>Initial title</h1>
<a href="/response">Load the response</a>
</main>
</body>
</html>
response
<title>Response title</title>
<main>
<h1>Response title</h1>
</main>
Resolves to
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Response title</title>
<script src="/assets/main.js" type="module"></script>
</head>
<body>
<main>
<h1>Response title</h1>
</main>
</body>
</html>
Useful to declare a contract to be applied for any element matching the selector, where:
selector
: a query selectorlistener
: a function to be applied on the matching elementselement
: the matching elementurl
: the url providing the node (can be empty, e.g. when the nodes are already in the current page)
import { on } from 'anticore'
// An listener can be async
on('body', async (element, url) => {
element.append(`Hello world from ${url}`)
})
A on
-like method allowing to lazily import a module method to use as a on
listener, but on-the-fly.
selector
: a query selector{ url }
: the currentimport.meta
path
: the wanted module pathpicker
: an optional function to pick an exported listener from the loaded module It receives 2 argumentsdefault
: the default module export (orundefined
)exports
: the named module exports
Example:
when('.anticore main', import.meta, 'view-switcher.js')
Is equivalent to
on('.anticore main', async (element, url) => {
const exports = await import(new URL('view-switcher.js', import.meta.url).toString())
return exports.default(element, url)
})
Or
when('.anticore main', import.meta, 'view-switcher.js', switcher => switcher)
Or
when('.anticore main', import.meta, 'view-switcher.js', (switcher, exports) => exports.default)
A function that wraps 2 contracts
A contract that tells to anticore to fetch the href
of any clicked anchor
It uses the following selector
a[href^="http"]:not([download]):not([target]),
a[href^="."]:not([download]):not([target]),
a[href^="/"]:not([download]):not([target])
A contract that tells to anticore to fetch the action
of any submitted form
It uses the following selector
form:not([target])
Additionally, before the fetching, it removes the form descendants that have a error
class, useful to remove the error messages from a previous submission.
Useful to apply the declared contracts on the provided node
, where:
- optional
node
: the targeted node (element or current document)
import { trigger } from 'anticore'
trigger(document)
Useful to parse an existing HTML/SVG source to real DOM nodes, to be able to trigger on it.
It parses the provides source into a <body class="anticore" id="{url}">{source}</body>
import { trigger } from 'anticore'
trigger(parse(source, url))
Useful to create your own DOM content fetchers, where:
request
: the Request instanceevent
: the event invoking thefetch
target
: the element invoking thefetch
(gets a.fetching
, until resolved)
** If no event
is provided, anticore just fetches without DOM parsing, no contracts triggering**
import { fetch } from 'anticore'
fetch(request, event, target) // fetches the contents & triggers the contracts on them
fetch(request) // just fetches
Once called, anticore attempts to fetch the response until its resolution.
If the user isn't connected, anticore retries after a delay (1 second by default)
If provided, anticore manages the event
like this:
- It checks if the event behavior is prevented by
Event.preventDefault()
(e.g. from a form validation contract) or if the bubbling is canceledevent.cancelBubble
. In that case, anticore just avoids doing anything - It prevents the event behavior
- It adds a
fetching
class on the element that triggers the event - It enqueues that request
- It really fetches the contents
- It triggers the contracts on them
- It removes the
fetching
class on the element that triggers the event
Useful to listen Server-Sent Events, where
url
: the URL to listenoptions
: theEventSource
optionsreviver
: a function to parse the DOM nodes from each message
import { sse } from 'anticore'
const eventSource = sse(url, options, reviver)
Useful to listen events, on any support (touch and/or not)
event
: the event typetarget
: anEventTarget
listener
: a function called each time the event triggersoptions
: theaddEventListener
options
It returns a function to remove the listener, without any arguments needed
import { listen } from 'anticore'
const forget = listen(event, target, listener, options)
The V4 is now promise-based, the V3 next()
is removed, if you need to await some async operations, just use an async
listener.
If your project is defining a strict Content-Security-Policy (CSP) you can need to add the following header/meta, to trust your DOM contents:
Content-Security-Policy: require-trusted-types-for 'script'; trusted-types anticore