feat: implement a simple loader
Play with htmx events
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
use std::io;
|
use std::{io, time::Duration};
|
||||||
|
|
||||||
use actix_files as fs;
|
use actix_files as fs;
|
||||||
use actix_web::{get, middleware, App, HttpRequest, HttpServer, Responder};
|
use actix_web::{get, middleware, App, HttpRequest, HttpServer, Responder, rt::time::sleep};
|
||||||
use actix_web_lab::respond::Html;
|
use actix_web_lab::respond::Html;
|
||||||
use askama::Template;
|
use askama::Template;
|
||||||
|
|
||||||
@@ -44,9 +44,11 @@ async fn users(req: HttpRequest) -> actix_web::Result<impl Responder> {
|
|||||||
email: "joe.doe@foo.baz".into(),
|
email: "joe.doe@foo.baz".into(),
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
match req.headers().get("hx-request") {
|
match req.headers().get("hx-request") {
|
||||||
Some(_) => {
|
Some(_) => {
|
||||||
// Render the hypermedia fragment.
|
// Render the hypermedia fragment.
|
||||||
|
sleep(Duration::from_millis(1000)).await;
|
||||||
Ok(Html(Fragment { users }.render().expect("Valid template")))
|
Ok(Html(Fragment { users }.render().expect("Valid template")))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
|
|||||||
@@ -1,3 +1,89 @@
|
|||||||
h1 {
|
h1 {
|
||||||
color: blue;
|
color: blue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-loader] {
|
||||||
|
position: fixed;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
z-index: 1000;
|
||||||
|
height: .2em;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
background-color: blue;
|
||||||
|
transition: all 500ms;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 15em 1fr;
|
||||||
|
grid-template-rows: 3em 1fr 3em;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
"nav header"
|
||||||
|
"nav main"
|
||||||
|
"nav footer";
|
||||||
|
|
||||||
|
margin: 0;
|
||||||
|
height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1080px) {
|
||||||
|
body {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: 3em 1fr 3em;
|
||||||
|
|
||||||
|
grid-template-areas:
|
||||||
|
"header"
|
||||||
|
"main"
|
||||||
|
"footer";
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
position: fixed;
|
||||||
|
left: 0;
|
||||||
|
top: 0;
|
||||||
|
bottom: 0;
|
||||||
|
width: 15em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body > * {
|
||||||
|
padding: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
hgroup * {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
nav {
|
||||||
|
grid-area: nav;
|
||||||
|
|
||||||
|
background-color: lightgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
header {
|
||||||
|
grid-area: header;
|
||||||
|
|
||||||
|
background-color: orange;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-evenly;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
grid-area: main;
|
||||||
|
|
||||||
|
background-color: darkgray;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer {
|
||||||
|
grid-area: footer;
|
||||||
|
|
||||||
|
background-color: grey;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|||||||
98
static/js/loader.js
Normal file
98
static/js/loader.js
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/**
|
||||||
|
* A loading bar that hooks into htmx events to automatically indicate the
|
||||||
|
* progression of pending requests.
|
||||||
|
*
|
||||||
|
* ```html
|
||||||
|
* <div data-loader></div>
|
||||||
|
* ```
|
||||||
|
* Only the first loader will be used.
|
||||||
|
*/
|
||||||
|
(function () {
|
||||||
|
var MAX_INCREMENT = 20;
|
||||||
|
var MIN_INCREMENT = 3;
|
||||||
|
var ADVANCE_UNTIL = 90;
|
||||||
|
|
||||||
|
var pending = 0;
|
||||||
|
var increment = MAX_INCREMENT;
|
||||||
|
var timeout = null;
|
||||||
|
var interval = null;
|
||||||
|
|
||||||
|
function getLoader() {
|
||||||
|
return document.querySelector("[data-loader]");
|
||||||
|
}
|
||||||
|
|
||||||
|
function clamp(x, a, b) {
|
||||||
|
return Math.max(a, Math.min(x, b));
|
||||||
|
}
|
||||||
|
|
||||||
|
function progress(loader) {
|
||||||
|
loader.hidden = false;
|
||||||
|
var current = parseInt(loader.dataset.loader);
|
||||||
|
|
||||||
|
if (current < ADVANCE_UNTIL) {
|
||||||
|
current = Math.min(current + increment, ADVANCE_UNTIL);
|
||||||
|
loader.dataset.loader = current;
|
||||||
|
loader.style.right = 100 - current + "%";
|
||||||
|
increment = clamp(Math.floor(increment * 0.65), MIN_INCREMENT, MAX_INCREMENT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function start() {
|
||||||
|
pending++;
|
||||||
|
|
||||||
|
if (pending > 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
increment = MAX_INCREMENT;
|
||||||
|
|
||||||
|
var loader = getLoader();
|
||||||
|
if (!loader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Give the request a reasonable time to complete before triggering the
|
||||||
|
// loader.
|
||||||
|
timeout = setTimeout(function () {
|
||||||
|
loader.dataset.loader = 0;
|
||||||
|
loader.style.right = "100%";
|
||||||
|
loader.hidden = false;
|
||||||
|
|
||||||
|
// Increment the loader state until it reaches the threshold.
|
||||||
|
// Trickery and lies!
|
||||||
|
interval = setInterval(function () {
|
||||||
|
progress(loader)
|
||||||
|
}, 100);
|
||||||
|
}, 250);
|
||||||
|
}
|
||||||
|
|
||||||
|
function finish() {
|
||||||
|
pending--;
|
||||||
|
|
||||||
|
if (pending <= 0) {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
clearInterval(interval);
|
||||||
|
|
||||||
|
var loader = getLoader();
|
||||||
|
if (!loader) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
loader.style.right = 0;
|
||||||
|
timeout = setTimeout(function () {
|
||||||
|
loader.hidden = true;
|
||||||
|
timeout = setTimeout(function () {
|
||||||
|
loader.dataset.loader = 0;
|
||||||
|
loader.style.right = "100%";
|
||||||
|
}, 500);
|
||||||
|
}, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener("htmx:beforeRequest", start);
|
||||||
|
addEventListener("htmx:afterRequest", finish);
|
||||||
|
})();
|
||||||
@@ -7,15 +7,34 @@
|
|||||||
<link rel="stylesheet" href="/static/css/style.css" />
|
<link rel="stylesheet" href="/static/css/style.css" />
|
||||||
|
|
||||||
<script src="/static/js/htmx.min.js"></script>
|
<script src="/static/js/htmx.min.js"></script>
|
||||||
<script src="/static/js/script.js"></script>
|
<script src="/static/js/script.js" defer></script>
|
||||||
|
<script src="/static/js/loader.js" defer></script>
|
||||||
|
|
||||||
<title>{% block title %}Hello htmx!{% endblock %}</title>
|
<title>{% block title %}Hello htmx!{% endblock %}</title>
|
||||||
|
|
||||||
{% block head %}{% endblock %}
|
{% block head %}{% endblock %}
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div data-loader hidden></div>
|
||||||
|
<nav data-nav>
|
||||||
|
<hgroup>
|
||||||
|
<h2>htmx</h2>
|
||||||
|
<p>Very cool</p>
|
||||||
|
</hgroup>
|
||||||
|
<ol>
|
||||||
|
<li><a href="/">Home</a></li>
|
||||||
|
<li><a href="/user">User</a></li>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
<header>
|
||||||
|
<div>Item 1</div>
|
||||||
|
<div>Item 2</div>
|
||||||
|
<div>Item 3</div>
|
||||||
|
</header>
|
||||||
<main id="content">
|
<main id="content">
|
||||||
{% block content %}{% endblock %}
|
{% block content %}{% endblock %}
|
||||||
</main>
|
</main>
|
||||||
</body>
|
<footer>
|
||||||
|
<small>* Not a pretty site...</small>
|
||||||
|
</footer>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
Reference in New Issue
Block a user