a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
1/**
2 * Demo module for showcasing VoltX features and voltx.css styling
3 *
4 * This module creates the entire demo structure programmatically using DOM APIs, then uses charge() to mount it declaratively.
5 */
6
7import { charge } from "$core/charge";
8import { isSignal } from "$core/shared";
9import { initNavigationListener, setRouterMode } from "$plugins/navigate";
10import { persistPlugin } from "$plugins/persist";
11import { scrollPlugin } from "$plugins/scroll";
12import { shiftPlugin } from "$plugins/shift";
13import { surgePlugin } from "$plugins/surge";
14import { urlPlugin } from "$plugins/url";
15import type { Signal } from "$types/volt";
16import { registerPlugin } from "$volt";
17import { createAnimationsSection } from "./sections/animations";
18import { createCssSection } from "./sections/css";
19import { createFormsSection } from "./sections/forms";
20import { createHomeSection } from "./sections/home";
21import { createInteractivitySection } from "./sections/interactivity";
22import { createPluginsSection } from "./sections/plugins";
23import { createReactivitySection } from "./sections/reactivity";
24import * as dom from "./utils";
25
26setRouterMode(import.meta.env.DEV ? "history" : "hash");
27
28registerPlugin("persist", persistPlugin);
29registerPlugin("scroll", scrollPlugin);
30registerPlugin("url", urlPlugin);
31registerPlugin("surge", surgePlugin);
32registerPlugin("shift", shiftPlugin);
33
34/**
35 * Helper functions for DOM operations that can't be expressed declaratively
36 * These are added to the scope so they can be called from data-volt-on-* attributes
37 */
38const helpers = {
39 openDialog(id: string) {
40 const dialog = document.querySelector(`#${id}`) as HTMLDialogElement;
41 if (dialog) {
42 dialog.showModal();
43 }
44 },
45
46 closeDialog(id: string) {
47 const dialog = document.querySelector(`#${id}`) as HTMLDialogElement;
48 if (dialog) {
49 dialog.close();
50 }
51 },
52
53 scrollToTop() {
54 window.scrollTo({ top: 0, behavior: "smooth" });
55 },
56
57 scrollToSection(id: string) {
58 const element = document.querySelector(`#${id}`);
59 if (element) {
60 element.scrollIntoView({ behavior: "smooth" });
61 }
62 },
63
64 handleFormSubmit(event: Event) {
65 event.preventDefault();
66 const form = event.target as HTMLFormElement;
67 const formData = new FormData(form);
68 const data = Object.fromEntries(formData.entries());
69 console.log("Form submitted:", data);
70 alert("Form submitted! Check console for data.");
71 },
72};
73
74const buildNav = () =>
75 dom.nav(
76 null,
77 dom.a({ "data-volt-navigate": "", href: "/" }, "Home"),
78 " | ",
79 dom.a({ "data-volt-navigate": "", href: "/css" }, "CSS"),
80 " | ",
81 dom.a({ "data-volt-navigate": "", href: "/interactivity" }, "Interactivity"),
82 " | ",
83 dom.a({ "data-volt-navigate": "", href: "/forms" }, "Forms"),
84 " | ",
85 dom.a({ "data-volt-navigate": "", href: "/reactivity" }, "Reactivity"),
86 " | ",
87 dom.a({ "data-volt-navigate": "", href: "/plugins" }, "Plugins"),
88 " | ",
89 dom.a({ "data-volt-navigate": "", href: "/animations" }, "Animations"),
90 );
91
92/**
93 * Get the current page from the URL (supports both hash and history routing)
94 */
95function getCurrentPageFromPath(): string {
96 if (globalThis.location.hash) {
97 const hash = globalThis.location.hash.slice(1);
98 if (hash === "/" || hash === "") return "home";
99 return hash.slice(1);
100 }
101
102 const path = globalThis.location.pathname;
103 if (path === "/" || path === "") return "home";
104 return path.slice(1);
105}
106
107function buildDemoStructure(): HTMLElement {
108 const initialState = {
109 currentPage: getCurrentPageFromPath(),
110 message: "Welcome to the VoltX.js Demo",
111 count: 0,
112 formData: { name: "", email: "", bio: "", country: "us", newsletter: false, plan: "free" },
113 todos: [{ id: 1, text: "Learn VoltX.js", done: false }, { id: 2, text: "Build an app", done: false }, {
114 id: 3,
115 text: "Ship to production",
116 done: false,
117 }],
118 newTodoText: "",
119 todoIdCounter: 4,
120 showAdvanced: false,
121 isActive: true,
122 isHighlighted: false,
123 dialogMessage: "",
124 dialogInput: "",
125 persistedCount: 0,
126 scrollPosition: 0,
127 urlParam: "",
128 showFade: false,
129 showSlideDown: false,
130 showScale: false,
131 showBlur: false,
132 showSlowFade: false,
133 showDelayedSlide: false,
134 showGranular: false,
135 showCombined: false,
136 triggerBounce: 0,
137 triggerShake: 0,
138 triggerFlash: 0,
139 triggerTripleBounce: 0,
140 triggerLongShake: 0,
141 spinningGear: true,
142 };
143
144 return dom.div(
145 {
146 "data-volt": "",
147 "data-volt-state": JSON.stringify(initialState),
148 "data-volt-computed:doubled": "count * 2",
149 "data-volt-computed:active-todos": "todos.filter(t => !t.done)",
150 "data-volt-computed:completed-todos": "todos.filter(t => t.done)",
151 },
152 dom.header(
153 null,
154 dom.h1({ "data-volt-text": "message" }, "Loading..."),
155 dom.p(
156 null,
157 "A comprehensive demo showcasing VoltX.js reactive framework and Volt CSS classless styling.",
158 dom.small(
159 null,
160 "This demo demonstrates both the framework's reactive capabilities and the elegant, semantic styling of Volt CSS. No CSS classes needed!",
161 ),
162 buildNav(),
163 ),
164 ),
165 dom.el(
166 "main",
167 null,
168 dom.div({ "data-volt-if": "currentPage === 'home'" }, createHomeSection()),
169 dom.div({ "data-volt-if": "currentPage === 'css'" }, createCssSection()),
170 dom.div({ "data-volt-if": "currentPage === 'interactivity'" }, createInteractivitySection()),
171 dom.div({ "data-volt-if": "currentPage === 'forms'" }, createFormsSection()),
172 dom.div({ "data-volt-if": "currentPage === 'reactivity'" }, createReactivitySection()),
173 dom.div({ "data-volt-if": "currentPage === 'plugins'" }, createPluginsSection()),
174 dom.div({ "data-volt-if": "currentPage === 'animations'" }, createAnimationsSection()),
175 ),
176 dom.footer(
177 null,
178 dom.p(
179 null,
180 "Built with ",
181 dom.a({ href: "https://github.com/stormlightlabs/volt" }, "VoltX.js"),
182 " - A lightweight, reactive hypermedia framework",
183 ),
184 dom.p(null, "This demo showcases both VoltX's reactive features and VoltX.css' classless styling."),
185 ),
186 );
187}
188
189export function setupDemo() {
190 const app = document.querySelector("#app");
191 if (!app) {
192 console.error("App container not found");
193 return;
194 }
195
196 const demoStructure = buildDemoStructure();
197 app.append(demoStructure);
198
199 const chargeResult = charge();
200 const cleanupNav = initNavigationListener();
201
202 const rootScope = chargeResult.roots[0]?.scope;
203 if (!rootScope) {
204 console.error("Failed to get root scope from charge result");
205 return;
206 }
207
208 rootScope.$helpers = helpers;
209
210 const handleNavigate = (event: Event) => {
211 const customEvent = event as CustomEvent;
212 const url = customEvent.detail?.url || "";
213
214 let page = "home";
215 if (url.startsWith("#")) {
216 const hash = url.slice(1);
217 page = hash === "/" || hash === "" ? "home" : hash.slice(1);
218 } else if (url) {
219 page = url === "/" || url === "" ? "home" : url.slice(1);
220 } else {
221 page = getCurrentPageFromPath();
222 }
223
224 const currentPageSignal = rootScope.currentPage as Signal<string>;
225 if (currentPageSignal && isSignal(currentPageSignal)) {
226 currentPageSignal.set(page);
227 }
228
229 window.scrollTo({ top: 0, behavior: "instant" });
230 };
231
232 const handlePopstate = () => {
233 const page = getCurrentPageFromPath();
234 const currentPageSignal = rootScope.currentPage as Signal<string>;
235 if (currentPageSignal && isSignal(currentPageSignal)) {
236 currentPageSignal.set(page);
237 }
238 };
239
240 const handleScroll = () => {
241 const scrollPositionSignal = rootScope.scrollPosition as Signal<number>;
242 if (scrollPositionSignal && isSignal(scrollPositionSignal)) {
243 scrollPositionSignal.set(window.scrollY);
244 }
245 };
246
247 globalThis.addEventListener("volt:navigate", handleNavigate);
248 globalThis.addEventListener("volt:popstate", handlePopstate);
249 window.addEventListener("scroll", handleScroll);
250
251 return () => {
252 chargeResult.cleanup();
253 cleanupNav();
254 globalThis.removeEventListener("volt:navigate", handleNavigate);
255 globalThis.removeEventListener("volt:popstate", handlePopstate);
256 window.removeEventListener("scroll", handleScroll);
257 };
258}