wip
This commit is contained in:
parent
a70c24398a
commit
e181f57a91
30 changed files with 2458 additions and 197 deletions
|
|
@ -8,47 +8,46 @@
|
|||
}
|
||||
|
||||
::-webkit-scrollbar {
|
||||
height: 1rem;
|
||||
width: 1rem;
|
||||
height: .5rem;
|
||||
width: .5rem;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: #450a0a;
|
||||
background-color: #002600;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-track {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: #450a0a;
|
||||
background-color: #002600;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(239,68,68,var(--tw-bg-opacity));
|
||||
background-color: #1aff1a;
|
||||
background-color: #00b300;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: #f87171;
|
||||
background-color: #dc2626
|
||||
background-color: #1aff1a;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb {
|
||||
border-radius: 0.25rem;
|
||||
border-radius: 9999px;
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: rgba(239,68,68,var(--tw-bg-opacity));
|
||||
background-color: #00b300;
|
||||
}
|
||||
|
||||
body::-webkit-scrollbar-thumb:hover {
|
||||
--tw-bg-opacity: 1;
|
||||
background-color: #f87171;
|
||||
background-color: #dc2626
|
||||
background-color: #1aff1a;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,67 +1,155 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>
|
||||
{% block title %}HTMX Patterns{% endblock %}
|
||||
</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="og:title" name="og:title" content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="twitter:title" name="twitter:title" content="HTMX Patterns" />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="og:image" name="og:image"
|
||||
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=600&width=1200&scaled_width=1200&scaled_height=600&selectors=" />
|
||||
<meta name="twitter:image" name="twitter:image"
|
||||
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=640&width=1280&scaled_width=1280&scaled_height=640&selectors=" />
|
||||
<meta name="og:image:height" content="640" />
|
||||
<meta name="og:image:width" content="1280" />
|
||||
<meta name="og:url" name="og:url" content="{{ request.url }}" />
|
||||
<meta name="description" name="description" content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="og:description" name="Check if my kids can play outside"
|
||||
content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="twitter:description" name="twitter:description"
|
||||
content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<head>
|
||||
{% block head %}
|
||||
<title>
|
||||
{% block title %}HTMX Patterns{% endblock %}
|
||||
</title>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="og:title" name="og:title" content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="twitter:title" name="twitter:title" content="HTMX Patterns" />
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta name="og:image" name="og:image"
|
||||
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=600&width=1200&scaled_width=1200&scaled_height=600&selectors=" />
|
||||
<meta name="twitter:image" name="twitter:image"
|
||||
content="https://shots.wayl.one/shot/?url={{ request.url }}&height=640&width=1280&scaled_width=1280&scaled_height=640&selectors=" />
|
||||
<meta name="og:image:height" content="640" />
|
||||
<meta name="og:image:width" content="1280" />
|
||||
<meta name="og:url" name="og:url" content="{{ request.url }}" />
|
||||
<meta name="description" name="description" content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="og:description" name="Check if my kids can play outside"
|
||||
content="HTMX Patterns from the hypermedia.systems book" />
|
||||
<meta name="twitter:description" name="twitter:description"
|
||||
content="HTMX Patterns from the hypermedia.systems book" />
|
||||
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('favicon') }}" />
|
||||
<link href="{{ url_for('app_css') }}" rel="stylesheet" />
|
||||
<script src="{{ url_for('htmx') }}"></script>
|
||||
<link href="https://cdn.jsdelivr.net/npm/tailwindcss@2.2.19/dist/tailwind.min.css" rel="stylesheet">
|
||||
{% if DEBUG %}
|
||||
{{ hot_reload.script(url_for('hot-reload') ) | safe }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
<link rel="icon" type="image/x-icon" href="{{ url_for('favicon') }}" />
|
||||
<link href="{{ url_for('app_css') }}" rel="stylesheet" />
|
||||
<script src="{{ url_for('htmx_js') }}"></script>
|
||||
<script src="{{ url_for('ws_js') }}"></script>
|
||||
<script src="{{ url_for('tailwind_js') }}"></script>
|
||||
<script>
|
||||
tailwind.config = {
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
clifford: '#da373d',
|
||||
//terminal
|
||||
'terminal-100': '#e6ffe6',
|
||||
'terminal-200': '#b3ffb3',
|
||||
'terminal-300': '#80ff80',
|
||||
'terminal-400': '#4dff4d',
|
||||
'terminal-500': '#1aff1a',
|
||||
'terminal-600': '#00e600',
|
||||
'terminal-700': '#00b300',
|
||||
'terminal-800': '#008000',
|
||||
'terminal-900': '#004d00',
|
||||
'terminal-950': '#002600',
|
||||
|
||||
{% set links = {
|
||||
"HTMX-PATTERNS": url_for("index"),
|
||||
"Boosted Links": url_for('boosted'),
|
||||
"Infinite Scroll": url_for('infinite'),
|
||||
} %}
|
||||
//
|
||||
// // aqua
|
||||
// 'terminal-50': '#f0f9ff',
|
||||
// 'terminal-100': '#e0f2fe',
|
||||
// 'terminal-200': '#bae6fd',
|
||||
// 'terminal-300': '#7dd3fc',
|
||||
// 'terminal-400': '#38bdf8',
|
||||
// 'terminal-500': '#0ea5e9',
|
||||
// 'terminal-600': '#0284c7',
|
||||
// 'terminal-700': '#0369a1',
|
||||
// 'terminal-800': '#075985',
|
||||
// 'terminal-900': '#0c4a6e',
|
||||
// 'terminal-950': '#042c47',
|
||||
//
|
||||
// //rainbow
|
||||
// 'terminal-50': '#ffecf6',
|
||||
// 'terminal-100': '#fbb6ce',
|
||||
// 'terminal-200': '#f687b3',
|
||||
// 'terminal-300': '#ed64a6',
|
||||
// 'terminal-400': '#d53f8c',
|
||||
// 'terminal-500': '#b83280',
|
||||
// 'terminal-600': '#97266d',
|
||||
// 'terminal-700': '#702459',
|
||||
// 'terminal-800': '#521b41',
|
||||
// 'terminal-900': '#36162e',
|
||||
// 'terminal-950': '#1c0e1f',
|
||||
//
|
||||
// // sherbert lemon
|
||||
// 'terminal-50': '#fffdf1',
|
||||
// 'terminal-100': '#fffbda',
|
||||
// 'terminal-200': '#fff7b3',
|
||||
// 'terminal-300': '#fff280',
|
||||
// 'terminal-400': '#ffea4d',
|
||||
// 'terminal-500': '#ffe01a',
|
||||
// 'terminal-600': '#e6c900',
|
||||
// 'terminal-700': '#b3a300',
|
||||
// 'terminal-800': '#806c00',
|
||||
// 'terminal-900': '#4d4700',
|
||||
// 'terminal-950': '#262300',
|
||||
//
|
||||
// //cobalt
|
||||
// 'terminal-50': '#f0f5f9',
|
||||
// 'terminal-100': '#d9e5f2',
|
||||
// 'terminal-200': '#a6c0e6',
|
||||
// 'terminal-300': '#739bda',
|
||||
// 'terminal-400': '#4b7fce',
|
||||
// 'terminal-400': '#ffea4d',
|
||||
// 'terminal-500': '#315fb6',
|
||||
// 'terminal-600': '#e6c900',
|
||||
// 'terminal-700': '#b3a300',
|
||||
// 'terminal-800': '#1c3b67',
|
||||
// 'terminal-900': '#4d4700',
|
||||
// 'terminal-950': '#0e1d30',
|
||||
//
|
||||
boxShadow: {
|
||||
xlc: "0 0 60px 15px rgba(0, 0, 0, 0.3)",
|
||||
lgc: "0 0 20px 0px #80ff80",
|
||||
},
|
||||
}
|
||||
|
||||
<body
|
||||
class="justify-center items-center min-h-screen bg-gray-900 bg-no-repeat bg-cover bg-gradient-to-b from-pink-950/50 min-w-screen text-shadow-xl text-shadow-zinc-950 w-screen h-screen">
|
||||
<div id="grit"
|
||||
class="absolute top-0 right-0 bottom-0 left-0 justify-center items-center min-w-full bg-repeat bg-cover"
|
||||
style="background-image: url(https://fokais.com/grit.svg), url(https://fokais.com/grit-light.svg); animation: pulse 10s cubic-bezier(0.4, 0, 0.6, 1) infinite; pointer-events: none">
|
||||
</div>
|
||||
<div id="content" class="flex flex-col items-center min-h-screen min-w-screen text-white border-b">
|
||||
<nav
|
||||
class="flex flex-col sm:flex-row flex-wrap w-screen gap-x-8 gap-y-2 justify-center items-center w-full p-4 bg-black border-b-4 border-gray-800 mb-8">
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
{% if DEBUG %}
|
||||
{{ hot_reload.script(url_for('hot-reload') ) | safe }}
|
||||
{% endif %}
|
||||
{% endblock %}
|
||||
</head>
|
||||
|
||||
{% set links = {
|
||||
"HTMX-PATTERNS": url_for("index"),
|
||||
"Boosted Links": url_for('boosted'),
|
||||
"Infinite Scroll": url_for('infinite'),
|
||||
"Toast": url_for('get_toast'),
|
||||
"WebSocket": url_for('websocket_index'),
|
||||
} %}
|
||||
|
||||
<body
|
||||
class="justify-center items-center min-h-screen bg-gray-900 bg-no-repeat bg-cover bg-gradient-to-b from-terminal-950/20 min-w-screen text-shadow-xl text-shadow-zinc-950 w-screen h-screen overflow-x-hidden">
|
||||
|
||||
<div id="grit"
|
||||
class="absolute top-0 right-0 bottom-0 left-0 justify-center items-center min-w-full bg-repeat bg-cover"
|
||||
style="background-image: url(https://fokais.com/grit.svg), url(https://fokais.com/grit-light.svg); animation: pulse 10s cubic-bezier(0.4, 0, 0.6, 1) infinite; pointer-events: none">
|
||||
</div>
|
||||
<div id="content" class="flex flex-col items-center min-h-screen min-w-screen text-white border-b">
|
||||
<nav
|
||||
class="flex flex-col sm:flex-row flex-wrap w-screen gap-x-8 gap-y-2 justify-center items-center w-full p-4 bg-black border-b-2 border-terminal-400 mb-8 font-mono">
|
||||
<!-- <a href="/" class="text-3xl gap-4 font-bold">HTMX PATTERNS</a> -->
|
||||
<!-- <a href="/infinite" class="text-3xl font-bold text-yellow-400">INFINITE</a> -->
|
||||
{% for link, url in links.items() %}
|
||||
<a href="{{ url }}"
|
||||
class="text-xl sm:text-3xl font-bold uppercase {% if not loop.first %}text-yellow-400{% endif %}">{{
|
||||
link
|
||||
}}</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% block content %}
|
||||
{{ body | safe }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
</body>
|
||||
{% for link, url in links.items() %}
|
||||
<a href="{{ url }}"
|
||||
class="text-xl sm:text-xl font-thin uppercase {% if not loop.first %}text-terminal-600{% endif %}">
|
||||
{{ link }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
</nav>
|
||||
{% block content %}
|
||||
{{ body | safe }}
|
||||
{% endblock %}
|
||||
</div>
|
||||
<section id='toast' class="fixed bottom-4 right-4">
|
||||
</section>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
|
|
|
|||
|
|
@ -2,60 +2,62 @@
|
|||
{% block title %}Contact - {{ person_id }} - {{ person.name }}{% endblock %}
|
||||
{% block content %}
|
||||
<h1 id="title"
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
HTMX PATTERNS - BOOSTED
|
||||
</h1>
|
||||
|
||||
<p class='text-3xl font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
|
||||
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
|
||||
Contact - {{ person_id }}
|
||||
</p>
|
||||
|
||||
<p class='text-3xl font-bold mt-0 mb-16 max-w-2xl text-center prose-xl'>
|
||||
{{ person.name }} -
|
||||
{% if person is not none %}
|
||||
<p
|
||||
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
|
||||
<span class='font-normal'>{{ person.name.upper() }}</span> -
|
||||
{{ person.phone_number }}
|
||||
</p>
|
||||
{% else %}
|
||||
<p
|
||||
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
|
||||
Person not found
|
||||
</p>
|
||||
{% endif %}
|
||||
|
||||
<h2 class='text-3xl font-bold mt-0 max-w-xl text-center prose-xl mt-8'>
|
||||
{% macro link(id, text, boosted=false) -%}
|
||||
<a
|
||||
class="
|
||||
{% if id is none %}
|
||||
pointer-events-none bg-terminal-950 text-terminal-900 ring-terminal-900
|
||||
{% else %}
|
||||
bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300
|
||||
{% endif %}
|
||||
cursor-pointer block text-center font-bold py-2 px-4 rounded w-full ring-1
|
||||
"
|
||||
{% if id is not none %}
|
||||
href="{{ url_for('boosted', id=id) }}"
|
||||
{% endif %}
|
||||
{% if boosted %}
|
||||
hx-boost="true"
|
||||
{% endif %}>
|
||||
{{ text }}
|
||||
</a>
|
||||
{%- endmacro %}
|
||||
|
||||
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
|
||||
Boosted Links
|
||||
</h2>
|
||||
|
||||
<div class='flex flex-row gap-4'>
|
||||
{% if prev_id is not none %}
|
||||
<a href="{{ url_for('boosted', id=prev_id) }}"
|
||||
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded" hx-boost='true'>
|
||||
Previous
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="pointer-events-none bg-gray-500 text-white font-bold py-2 px-4 rounded">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('boosted', id=next_id) }}"
|
||||
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded" hx-boost='true'>
|
||||
Next
|
||||
</a>
|
||||
{{ link(prev_id, 'Previous', boosted=True) }}
|
||||
{{ link(next_id, 'Next', boosted=True) }}
|
||||
</div>
|
||||
|
||||
<h2 class='text-3xl font-bold mt-0 max-w-xl text-center prose-xl mt-8'>
|
||||
<h2 class='text-3xl font-light mt-0 max-w-xl text-center prose-xl mt-8 text-terminal-500'>
|
||||
Normal Links
|
||||
</h2>
|
||||
|
||||
<div class='flex flex-row gap-4'>
|
||||
{% if prev_id is not none %}
|
||||
<a href="{{ url_for('boosted', id=prev_id) }}"
|
||||
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
Previous
|
||||
</a>
|
||||
{% else %}
|
||||
<a class="pointer-events-none bg-gray-500 text-white font-bold py-2 px-4 rounded">
|
||||
Previous
|
||||
</a>
|
||||
{% endif %}
|
||||
|
||||
<a href="{{ url_for('boosted', id=next_id) }}"
|
||||
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
Next
|
||||
</a>
|
||||
{{ link(prev_id, 'Previous', boosted=False) }}
|
||||
{{ link(next_id, 'Next', boosted=False) }}
|
||||
</div>
|
||||
{% endblock %}
|
||||
|
|
|
|||
|
|
@ -1,19 +1,34 @@
|
|||
{% extends "base.html" %}
|
||||
{% block content %}
|
||||
<h1 id="title"
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
HTMX PATTERNS
|
||||
</h1>
|
||||
|
||||
<p class='text-3xl px-2 font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
|
||||
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
|
||||
A collection of HTMX patterns
|
||||
</p>
|
||||
|
||||
<p class='text-xl px-2 mt-0 mb-16 max-w-xl prose-xl text-terminal-500 font-extralight'>
|
||||
These are patterns that I have written based on content from the <a
|
||||
href="https://hypermedia.systems/htmx-in-action/">hypermedia.systems</a>
|
||||
book. There is lots of code duplication as each pattern is meant to be standalone.
|
||||
</p>
|
||||
|
||||
<p class='text-xl px-2 mt-0 mb-16 max-w-xl prose-xl text-terminal-500 font-extralight'>
|
||||
I currently make use of htmx with fastapi, sqlmodel, sqlite, and tailwindcss
|
||||
for many of my projects. These patterns are here to serve for reference to
|
||||
myself implemented using this stack in the most pure way possible to remain
|
||||
simple and understandable. Sometimes real projects get complicated and are
|
||||
hard to bring in new features correctly. This is a playground with completely
|
||||
separate routers, models, and templates for each project.
|
||||
</p>
|
||||
|
||||
<ul id="patterns" class="flex flex-col sm:grid grid-cols-3 gap-4">
|
||||
{% for link, url in links.items() %}
|
||||
<li class='w-full'>
|
||||
<a href="{{ url }}"
|
||||
class="cursor-pointer block bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded w-full">
|
||||
class="cursor-pointer block text-center bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 font-bold py-2 px-4 rounded w-full ring-terminal-300 ring-1 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30">
|
||||
{{ link }}
|
||||
</a>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -2,22 +2,22 @@
|
|||
{% block title %}Contacts List{% endblock %}
|
||||
{% block content %}
|
||||
<h1 id="title"
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-black text-transparent bg-clip-text bg-gradient-to-r from-red-600 via-pink-500 to-yellow-400 ring-red-500 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
HTMX PATTERNS - INFINITE
|
||||
</h1>
|
||||
|
||||
<p class='text-3xl font-bold mt-0 mb-16 max-w-xl text-center prose-xl'>
|
||||
<p class='text-3xl px-2 mt-0 mb-16 max-w-xl text-center prose-xl text-terminal-500 font-extralight'>
|
||||
Contacts List
|
||||
</p>
|
||||
|
||||
<ul id="persons" class="flex flex-col gap-4 mb-16 px-4 sm:px-0">
|
||||
<ul id="persons" class="flex flex-col gap-4 mb-16 px-4 sm:px-0 container max-w-xl">
|
||||
{% include "infinite/persons_partial.html" %}
|
||||
</ul>
|
||||
|
||||
<div id='persons-loading' class='spinner mb-24 animate-bounce'>
|
||||
<p class='text-xl prose-xl'>loading more contacts</p>
|
||||
<p class='text-xl prose-xl text-terminal-700'>loading more contacts</p>
|
||||
<p class='text-xl prose-xl'>
|
||||
<em class='text-red-500'>with simulated slow down</em>
|
||||
<em class='text-terminal-500'>with simulated slow down</em>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,16 +1,14 @@
|
|||
{% for person in persons %}
|
||||
<li
|
||||
{% if loop.last %}
|
||||
<li hx-get="{{ url_for('infinite', page=next_page) }}" hx-trigger="intersect once" hx-target="#persons"
|
||||
hx-swap='beforeend' hx-indicator="#persons-loading"
|
||||
class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
|
||||
{{ person.name }} -
|
||||
{{ person.phone_number }}
|
||||
</li>
|
||||
{% else %}
|
||||
<li class="cursor-pointer bg-red-500 hover:bg-red-600 text-white font-bold py-2 px-4 rounded">
|
||||
{{ person.name }} -
|
||||
{{ person.phone_number }}
|
||||
</li>
|
||||
hx-get="{{ url_for('infinite', page=next_page) }}"
|
||||
hx-trigger="intersect once"
|
||||
hx-target="#persons"
|
||||
hx-swap='beforeend'
|
||||
hx-indicator="#persons-loading"
|
||||
{% endif %}
|
||||
class='max-w-xl container text-xl font-light px-2 mt-0 mb-4 py-2 text-center text-terminal-500 bg-terminal-950 prose-xl ring-1 ring-terminal-500 rounded-xl shadow-lg shadow-terminal-300/20'>
|
||||
<span class='font-normal'>{{ person.name.upper() }}</span> -
|
||||
{{ person.phone_number }}
|
||||
</li>
|
||||
{% endfor %}
|
||||
|
|
|
|||
62
templates/tailwind.js
Normal file
62
templates/tailwind.js
Normal file
File diff suppressed because one or more lines are too long
0
templates/tailwindcss.js
Normal file
0
templates/tailwindcss.js
Normal file
9
templates/toast/toast-message.html
Normal file
9
templates/toast/toast-message.html
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
<div
|
||||
class='text-terminal-500 font-extralight text-center text-3xl my-2 bg-terminal-950 ring-5 ring-terminal-500 rounded-lg px-4 py-2'
|
||||
hx-delete="{{ url_for('null') }}"
|
||||
hx-trigger='load delay:5s'
|
||||
hx-swap='outerHTML'
|
||||
>
|
||||
{{ message }}
|
||||
</div>
|
||||
|
||||
17
templates/toast/toast.html
Normal file
17
templates/toast/toast.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Toast{% endblock %}
|
||||
{% block content %}
|
||||
<h1 id="title"
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
HTMX PATTERNS - TOAST
|
||||
</h1>
|
||||
<button
|
||||
class="text-3xl my-24 px-8 py-2 rounded rounded-xl bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300"
|
||||
hx-post="{{ url_for('post_toast') }}"
|
||||
hx-target="#toast"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
|
||||
{% endblock %}
|
||||
26
templates/websocket/index.html
Normal file
26
templates/websocket/index.html
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}WebSocket{% endblock %}
|
||||
{% block content %}
|
||||
<h1 id="title"
|
||||
class="inline-block px-4 pb-0 mx-auto mb-0 text-center text-6xl sm:text-8xl font-thin text-transparent bg-clip-text bg-gradient-to-r from-terminal-600 via-terminal-500 to-terminal-900 ring-red-700 text-shadow-xl text-shadow-zinc-950 ring-5 leading-none">
|
||||
HTMX PATTERNS - WEBSOCKET
|
||||
</h1>
|
||||
<button
|
||||
class="text-3xl my-24 px-8 py-2 rounded rounded-xl bg-terminal-950 hover:bg-terminal-900 hover:text-terminal-400 text-terminal-500 shadow-lg shadow-terminal-300/20 hover:shadow-terminal-300/30 ring-terminal-300"
|
||||
hx-post="{{ url_for('post_toast') }}"
|
||||
hx-target="#toast"
|
||||
hx-swap="beforeend"
|
||||
>
|
||||
Submit
|
||||
</button>
|
||||
<div hx-ext="ws" ws-connect="/websocket/">
|
||||
<div id="notifications"></div>
|
||||
<div id="chat_room">
|
||||
...
|
||||
</div>
|
||||
<form id="form" ws-send>
|
||||
<input name="chat_message" class='text-black'>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
||||
476
templates/ws.js
Normal file
476
templates/ws.js
Normal file
|
|
@ -0,0 +1,476 @@
|
|||
/*
|
||||
WebSockets Extension
|
||||
============================
|
||||
This extension adds support for WebSockets to htmx. See /www/extensions/ws.md for usage instructions.
|
||||
*/
|
||||
|
||||
(function () {
|
||||
|
||||
/** @type {import("../htmx").HtmxInternalApi} */
|
||||
var api;
|
||||
|
||||
htmx.defineExtension("ws", {
|
||||
|
||||
/**
|
||||
* init is called once, when this extension is first registered.
|
||||
* @param {import("../htmx").HtmxInternalApi} apiRef
|
||||
*/
|
||||
init: function (apiRef) {
|
||||
|
||||
// Store reference to internal API
|
||||
api = apiRef;
|
||||
|
||||
// Default function for creating new EventSource objects
|
||||
if (!htmx.createWebSocket) {
|
||||
htmx.createWebSocket = createWebSocket;
|
||||
}
|
||||
|
||||
// Default setting for reconnect delay
|
||||
if (!htmx.config.wsReconnectDelay) {
|
||||
htmx.config.wsReconnectDelay = "full-jitter";
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* onEvent handles all events passed to this extension.
|
||||
*
|
||||
* @param {string} name
|
||||
* @param {Event} evt
|
||||
*/
|
||||
onEvent: function (name, evt) {
|
||||
var parent = evt.target || evt.detail.elt;
|
||||
|
||||
switch (name) {
|
||||
|
||||
// Try to close the socket when elements are removed
|
||||
case "htmx:beforeCleanupElement":
|
||||
|
||||
var internalData = api.getInternalData(parent)
|
||||
|
||||
if (internalData.webSocket) {
|
||||
internalData.webSocket.close();
|
||||
}
|
||||
return;
|
||||
|
||||
// Try to create websockets when elements are processed
|
||||
case "htmx:beforeProcessNode":
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-connect"), function (child) {
|
||||
ensureWebSocket(child)
|
||||
});
|
||||
forEach(queryAttributeOnThisOrChildren(parent, "ws-send"), function (child) {
|
||||
ensureWebSocketSend(child)
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
function splitOnWhitespace(trigger) {
|
||||
return trigger.trim().split(/\s+/);
|
||||
}
|
||||
|
||||
function getLegacyWebsocketURL(elt) {
|
||||
var legacySSEValue = api.getAttributeValue(elt, "hx-ws");
|
||||
if (legacySSEValue) {
|
||||
var values = splitOnWhitespace(legacySSEValue);
|
||||
for (var i = 0; i < values.length; i++) {
|
||||
var value = values[i].split(/:(.+)/);
|
||||
if (value[0] === "connect") {
|
||||
return value[1];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureWebSocket creates a new WebSocket on the designated element, using
|
||||
* the element's "ws-connect" attribute.
|
||||
* @param {HTMLElement} socketElt
|
||||
* @returns
|
||||
*/
|
||||
function ensureWebSocket(socketElt) {
|
||||
|
||||
// If the element containing the WebSocket connection no longer exists, then
|
||||
// do not connect/reconnect the WebSocket.
|
||||
if (!api.bodyContains(socketElt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Get the source straight from the element's value
|
||||
var wssSource = api.getAttributeValue(socketElt, "ws-connect")
|
||||
|
||||
if (wssSource == null || wssSource === "") {
|
||||
var legacySource = getLegacyWebsocketURL(socketElt);
|
||||
if (legacySource == null) {
|
||||
return;
|
||||
} else {
|
||||
wssSource = legacySource;
|
||||
}
|
||||
}
|
||||
|
||||
// Guarantee that the wssSource value is a fully qualified URL
|
||||
if (wssSource.indexOf("/") === 0) {
|
||||
var base_part = location.hostname + (location.port ? ':' + location.port : '');
|
||||
if (location.protocol === 'https:') {
|
||||
wssSource = "wss://" + base_part + wssSource;
|
||||
} else if (location.protocol === 'http:') {
|
||||
wssSource = "ws://" + base_part + wssSource;
|
||||
}
|
||||
}
|
||||
|
||||
var socketWrapper = createWebsocketWrapper(socketElt, function () {
|
||||
return htmx.createWebSocket(wssSource)
|
||||
});
|
||||
|
||||
socketWrapper.addEventListener('message', function (event) {
|
||||
if (maybeCloseWebSocketSource(socketElt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
var response = event.data;
|
||||
if (!api.triggerEvent(socketElt, "htmx:wsBeforeMessage", {
|
||||
message: response,
|
||||
socketWrapper: socketWrapper.publicInterface
|
||||
})) {
|
||||
return;
|
||||
}
|
||||
|
||||
api.withExtensions(socketElt, function (extension) {
|
||||
response = extension.transformResponse(response, null, socketElt);
|
||||
});
|
||||
|
||||
var settleInfo = api.makeSettleInfo(socketElt);
|
||||
var fragment = api.makeFragment(response);
|
||||
|
||||
if (fragment.children.length) {
|
||||
var children = Array.from(fragment.children);
|
||||
for (var i = 0; i < children.length; i++) {
|
||||
api.oobSwap(api.getAttributeValue(children[i], "hx-swap-oob") || "true", children[i], settleInfo);
|
||||
}
|
||||
}
|
||||
|
||||
api.settleImmediately(settleInfo.tasks);
|
||||
api.triggerEvent(socketElt, "htmx:wsAfterMessage", { message: response, socketWrapper: socketWrapper.publicInterface })
|
||||
});
|
||||
|
||||
// Put the WebSocket into the HTML Element's custom data.
|
||||
api.getInternalData(socketElt).webSocket = socketWrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {Object} WebSocketWrapper
|
||||
* @property {WebSocket} socket
|
||||
* @property {Array<{message: string, sendElt: Element}>} messageQueue
|
||||
* @property {number} retryCount
|
||||
* @property {(message: string, sendElt: Element) => void} sendImmediately sendImmediately sends message regardless of websocket connection state
|
||||
* @property {(message: string, sendElt: Element) => void} send
|
||||
* @property {(event: string, handler: Function) => void} addEventListener
|
||||
* @property {() => void} handleQueuedMessages
|
||||
* @property {() => void} init
|
||||
* @property {() => void} close
|
||||
*/
|
||||
/**
|
||||
*
|
||||
* @param socketElt
|
||||
* @param socketFunc
|
||||
* @returns {WebSocketWrapper}
|
||||
*/
|
||||
function createWebsocketWrapper(socketElt, socketFunc) {
|
||||
var wrapper = {
|
||||
socket: null,
|
||||
messageQueue: [],
|
||||
retryCount: 0,
|
||||
|
||||
/** @type {Object<string, Function[]>} */
|
||||
events: {},
|
||||
|
||||
addEventListener: function (event, handler) {
|
||||
if (this.socket) {
|
||||
this.socket.addEventListener(event, handler);
|
||||
}
|
||||
|
||||
if (!this.events[event]) {
|
||||
this.events[event] = [];
|
||||
}
|
||||
|
||||
this.events[event].push(handler);
|
||||
},
|
||||
|
||||
sendImmediately: function (message, sendElt) {
|
||||
if (!this.socket) {
|
||||
api.triggerErrorEvent()
|
||||
}
|
||||
if (!sendElt || api.triggerEvent(sendElt, 'htmx:wsBeforeSend', {
|
||||
message: message,
|
||||
socketWrapper: this.publicInterface
|
||||
})) {
|
||||
this.socket.send(message);
|
||||
sendElt && api.triggerEvent(sendElt, 'htmx:wsAfterSend', {
|
||||
message: message,
|
||||
socketWrapper: this.publicInterface
|
||||
})
|
||||
}
|
||||
},
|
||||
|
||||
send: function (message, sendElt) {
|
||||
if (this.socket.readyState !== this.socket.OPEN) {
|
||||
this.messageQueue.push({ message: message, sendElt: sendElt });
|
||||
} else {
|
||||
this.sendImmediately(message, sendElt);
|
||||
}
|
||||
},
|
||||
|
||||
handleQueuedMessages: function () {
|
||||
while (this.messageQueue.length > 0) {
|
||||
var queuedItem = this.messageQueue[0]
|
||||
if (this.socket.readyState === this.socket.OPEN) {
|
||||
this.sendImmediately(queuedItem.message, queuedItem.sendElt);
|
||||
this.messageQueue.shift();
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
init: function () {
|
||||
if (this.socket && this.socket.readyState === this.socket.OPEN) {
|
||||
// Close discarded socket
|
||||
this.socket.close()
|
||||
}
|
||||
|
||||
// Create a new WebSocket and event handlers
|
||||
/** @type {WebSocket} */
|
||||
var socket = socketFunc();
|
||||
|
||||
// The event.type detail is added for interface conformance with the
|
||||
// other two lifecycle events (open and close) so a single handler method
|
||||
// can handle them polymorphically, if required.
|
||||
api.triggerEvent(socketElt, "htmx:wsConnecting", { event: { type: 'connecting' } });
|
||||
|
||||
this.socket = socket;
|
||||
|
||||
socket.onopen = function (e) {
|
||||
wrapper.retryCount = 0;
|
||||
api.triggerEvent(socketElt, "htmx:wsOpen", { event: e, socketWrapper: wrapper.publicInterface });
|
||||
wrapper.handleQueuedMessages();
|
||||
}
|
||||
|
||||
socket.onclose = function (e) {
|
||||
// If socket should not be connected, stop further attempts to establish connection
|
||||
// If Abnormal Closure/Service Restart/Try Again Later, then set a timer to reconnect after a pause.
|
||||
if (!maybeCloseWebSocketSource(socketElt) && [1006, 1012, 1013].indexOf(e.code) >= 0) {
|
||||
var delay = getWebSocketReconnectDelay(wrapper.retryCount);
|
||||
setTimeout(function () {
|
||||
wrapper.retryCount += 1;
|
||||
wrapper.init();
|
||||
}, delay);
|
||||
}
|
||||
|
||||
// Notify client code that connection has been closed. Client code can inspect `event` field
|
||||
// to determine whether closure has been valid or abnormal
|
||||
api.triggerEvent(socketElt, "htmx:wsClose", { event: e, socketWrapper: wrapper.publicInterface })
|
||||
};
|
||||
|
||||
socket.onerror = function (e) {
|
||||
api.triggerErrorEvent(socketElt, "htmx:wsError", { error: e, socketWrapper: wrapper });
|
||||
maybeCloseWebSocketSource(socketElt);
|
||||
};
|
||||
|
||||
var events = this.events;
|
||||
Object.keys(events).forEach(function (k) {
|
||||
events[k].forEach(function (e) {
|
||||
socket.addEventListener(k, e);
|
||||
})
|
||||
});
|
||||
},
|
||||
|
||||
close: function () {
|
||||
this.socket.close()
|
||||
}
|
||||
}
|
||||
|
||||
wrapper.init();
|
||||
|
||||
wrapper.publicInterface = {
|
||||
send: wrapper.send.bind(wrapper),
|
||||
sendImmediately: wrapper.sendImmediately.bind(wrapper),
|
||||
queue: wrapper.messageQueue
|
||||
};
|
||||
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
/**
|
||||
* ensureWebSocketSend attaches trigger handles to elements with
|
||||
* "ws-send" attribute
|
||||
* @param {HTMLElement} elt
|
||||
*/
|
||||
function ensureWebSocketSend(elt) {
|
||||
var legacyAttribute = api.getAttributeValue(elt, "hx-ws");
|
||||
if (legacyAttribute && legacyAttribute !== 'send') {
|
||||
return;
|
||||
}
|
||||
|
||||
var webSocketParent = api.getClosestMatch(elt, hasWebSocket)
|
||||
processWebSocketSend(webSocketParent, elt);
|
||||
}
|
||||
|
||||
/**
|
||||
* hasWebSocket function checks if a node has webSocket instance attached
|
||||
* @param {HTMLElement} node
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function hasWebSocket(node) {
|
||||
return api.getInternalData(node).webSocket != null;
|
||||
}
|
||||
|
||||
/**
|
||||
* processWebSocketSend adds event listeners to the <form> element so that
|
||||
* messages can be sent to the WebSocket server when the form is submitted.
|
||||
* @param {HTMLElement} socketElt
|
||||
* @param {HTMLElement} sendElt
|
||||
*/
|
||||
function processWebSocketSend(socketElt, sendElt) {
|
||||
var nodeData = api.getInternalData(sendElt);
|
||||
var triggerSpecs = api.getTriggerSpecs(sendElt);
|
||||
triggerSpecs.forEach(function (ts) {
|
||||
api.addTriggerHandler(sendElt, ts, nodeData, function (elt, evt) {
|
||||
if (maybeCloseWebSocketSource(socketElt)) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @type {WebSocketWrapper} */
|
||||
var socketWrapper = api.getInternalData(socketElt).webSocket;
|
||||
var headers = api.getHeaders(sendElt, api.getTarget(sendElt));
|
||||
var results = api.getInputValues(sendElt, 'post');
|
||||
var errors = results.errors;
|
||||
var rawParameters = results.values;
|
||||
var expressionVars = api.getExpressionVars(sendElt);
|
||||
var allParameters = api.mergeObjects(rawParameters, expressionVars);
|
||||
var filteredParameters = api.filterValues(allParameters, sendElt);
|
||||
|
||||
var sendConfig = {
|
||||
parameters: filteredParameters,
|
||||
unfilteredParameters: allParameters,
|
||||
headers: headers,
|
||||
errors: errors,
|
||||
|
||||
triggeringEvent: evt,
|
||||
messageBody: undefined,
|
||||
socketWrapper: socketWrapper.publicInterface
|
||||
};
|
||||
|
||||
if (!api.triggerEvent(elt, 'htmx:wsConfigSend', sendConfig)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (errors && errors.length > 0) {
|
||||
api.triggerEvent(elt, 'htmx:validation:halted', errors);
|
||||
return;
|
||||
}
|
||||
|
||||
var body = sendConfig.messageBody;
|
||||
if (body === undefined) {
|
||||
var toSend = Object.assign({}, sendConfig.parameters);
|
||||
if (sendConfig.headers)
|
||||
toSend['HEADERS'] = headers;
|
||||
body = JSON.stringify(toSend);
|
||||
}
|
||||
|
||||
socketWrapper.send(body, elt);
|
||||
|
||||
if (evt && api.shouldCancel(evt, elt)) {
|
||||
evt.preventDefault();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* getWebSocketReconnectDelay is the default easing function for WebSocket reconnects.
|
||||
* @param {number} retryCount // The number of retries that have already taken place
|
||||
* @returns {number}
|
||||
*/
|
||||
function getWebSocketReconnectDelay(retryCount) {
|
||||
|
||||
/** @type {"full-jitter" | ((retryCount:number) => number)} */
|
||||
var delay = htmx.config.wsReconnectDelay;
|
||||
if (typeof delay === 'function') {
|
||||
return delay(retryCount);
|
||||
}
|
||||
if (delay === 'full-jitter') {
|
||||
var exp = Math.min(retryCount, 6);
|
||||
var maxDelay = 1000 * Math.pow(2, exp);
|
||||
return maxDelay * Math.random();
|
||||
}
|
||||
|
||||
logError('htmx.config.wsReconnectDelay must either be a function or the string "full-jitter"');
|
||||
}
|
||||
|
||||
/**
|
||||
* maybeCloseWebSocketSource checks to the if the element that created the WebSocket
|
||||
* still exists in the DOM. If NOT, then the WebSocket is closed and this function
|
||||
* returns TRUE. If the element DOES EXIST, then no action is taken, and this function
|
||||
* returns FALSE.
|
||||
*
|
||||
* @param {*} elt
|
||||
* @returns
|
||||
*/
|
||||
function maybeCloseWebSocketSource(elt) {
|
||||
if (!api.bodyContains(elt)) {
|
||||
api.getInternalData(elt).webSocket.close();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* createWebSocket is the default method for creating new WebSocket objects.
|
||||
* it is hoisted into htmx.createWebSocket to be overridden by the user, if needed.
|
||||
*
|
||||
* @param {string} url
|
||||
* @returns WebSocket
|
||||
*/
|
||||
function createWebSocket(url) {
|
||||
var sock = new WebSocket(url, []);
|
||||
sock.binaryType = htmx.config.wsBinaryType;
|
||||
return sock;
|
||||
}
|
||||
|
||||
/**
|
||||
* queryAttributeOnThisOrChildren returns all nodes that contain the requested attributeName, INCLUDING THE PROVIDED ROOT ELEMENT.
|
||||
*
|
||||
* @param {HTMLElement} elt
|
||||
* @param {string} attributeName
|
||||
*/
|
||||
function queryAttributeOnThisOrChildren(elt, attributeName) {
|
||||
|
||||
var result = []
|
||||
|
||||
// If the parent element also contains the requested attribute, then add it to the results too.
|
||||
if (api.hasAttribute(elt, attributeName) || api.hasAttribute(elt, "hx-ws")) {
|
||||
result.push(elt);
|
||||
}
|
||||
|
||||
// Search all child nodes that match the requested attribute
|
||||
elt.querySelectorAll("[" + attributeName + "], [data-" + attributeName + "], [data-hx-ws], [hx-ws]").forEach(function (node) {
|
||||
result.push(node)
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @param {T[]} arr
|
||||
* @param {(T) => void} func
|
||||
*/
|
||||
function forEach(arr, func) {
|
||||
if (arr) {
|
||||
for (var i = 0; i < arr.length; i++) {
|
||||
func(arr[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})();
|
||||
|
||||
Loading…
Add table
Add a link
Reference in a new issue