Isomorphic-crystal : Updates :

Yet another approach to developing GUI’s for Desktop apps using Crystal and Webview

Specificity of this variant :

  1. The underlying HTML is generated using “Blueprint”, which allows for a Vue-like developing experience (while writing no HTML or JS).
  2. The code that generates the interface maps the actual visual layout of the interface elements.
  3. Basically, Blueprint plays the role of JSX in React.

Excerpt:

class Form
  include Blueprint::HTML

  def initialize(@state : Int32 = state)
  end

  private def blueprint
    # Global style
    style { picoCSS }

    # Local widget Style
    style { <<-CSS
      article {
        padding: 20px;
      }
      input {
        border: 1px solid black;
      }
      CSS
    }

    # Widget description

    # Links (routing)
    a href: "/hello" {"[Hello]"} 
    a href: "/#{ROOT}" {"[Root]"} 
    
    br 
    br

    div {
      article {
        form action: "/get_name", method: "POST" {

          label for: "#{Fname}" { "First name" }
          input type: "text", id: Fname, name: Fname

          br

          label for: "#{Lname}" { "Last name" }
          input type: "text", id: Lname, name: Lname

          br
          br

          input type: "submit", value: "Get name (and increment state)"
        }

        br

        form action: "/reset", method: "POST" {
          input type: "submit", value: "Reset state"
        }
      }

      article {
        label { "#{@state}" }
      }
    }
  end
end
3 Likes

Although I’m not a fan of JSX and similar, I acknowledge that many like it.

This positively enriches the Crystal ecosystem and, at the same time, is a creative, functional and useful approach for the target audience.

1 Like

It is

  1. Expressive / intuitive
  2. Concise.
  3. Its code structure matches the widgets hierarchy / layout of the app (not unlike HTML tags).
  4. No extra parser or language needed: its all Crystal.
1 Like

New version:

  • Improved CSS.
  • Modular GUI development example (as in Svelte, for instance).
2 Likes

How does it compare to Lucky framework template methods?
You also mentioned front-end frameworks like Svelte. Is this reactive on the front-end, just like Svelte, or uses backend rendering?

It is Backend rendering, but the widgets can be built in a modular manner.

It is Backend rendering, but one could create separately custom HTML tags with Svelte (for instance) that would do some client-side stuff or alternatively use a prefabricated HTML/JS/Css object
(widget) which does its own client-side stuff (like, e.g. an editor widget written in javascript)…

Isomorphic Crystal does Ajax, now !

Using forms, POST and reloading an entire is no longer required, widgets (elements) can modify themselves, using Ajax.

Ajax requests are made possible using HTMX.

HTMX uses JS in a transparent manner (behind the scenes), not explicit JS coding is required.

Snippet :

#########
# Form 1
#########

class Form1
  include Blueprint::HTML

  def initialize(@state : State = state)
  end

  private def blueprint
    # including 'Links'widget
    render Links.new

    # Global style
    style { picoCSS }

    # Local widget Style
    style { LocalCSS }

    script src: "https://unpkg.com/htmx.org@1.9.6"

    div class: "main" {
      br
      article {
        input type: "text"
      }
      br
      article {
        # first form (uses Ajax, via HTMX) : Only redraws the target element, NOT the enrire page.
        form {
          input "type": "submit", "value": "Increment counter, version 1", "hx-post": "/increment_2", "hx-target": "#counter"
        }
        br
        # second form : Classical form redraws the entire page
        form action: "/increment_1", method: "POST" {
          input type: "submit", value: "increment counter, version 2"
        }
        br
        # classical HTML form
        form action: "/reset_1", method: "POST" {
          input type: "submit", value: "Reset counter"
        }
      }
      article {
        label id: "counter" { "State : #{@state.count}" }
        label { "State : #{@state.lname}" }
        label { "State : #{@state.fname}" }
      }
    }
    div class: "message" { "state demo" }
  end
end

2 Likes

With Ajax and actions defined alongside forms, the code looks more and more like Vue.js or React.js, except the entire app code uses Crystal both on front-end and back-end.

The complete example Is in the repo:

class Form1
  include Blueprint::HTML

  def initialize(@state : State = state)
  end

  private def blueprint
    # including 'Links'widget
    render Links.new

    div class: "main" {
      br
      article {
        input type: "text"
      }
      br
      article {
        # first form (uses Ajax, via HTMX) : Only redraws the target element, NOT the enrire page.

        form {
          button "hx-post": "/increment_2", "hx-target": "#counter" {
            "Increment counter, version 1"
          }
        }

        br
        # second form (Classical form) redraws the entire page
        form action: "/increment_1", method: "POST" {
          input type: "submit", value: "increment counter, version 2"
        }
        br
        # classical HTML form
        form action: "/reset_1", method: "POST" {
          input type: "submit", value: "Reset counter"
        }
      }
      article {
        label id: "counter" { "State : #{@state.count}" }
        label { "State : #{@state.lname}" }
        label { "State : #{@state.fname}" }
      }
    }
    div class: "message" { "state demo" }
  end
end

# Form action which refreshes the entire form 
# (deprecated)
def increment_1(env : HTTP::Server::Context, state : State)
  state.count = state.count + 1
  Form1.new(state).to_html
end

# Form action which refreshes only one targeted element of the form 
# (recommended)
def increment_2(env : HTTP::Server::Context, state : State)
  state.count = state.count + 1
  "<div> State: #{state.count} </div>"
end
1 Like

With these macros there’s now one single source of truth as far as the registration of actions is concerned:

    # Action defined client-side
    post "/increment_1" do |env|
      increment_1(env, state)
    end

becomes simply:

    register(env, state, increment_1)

and

        form {
          button "hx-post": "increment_2", "hx-target": "#counter" {
            "Increment counter, version 1"
          }
        }

becomes:

        form {
          button "hx-post": action(increment_2), "hx-target": "#counter" {
            "Increment counter, version 1"
          }
        }
1 Like

I have started separating the code of Isomorphic Crystal into a library (shard) and a repo with examples using said library:

  1. GitHub - serge-hulne/isomorphic-crystal-lib: GUI lib for Crystal-lang allowing for developing simple desktop apps using Crystal only. Based on Blueprint, Webview, Pico CSS, HTMX and Crystal lang.
  2. GitHub - serge-hulne/isomorphic-crystal-examples: Examples for isomorphic crystal
1 Like

Added second example : A minimalist counter using w3css for its styling:

# file app.cr

# ===========================
# Widgets / layout
# ===========================

require "blueprint/html"

require "gui/pico"
require "gui/macros"

require "./css"
require "./register"
require "./state"
require "./w3css"

class App
  include Blueprint::HTML

  def initialize(@state = STATE)
  end

  private def blueprint
    style { W3CSS }
    style { LocalCSS }

    # HTMX
    script src: HTMX

    # GUI Definition (uses Blueprint)
    div class: "main w3-panel" {
     
      div {
        input type: "text", value: "This editable text should not change when the counter changes", class: "w3-input"
      }
      
      div  {
        button class: "w3-btn w3-circle w3-blue w3-margin w3-large w3-monospace",
          "hx-post": action(increment),
          "hx-target": "#counter" {
          "+"
        }
        button class: "w3-btn w3-circle w3-green w3-margin w3-large w3-monospace",
          "hx-post": action(decrement),
          "hx-target": "#counter" {
          "-"
        }
      }      
    }

    div class: "message" {
      label id: "counter" { "State : #{@state.count}" }
    }

  end # method Blueprint
end # Class

def increment(env : HTTP::Server::Context, state : State)
  state.count = state.count + 1
  "<div> State: #{state.count} </div>"
end

def decrement(env : HTTP::Server::Context, state : State)
  state.count = state.count - 1
  "<div> State: #{state.count} </div>"
end

1 Like

Both ex1 and ex2 work, however, Increment stops at 2
Decrements then count to -1
Increment doesn’t increment any more after one decrement

When starting with decrement, I get to -2, then increment get to +1, after that neither works any more

1 Like

Thank you for the feedback.

I will see how it behaves under Linux, I have been testing it only on Mac so far.

I’ll keep you posted.

I am using Linux

I tried on Linux (Ubuntu).

The server (back-end) of the app seems to work correctly. but not the front-end when using HTMX.

There seems to be a problem when using HTMX in Webview under Linux.

This can be demonstrated by starting one of the two apps on the command-line and then (while the app is still running) point a browser to the URL used by the server (the default is http://127.0.0.1:3000/root or http://localhost/root )

The app front-end behaves as expected in a regular browser.

It seems therefore that HTMX does not play well with Webview.

HTMX must be using some clever javaScript that Webview does not provide or does not interpret correctly.

I will look into it and try to debug the HTMX issue with Webview under Linux.

Fixes in the meantime:

  • Not use Ajax (i.e. not use HTMX), use the formulation withe regular HTTP requests.
  • Alternatively : Use a regular browser as a front-end.

Yes, I can confirm that it works as expected when using my browser (Firefox)

I also get these messages (you probably also):
➜ isomorphic-crystal-examples git:(main) ✗ ./ex2

http://127.0.0.1:3000/root

[development] Kemal is ready to lead at http://127.0.0.1:3000

(ex2:715843): Gtk-WARNING **: 16:29:20.534: GTK+ module /usr/lib/x86_64-linux-gnu//gtk-2.0/modules/libgail.so cannot be loaded.

GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same process is not supported.

Gtk-Message: 16:29:20.535: Not loading module “atk-bridge”: The functionality is provided by GTK natively. Please try to not load it.

(WebKitWebProcess:715862): Gtk-WARNING **: 16:29:20.642: GTK+ module /usr/lib/x86_64-linux-gnu//gtk-2.0/modules/libgail.so cannot be loaded.

GTK+ 2.x symbols detected. Using GTK+ 2.x and GTK+ 3 in the same process is not supported.

Gtk-Message: 16:29:20.642: Not loading module “atk-bridge”: The functionality is provided by GTK natively. Please try to not load it.

HTMX is awesome! and maintain actively, maybe create a issue there is more quickly than debug self.

On second thought, In think I made a mistake in my code.

:slight_smile:

I will issue an amended version of the examples during the weekend.

Thanks!

I think I made a mistake in my code.

:slight_smile:

I will issue an amended version of these first two examples during the weekend.

Thanks!

1 Like