Skip to content

Commit

Permalink
first commit
Browse files Browse the repository at this point in the history
  • Loading branch information
croxton committed Dec 1, 2023
0 parents commit aa8f1fb
Show file tree
Hide file tree
Showing 14 changed files with 2,071 additions and 0 deletions.
20 changes: 20 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# #############################################################################
# # GIT IGNORE # #
# #############################################################################

node_modules/*
/yarn-error.log
/npm-debug.log
.env
ehthumbs.db
.history
.cache
.DS_Store
.idea
.project
.rvmrc
.settings
*.esproj
*.tmproj
*.tmproject
.vscode
74 changes: 74 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
# htmx components

## Overview

A minimalistic javaScript component framework that works seamlessly with htmx. No bundler required, all you need is `<script>`.

## Requirements

Modern browsers only (sorry IE11!): https://caniuse.com/es6-module-dynamic-import

## How to use

1. Include `components.min.js` and `history-preserve.min.js` in the `<head>` of your page, right after `htmx`:
```html
<script defer src="https://cdn.jsdelivr.net/gh/bigskysoftware/htmx/src/htmx.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/croxton/htmx-components/dist/components.min.js"></script>
<script defer src="https://cdn.jsdelivr.net/croxton/htmx-components/dist/history-preserve.js"></script>
```

2. Create a folder in the webroot of your project to store components, e.g. `/scripts/components/.` Add a `<meta>` tag and set the `basePath` of your folder:
```html
<meta name="htmx-components-config" content='{ "basePath" : "/scripts/components/" }'>
```

3. Reference the `components` and `history-preserve` extensions via the `hx-ext` attribute:
```html
<body hx-ext="components,history-preserve">
```

4. Attach a component to an html element with the `data-component` attribute.
```html
<div id="message" data-component="hello"></div>
```

5. Add a script in your components folder with the name of your component:
```js
export default class Hello extends HtmxComponent {
message;

constructor(elm) {
super(elm);
this.mount();
}

mount() {
this.message = document.querySelector(this.elm);
this.message.textContent = 'Hello world!';
}

unmount() {
if (this.mounted) {
this.message = null;
}
}
}
```

## Attributes

### data-component

### data-load

### data-version

### data-options

### hx-history-preserve

## Component classes

### mount()

### unmount()
191 changes: 191 additions & 0 deletions dist/components.min.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
var __defProp = Object.defineProperty;
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: !0, configurable: !0, writable: !0, value }) : obj[key] = value;
var __publicField = (obj, key, value) => (__defNormalProp(obj, typeof key != "symbol" ? key + "" : key, value), value);
class HtmxComponent {
constructor(element = "", options = {}) {
__publicField(this, "mounted", !1);
__publicField(this, "elm", null);
__publicField(this, "target", null);
this._options = options || {}, element && (this.elm = element);
}
get options() {
return this._options;
}
set options(defaults) {
let options = {};
if (this.elm) {
let mount = document.querySelector(this.elm);
if (mount) {
let optionsFromAttribute = mount.dataset.options;
optionsFromAttribute && (options = JSON.parse(optionsFromAttribute)), mount = null;
}
}
this._options = {
...this._options,
...defaults,
...options
};
}
mount() {
}
unmount() {
}
refresh() {
this.unmount(), this.mount();
}
}
(function() {
let config = {
origin: location.origin,
basePath: "scripts/components"
}, configMeta = document.querySelector('meta[name="htmx-components-config"]') ?? null;
configMeta && (config = {
...config,
...JSON.parse(configMeta.content)
}), config.basePath = config.basePath.replace(/^\/|\/$/g, "");
const strategies = {
event: (requirement) => new Promise((resolve) => {
let topic;
if (requirement.indexOf("(") !== -1) {
const topicStart = requirement.indexOf("(") + 1;
topic = requirement.slice(topicStart, -1);
}
topic ? document.body.addEventListener(
topic,
() => {
resolve();
},
{ once: !0 }
) : resolve();
}),
idle: () => new Promise((resolve) => {
"requestIdleCallback" in window ? window.requestIdleCallback(resolve) : setTimeout(resolve, 200);
}),
media: (requirement) => new Promise((resolve) => {
const queryStart = requirement.indexOf("("), query = requirement.slice(queryStart), mediaQuery = window.matchMedia(query);
mediaQuery.matches ? resolve() : mediaQuery.addEventListener("change", resolve, { once: !0 });
}),
visible: (selector = null, requirement) => selector ? new Promise((resolve) => {
let rootMargin = "0px 0px 0px 0px";
if (requirement.indexOf("(") !== -1) {
const rootMarginStart = requirement.indexOf("(") + 1;
rootMargin = requirement.slice(rootMarginStart, -1);
}
const observer = new IntersectionObserver(
(entries) => {
entries[0].isIntersecting && (observer.disconnect(), resolve());
},
{ rootMargin }
);
let elm = document.querySelector(selector);
elm ? observer.observe(elm) : resolve();
}) : Promise.resolve(!0)
};
function loadStrategies(strategy, selector) {
let promises = [];
if (strategy) {
let requirements = strategy.split("|").map((requirement) => requirement.trim()).filter((requirement) => requirement !== "immediate").filter((requirement) => requirement !== "eager");
for (let requirement of requirements) {
if (requirement.startsWith("event")) {
promises.push(strategies.event(requirement));
continue;
}
if (requirement === "idle") {
promises.push(strategies.idle());
continue;
}
if (requirement.startsWith("media")) {
promises.push(strategies.media(requirement));
continue;
}
requirement.startsWith("visible") && promises.push(strategies.visible(selector, requirement));
}
}
return promises;
}
class componentFactory extends HtmxComponent {
constructor() {
super();
__publicField(this, "loaded", []);
this.mount();
}
mount() {
let targetId = htmx.config.currentTargetId ?? "main", target = document.getElementById(targetId);
if (target) {
let components = target.querySelectorAll("[data-component]");
for (let el of components)
this.lazyload(el);
target = null, components = null;
}
}
unmount() {
let targetId = htmx.config.currentTargetId ?? "main", target = document.getElementById(targetId);
if (target) {
for (let i = this.loaded.length - 1; i >= 0; i--) {
let inTarget = target.querySelector(this.loaded[i].selector), inDocument = document.querySelector(this.loaded[i].selector);
(inTarget || !inDocument) && (this.loaded[i].instance.unmount(), this.loaded.splice(i, 1));
}
target = null;
}
}
/**
* Import a component on demand, optionally using a loading strategy
*
* @param el
*/
lazyload(el) {
let component = el.dataset.component, version = el.dataset.version ?? "1", strategy = el.dataset.load ?? null, selector = el.getAttribute("id") ? "#" + el.getAttribute("id") : '[data-component="' + component + '"]', promises = loadStrategies(strategy, selector);
Promise.all(promises).then(() => {
import(
/* @vite-ignore */
`${config.origin}/${config.basePath}/${component}.js?v=${version}`
).then(
(lazyComponent) => {
let instance = new lazyComponent.default(selector);
instance.mounted = !0, this.loaded.push({
name: component,
selector,
instance
});
}
);
});
}
}
let factory;
htmx.defineExtension("components", {
init: function() {
factory = new componentFactory(), factory.mounted = !0;
},
onEvent: function(name, htmxEvent) {
name === "htmx:afterSwap" && (htmx.config.currentTargetId = htmxEvent.target.id, factory.refresh()), name === "htmx:historyRestore" && (htmx.config.currentTargetId = null, factory.refresh());
}
});
})();
(function() {
Element.prototype._addEventListener = Element.prototype.addEventListener, Element.prototype._removeEventListener = Element.prototype.removeEventListener, Element.prototype.addEventListener = function(type, listener, useCapture = !1) {
this._addEventListener(type, listener, useCapture), this.eventListenerList || (this.eventListenerList = {}), this.eventListenerList[type] || (this.eventListenerList[type] = []), this.eventListenerList[type].push({ type, listener, useCapture });
}, Element.prototype.removeEventListener = function(type, listener, useCapture = !1) {
this._removeEventListener(type, listener, useCapture), this.eventListenerList || (this.eventListenerList = {}), this.eventListenerList[type] || (this.eventListenerList[type] = []);
for (let i = 0; i < this.eventListenerList[type].length; i++)
if (this.eventListenerList[type][i].listener === listener && this.eventListenerList[type][i].useCapture === useCapture) {
this.eventListenerList[type].splice(i, 1);
break;
}
this.eventListenerList[type].length === 0 && delete this.eventListenerList[type];
}, Element.prototype.getEventListeners = function(type) {
return this.eventListenerList || (this.eventListenerList = {}), type === void 0 ? this.eventListenerList : this.eventListenerList[type];
}, Element.prototype.clearEventListeners = function(a) {
if (this.eventListenerList || (this.eventListenerList = {}), a === void 0) {
for (let x in this.getEventListeners())
this.clearEventListeners(x);
return;
}
const el = this.getEventListeners(a);
if (el !== void 0)
for (let i = el.length - 1; i >= 0; --i) {
let ev = el[i];
this.removeEventListener(a, ev.listener, ev.useCapture);
}
};
})();
59 changes: 59 additions & 0 deletions dist/history-preserve.min.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
(function() {
let cache = {
now: {},
next: {}
};
function saveToCache(dom, store) {
let markers = dom.querySelectorAll("[hx-history-preserve]");
if (markers)
for (let i = 0; i < markers.length; ++i)
typeof markers[i].id < "u" && (cache[store][markers[i].id] = markers[i].outerHTML);
}
function rotateCache() {
let prunedCache = {};
for (let key in cache.now) {
let el = document.getElementById(key);
el && (prunedCache[key] = cache.now[key]), el = null;
}
cache.now = prunedCache, Object.keys(cache.next).length > 0 && (cache.now = {
...cache.now,
...cache.next
}, cache.next = {});
}
htmx.defineExtension("history-preserve", {
init: function() {
saveToCache(document, "now");
},
onEvent: function(name, event) {
var _a, _b;
if (name === "htmx:beforeSwap") {
let incomingDOM = new DOMParser().parseFromString(
event.detail.xhr.response,
"text/html"
);
incomingDOM && saveToCache(incomingDOM, "next"), incomingDOM = null;
}
if (name === "htmx:historyItemCreated" && event.detail.item.content) {
let cachedDOM = new DOMParser().parseFromString(
event.detail.item.content,
"text/html"
);
for (let key in cache.now) {
let el = cachedDOM.getElementById(key);
el && (el.outerHTML = cache.now[key]), el = null;
}
event.detail.item.content = cachedDOM.body.innerHTML, rotateCache();
}
if (name === "htmx:historyRestore") {
let restored = (_b = (_a = event == null ? void 0 : event.detail) == null ? void 0 : _a.item) == null ? void 0 : _b.content;
if (restored) {
let restoredDOM = new DOMParser().parseFromString(
restored,
"text/html"
);
restoredDOM && saveToCache(restoredDOM, "now");
}
}
}
});
})();
38 changes: 38 additions & 0 deletions index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>Home</title>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width" />
<meta name="htmx-components-config" content='{ "basePath" : "/scripts/components/" }'>
<link rel="stylesheet" href="/styles/styles.css" />
<script
defer
src="https://cdn.jsdelivr.net/gh/bigskysoftware/htmx/src/htmx.min.js"
></script>
<script defer src="/lib/ext/components.js"></script>
<script defer src="/lib/ext/history-preserve.js"></script>
</head>
<body hx-ext="components,history-preserve">
<header id="header">
<nav
hx-boost="true"
hx-target="#main"
hx-select="#main"
hx-swap="outerHTML"
>
<a href="/">Home</a>
<a href="/page2.html">Other page</a>
</nav>
</header>
<main id="main" hx-history-elt>
<h1>Home page</h1>
<section>
<header>
<h3>Example htmx component</h3>
</header>
<div id="message" data-component="hello"></div>
</section>
</main>
</body>
</html>
Loading

0 comments on commit aa8f1fb

Please sign in to comment.