-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit aa8f1fb
Showing
14 changed files
with
2,071 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
}; | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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"); | ||
} | ||
} | ||
} | ||
}); | ||
})(); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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> |
Oops, something went wrong.