| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577 |
- using Astralis;
- using Invercargill;
- using Invercargill.DataStructures;
- using Inversion;
- using Spry;
- /**
- * CounterComponent Example
- *
- * Demonstrates using the Spry.Component class with IoC composition and HTMX.
- * Shows how to:
- * - Define Component subclasses with embedded HTML markup
- * - Use spry-outlet elements for dynamic content injection
- * - Use inject<T>() for dependency injection
- * - Use ComponentFactory to create component instances
- * - Use spry-action and spry-target for declarative HTMX interactions
- * - Use handle_action() for action handling
- *
- * Usage: counter-component [port]
- */
- /**
- * AppState - Application state registered as singleton.
- */
- class AppState : Object {
- public int counter { get; set; }
- public int total_changes { get; set; }
- public string last_action { get; set; }
- public DateTime last_update { get; set; }
-
- public AppState() {
- counter = 0;
- total_changes = 0;
- last_action = "Initialized";
- last_update = new DateTime.now_local();
- }
-
- public void increment() {
- counter++;
- total_changes++;
- last_action = "Incremented";
- last_update = new DateTime.now_local();
- }
-
- public void decrement() {
- counter--;
- total_changes++;
- last_action = "Decremented";
- last_update = new DateTime.now_local();
- }
-
- public void reset() {
- counter = 0;
- total_changes++;
- last_action = "Reset";
- last_update = new DateTime.now_local();
- }
- }
- /**
- * CSS content for the counter page.
- * Served as a FastResource for optimal performance with pre-compression.
- */
- private const string COUNTER_CSS = """
- body {
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
- max-width: 700px;
- margin: 0 auto;
- padding: 20px;
- background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
- min-height: 100vh;
- }
- .card {
- background: white;
- border-radius: 12px;
- padding: 25px;
- margin: 15px 0;
- box-shadow: 0 10px 40px rgba(0,0,0,0.2);
- }
- h1 {
- color: #333;
- margin-top: 0;
- }
- .counter-display {
- text-align: center;
- padding: 30px;
- background: #f8f9fa;
- border-radius: 8px;
- margin: 20px 0;
- }
- .counter-value {
- font-size: 72px;
- font-weight: bold;
- color: #667eea;
- line-height: 1;
- }
- .counter-label {
- color: #666;
- font-size: 14px;
- text-transform: uppercase;
- letter-spacing: 2px;
- }
- .button-group {
- display: flex;
- gap: 10px;
- justify-content: center;
- margin: 20px 0;
- }
- button {
- padding: 12px 24px;
- border: none;
- border-radius: 6px;
- cursor: pointer;
- font-size: 16px;
- font-weight: 600;
- transition: all 0.2s;
- }
- button:hover {
- transform: translateY(-2px);
- box-shadow: 0 4px 12px rgba(0,0,0,0.15);
- }
- .btn-primary {
- background: #667eea;
- color: white;
- }
- .btn-primary:hover {
- background: #5a6fd6;
- }
- .btn-danger {
- background: #e74c3c;
- color: white;
- }
- .btn-danger:hover {
- background: #c0392b;
- }
- .btn-success {
- background: #2ecc71;
- color: white;
- }
- .btn-success:hover {
- background: #27ae60;
- }
- .info-grid {
- display: grid;
- grid-template-columns: repeat(2, 1fr);
- gap: 15px;
- margin: 20px 0;
- }
- .info-item {
- background: #f8f9fa;
- padding: 15px;
- border-radius: 6px;
- text-align: center;
- }
- .info-label {
- font-size: 12px;
- color: #666;
- text-transform: uppercase;
- letter-spacing: 1px;
- }
- .info-value {
- font-size: 18px;
- font-weight: 600;
- color: #333;
- margin-top: 5px;
- }
- .status-positive {
- color: #2ecc71 !important;
- }
- .status-negative {
- color: #e74c3c !important;
- }
- .status-zero {
- color: #666 !important;
- }
- code {
- background: #e8e8e8;
- padding: 2px 6px;
- border-radius: 4px;
- font-size: 14px;
- }
- pre {
- background: #263238;
- color: #aed581;
- padding: 15px;
- border-radius: 6px;
- overflow-x: auto;
- font-size: 13px;
- }
- .feature-list {
- margin: 0;
- padding-left: 20px;
- }
- .feature-list li {
- margin: 8px 0;
- color: #555;
- }
- a {
- color: #667eea;
- text-decoration: none;
- }
- a:hover {
- text-decoration: underline;
- }
- """;
- /**
- * InfoItemComponent - A single info grid item.
- */
- class InfoItemComponent : Component {
- public override string markup { get {
- return """
- <div class="info-item">
- <div class="info-label" sid="label"></div>
- <div class="info-value" sid="value"></div>
- </div>
- """;
- }}
- public string label { set {
- this["label"].text_content = value;
- }}
- public string info_value { set {
- this["value"].text_content = value;
- }}
- public string? status_class { set {
- var el = this["value"];
- if (value != null) {
- el.add_class(value);
- }
- }}
- }
- /**
- * InfoGridComponent - Container for info items.
- */
- class InfoGridComponent : Component {
-
- public void set_items(Enumerable<Renderable> items) {
- set_outlet_children("items", items);
- }
-
- public override string markup { get {
- return """
- <div class="info-grid">
- <spry-outlet sid="items"/>
- </div>
- """;
- }}
- }
- /**
- * CounterDisplayComponent - Shows the counter value with HTMX buttons.
- * Uses declarative spry-action and spry-target attributes.
- */
- class CounterDisplayComponent : Component {
- private ComponentFactory factory = inject<ComponentFactory>();
- private AppState app_state = inject<AppState>();
-
- public void set_info_items(Enumerable<Renderable> items) throws Error {
- var info_grid = factory.create<InfoGridComponent>();
- info_grid.set_items(items);
- set_outlet_child("info-grid", info_grid);
- }
-
- public override string markup { get {
- return """
- <div class="card" sid="counter-card">
- <div class="counter-display">
- <div class="counter-label">Current Value</div>
- <div class="counter-value" sid="counter-value">0</div>
- </div>
- <div class="button-group">
- <button type="button" class="btn-danger" sid="decrement-btn" spry-action=":Decrement" spry-target="counter-card" hx-swap="outerHTML">- Decrease</button>
- <button type="button" class="btn-primary" sid="reset-btn" spry-action=":Reset" spry-target="counter-card" hx-swap="outerHTML">Reset</button>
- <button type="button" class="btn-success" sid="increment-btn" spry-action=":Increment" spry-target="counter-card" hx-swap="outerHTML">+ Increase</button>
- </div>
- <spry-outlet sid="info-grid"/>
- </div>
- """;
- }}
-
- public async override void handle_action(string action) throws Error {
- // Update state based on action
- switch (action) {
- case "Increment":
- app_state.increment();
- break;
- case "Decrement":
- app_state.decrement();
- break;
- case "Reset":
- app_state.reset();
- break;
- }
-
- // Update counter value
- var counter_el = this["counter-value"];
- counter_el.text_content = app_state.counter.to_string();
-
- // Update status class
- counter_el.remove_class("status-positive");
- counter_el.remove_class("status-negative");
- counter_el.remove_class("status-zero");
-
- if (app_state.counter > 0) {
- counter_el.add_class("status-positive");
- } else if (app_state.counter < 0) {
- counter_el.add_class("status-negative");
- } else {
- counter_el.add_class("status-zero");
- }
-
- // Build info grid
- var info_grid = factory.create<InfoGridComponent>();
- var items = new Series<Renderable>();
-
- // Determine status
- string status_text;
- string status_class;
- if (app_state.counter > 0) {
- status_text = "Positive";
- status_class = "status-positive";
- } else if (app_state.counter < 0) {
- status_text = "Negative";
- status_class = "status-negative";
- } else {
- status_text = "Zero";
- status_class = "status-zero";
- }
-
- var status_item = factory.create<InfoItemComponent>();
- status_item.label = "Status";
- status_item.info_value = status_text;
- status_item.status_class = status_class;
- items.add(status_item);
-
- var action_item = factory.create<InfoItemComponent>();
- action_item.label = "Last Action";
- action_item.info_value = app_state.last_action;
- items.add(action_item);
-
- var time_item = factory.create<InfoItemComponent>();
- time_item.label = "Last Update";
- time_item.info_value = app_state.last_update.format("%H:%M:%S");
- items.add(time_item);
-
- var changes_item = factory.create<InfoItemComponent>();
- changes_item.label = "Total Changes";
- changes_item.info_value = app_state.total_changes.to_string();
- items.add(changes_item);
-
- info_grid.set_items(items);
- set_outlet_child("info-grid", info_grid);
- }
-
- public int counter { set {
- var counter_el = this["counter-value"];
- counter_el.text_content = value.to_string();
-
- // Update status class
- counter_el.remove_class("status-positive");
- counter_el.remove_class("status-negative");
- counter_el.remove_class("status-zero");
-
- if (value > 0) {
- counter_el.add_class("status-positive");
- } else if (value < 0) {
- counter_el.add_class("status-negative");
- } else {
- counter_el.add_class("status-zero");
- }
- }}
- }
- /**
- * CounterPageComponent - The main page structure.
- */
- class CounterPageComponent : Component {
-
- public void set_counter_section(Renderable component) {
- set_outlet_child("counter-section", component);
- }
-
- public override string markup { get {
- return """
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta charset="UTF-8"/>
- <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
- <title>Component Counter Example</title>
- <link rel="stylesheet" href="/styles.css"/>
- <script spry-res="htmx.js"></script>
- </head>
- <body>
- <div class="card">
- <h1>Component Counter Example</h1>
- <p>This page demonstrates using Spry.Component with IoC composition and HTMX.</p>
- </div>
- <spry-outlet sid="counter-section"/>
- <div class="card">
- <h2>How It Works</h2>
- <p>The counter uses declarative HTMX attributes for dynamic updates:</p>
- <pre><button spry-action=":Increment" spry-target="counter-card" hx-swap="outerHTML">+ Increase</button></pre>
- <h3>Component Features Used:</h3>
- <ul class="feature-list">
- <li><code>spry-outlet</code> - Placeholder for child components</li>
- <li><code>spry-action=":Action"</code> - Declarative HTMX action</li>
- <li><code>spry-target="sid"</code> - Target element by sid</li>
- <li><code>handle_action(action)</code> - Handle HTMX requests</li>
- <li><code>inject<ComponentFactory>()</code> - Factory for creating components</li>
- </ul>
- </div>
- <div class="card">
- <p style="text-align: center; color: #666; margin: 0;">
- Built with <code>Spry.Component</code> + HTMX |
- <a href="/raw">View Raw HTML</a>
- </p>
- </div>
- </body>
- </html>
- """;
- }}
- }
- /**
- * NoticeComponent - A simple notice for the raw view.
- */
- class NoticeComponent : Component {
- public override string markup { get {
- return """
- <div class="card" style="background: #fff3cd; color: #856404; border: 1px solid #ffc107;">
- <p>This is the raw template without dynamic modifications.</p>
- <p><a href="/">Back to dynamic version</a></p>
- </div>
- """;
- }}
- }
- // Home page endpoint - builds the component tree
- class HomePageEndpoint : Object, Endpoint {
- private AppState app_state = inject<AppState>();
- private ComponentFactory factory = inject<ComponentFactory>();
-
- public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
- // Determine status
- string status_text;
- string status_class;
- if (app_state.counter > 0) {
- status_text = "Positive";
- status_class = "status-positive";
- } else if (app_state.counter < 0) {
- status_text = "Negative";
- status_class = "status-negative";
- } else {
- status_text = "Zero";
- status_class = "status-zero";
- }
-
- // Create info items
- var items = new Series<Renderable>();
-
- var status_item = factory.create<InfoItemComponent>();
- status_item.label = "Status";
- status_item.info_value = status_text;
- status_item.status_class = status_class;
- items.add(status_item);
-
- var action_item = factory.create<InfoItemComponent>();
- action_item.label = "Last Action";
- action_item.info_value = app_state.last_action;
- items.add(action_item);
-
- var time_item = factory.create<InfoItemComponent>();
- time_item.label = "Last Update";
- time_item.info_value = app_state.last_update.format("%H:%M:%S");
- items.add(time_item);
-
- var changes_item = factory.create<InfoItemComponent>();
- changes_item.label = "Total Changes";
- changes_item.info_value = app_state.total_changes.to_string();
- items.add(changes_item);
-
- // Create counter display
- var counter_display = factory.create<CounterDisplayComponent>();
- counter_display.counter = app_state.counter;
- counter_display.set_info_items(items);
-
- // Create page and set counter section
- var page = factory.create<CounterPageComponent>();
- page.set_counter_section(counter_display);
-
- // to_result() handles all outlet replacement
- return yield page.to_result();
- }
- }
- // Raw HTML endpoint - shows the unmodified template
- class RawHtmlEndpoint : Object, Endpoint {
- private ComponentFactory factory = inject<ComponentFactory>();
-
- public async HttpResult handle_request(HttpContext context, RouteContext route) throws Error {
- var page = factory.create<CounterPageComponent>();
- var notice = factory.create<NoticeComponent>();
-
- // Add a notice component
- page.set_counter_section(notice);
-
- return yield page.to_result();
- }
- }
- void main(string[] args) {
- int port = args.length > 1 ? int.parse(args[1]) : 8080;
-
- print("═══════════════════════════════════════════════════════════════\n");
- print(" Spry CounterComponent Example (IoC + HTMX)\n");
- print("═══════════════════════════════════════════════════════════════\n");
- print(" Port: %d\n", port);
- print("═══════════════════════════════════════════════════════════════\n");
- print(" Endpoints:\n");
- print(" / - Counter page (Component-based with HTMX)\n");
- print(" /styles.css - CSS stylesheet (FastResource)\n");
- print(" /raw - Raw template (no modifications)\n");
- print("═══════════════════════════════════════════════════════════════\n");
- print(" HTMX Actions (via SpryModule):\n");
- print(" Increment, Decrement, Reset - Dynamic updates\n");
- print("═══════════════════════════════════════════════════════════════\n");
- print("\nPress Ctrl+C to stop the server\n\n");
-
- try {
- var application = new WebApplication(port);
-
- // Register compression components
- application.use_compression();
-
- // Add Spry module for automatic HTMX endpoint registration
- application.add_module<SpryModule>();
-
- // Register application state as singleton
- application.add_singleton<AppState>();
-
- // Register ComponentFactory as scoped
- application.add_scoped<ComponentFactory>();
-
- // Register components as transient (created via factory)
- application.add_transient<InfoItemComponent>();
- application.add_transient<InfoGridComponent>();
- application.add_transient<CounterDisplayComponent>();
- application.add_transient<CounterPageComponent>();
- application.add_transient<NoticeComponent>();
-
- // Register CSS as a FastResource
- application.add_startup_endpoint<FastResource>(new EndpointRoute("/styles.css"), () => {
- try {
- return new FastResource.from_string(COUNTER_CSS)
- .with_content_type("text/css; charset=utf-8")
- .with_default_compressors();
- } catch (Error e) {
- error("Failed to create CSS resource: %s", e.message);
- }
- });
-
- // Register endpoints
- application.add_endpoint<HomePageEndpoint>(new EndpointRoute("/"));
- application.add_endpoint<RawHtmlEndpoint>(new EndpointRoute("/raw"));
-
- application.run();
-
- } catch (Error e) {
- printerr("Error: %s\n", e.message);
- Process.exit(1);
- }
- }
|