anticore

A library to handle your AJAX/SSE DOM contents and to automate your requests, using some contracts.

Usage no npm install needed!

<script type="module">
  import anticore from 'https://cdn.skypack.dev/anticore';
</script>

README

anticore

A library to handle your AJAX/SSE DOM contents and to automate your requests, using some contracts.

npm Downloads Language grade: JavaScript Total alerts anticore

A brief explanation

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

Demos

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)

Key features

  • 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

Install

Using NPM: npm i anticore

Or using a CDN:

import anticore, { defaults, on, trigger } from 'https://unpkg.com/anticore@latest/index.js'

Contract anatomy

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
})

Target the AJAX/SSE loaded elements only

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)
  }
})

A full implementation

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>

APIs

on(selector, listener)

Useful to declare a contract to be applied for any element matching the selector, where:

  • selector: a query selector
  • listener: a function to be applied on the matching elements
    • element: the matching element
    • url: 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}`)
})

when(selector, { url }, path, picker = picked => picked)

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 current import.meta
  • path: the wanted module path
  • picker: an optional function to pick an exported listener from the loaded module It receives 2 arguments
    • default: the default module export (or undefined)
    • 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)

defaults()

A function that wraps 2 contracts

A click handler

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 submit handler

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.

trigger([node])

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)

parse(source, url)

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))

fetch(request, event = null, target = event?.target)

Useful to create your own DOM content fetchers, where:

  • request: the Request instance
  • event: the event invoking the fetch
  • target: the element invoking the fetch (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

Why another fetch()?

Connection loss management

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)

Event handling

If provided, anticore manages the event like this:

  1. It checks if the event behavior is prevented by Event.preventDefault() (e.g. from a form validation contract) or if the bubbling is canceled event.cancelBubble. In that case, anticore just avoids doing anything
  2. It prevents the event behavior
  3. It adds a fetching class on the element that triggers the event
  4. It enqueues that request
  5. It really fetches the contents
  6. It triggers the contracts on them
  7. It removes the fetching class on the element that triggers the event

sse(url, [options, [reviver]])

Useful to listen Server-Sent Events, where

  • url: the URL to listen
  • options: the EventSource options
  • reviver: a function to parse the DOM nodes from each message
import { sse } from 'anticore'

const eventSource = sse(url, options, reviver)

listen(event, target, listener[, options])

Useful to listen events, on any support (touch and/or not)

  • event: the event type
  • target: an EventTarget
  • listener: a function called each time the event triggers
  • options: the addEventListener options

It returns a function to remove the listener, without any arguments needed

import { listen } from 'anticore'

const forget = listen(event, target, listener, options)

Notable changes from V3

The V4 is now promise-based, the V3 next() is removed, if you need to await some async operations, just use an async listener.

Security

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

License

MIT

Contributors

Sponsors

JetBrains