Glimmer DSL for Web

repository·master·Indexed 42 days ago

https://github.com/andyobtiva/glimmer-dsl-web

A Ruby-in-the-browser frontend framework for building Single Page Applications (SPAs) using Ruby instead of JavaScript. Built on Opal, it provides a 'Rails-like' experience featuring a Ruby HTML DSL, unidirectional and bidirectional data-binding, dynamic content rendering, and a component-based architecture with Glimmer Web Components. It includes Glimmer DSL for CSS for styling in Ruby and is designed for seamless integration with Rails 7 and 8.

Tokens
21.6K
Snippets
77
Records
96
Agent score
53%

What's inside Glimmer DSL for Web

  1. Overview of Glimmer DSL for Web

    master

    Glimmer DSL for Web is a Ruby-in-the-Browser frontend framework designed for building Single Page Applications (SPAs) using Ruby instead of JavaScript. It is built on top of Opal (a Ruby-to-JavaScript transpiler) and aims to provide a highly productive, 'Rails-like' experience for frontend development.

    Key Features:

    • Ruby HTML DSL: Write HTML structure and logic in a single language, with full support for SVG.
    • Data-Binding: Supports both Unidirectional (One-Way) data-binding (using <=) and Bidirectional (Two-Way) data-binding (using <=>).
    • Dynamic Rendering: Supports re-rendering HTML content via Content Data-Binding.
    • Modular Design: Utilizes Glimmer Web Components, Component Slots, Attribute Listeners, and Custom Event Listeners.
    • CSS Support: Includes Glimmer DSL for CSS for writing styles in Ruby.
    • Isomorphic Capabilities: Designed to work seamlessly with Rails, allowing Ruby developers to build isomorphic applications where backend and frontend logic can share patterns and even code.
    • Interoperability: Integrates with existing JavaScript libraries and can make HTTP/Ajax requests.
  2. Overview of Glimmer DSL for Web design principles

    master

    Glimmer DSL for Web is built on several core principles designed to provide a 'Rails-like' experience for the frontend:

    • The Ruby Way: Emphasizing idiomatic Ruby and the principle of 'There Is More Than One Way To Do It' (TIMTOWTDI).
    • Convention over Configuration: Using smart defaults and automation to reduce low-level boilerplate.
    • Declarative Syntax: Providing a syntax that visually maps to the DOM hierarchy.
    • Hybrid Code: Allowing the convenient mixing of declarative and imperative code in a single language.
    • Modular Design: Supporting component-based architecture.
  3. Use bidirectional data-binding with the `<=>` operator

    master

    The most minimal and declarative way to sync a View property with a Model attribute is using the <=> operator. This performs bidirectional data-binding: changes in the UI update the model, and changes in the model update the UI.

    This approach significantly reduces the amount of boilerplate code compared to manual observers or oninput listeners.

    class HelloObserver
      include Glimmer::Web::Component
    
      before_render do
        @number_holder = NumberHolder.new
      end
    
      markup {
        div {
          input(type: 'number', min: 0, max: 100) {
            # Bidirectional binding between input value and @number_holder.number
            value <=> [@number_holder, :number]
          }
          input(type: 'range', min: 0, max: 100) {
            # Bidirectional binding between range slider and @number_holder.number
            value <=> [@number_holder, :number]
          }
        }
      }
    end
  4. Implement the Observer pattern with `observe`

    master

    The observe(model, attribute) { ... } keyword allows a View to watch a Model attribute for changes. When the attribute changes, the provided block executes, allowing you to manually update View elements.

    If observe is used inside a Glimmer::Web::Component, the observer is automatically cleaned up when the component or its top-level element is removed.

    Manual Observer Example:

    # Inside a component's after_render block
    observe(@model, :attribute) do
      @element.value = @model.attribute.to_s
    end
    require 'glimmer-dsl-web'
    
    class NumberHolder
      attr_accessor :number
      def initialize; self.number = 50; end
    end
    
    class HelloObserver
      include Glimmer::Web::Component
    
      before_render do
        @number_holder = NumberHolder.new
      end
    
      after_render do
        @number_input.value = @number_holder.number.to_s
        
        observe(@number_holder, :number) do
          number_string = @number_holder.number.to_s
          @number_input.value = number_string unless @number_input.value == number_string
        end
      end
    
      markup {
        div {
          @number_input = input(type: 'number') {
            oninput do
              @number_holder.number = @number_input.value.to_i
            end
          }
        }
      }
    end
    
    Document.ready? { HelloObserver.render }
  5. Use Slots and Default Slots in Components

    master

    Glimmer components support slots to allow consumers to inject content into specific parts of a component's markup.

    • Default Slots: Use default_slot :name to define a primary insertion point. Content nested inside the component call in consumer code is automatically inserted into this slot.
    • Named Slots: Use slot: :name within the markup block to specify where content should be placed.
    • Markup Root Slot: If no slots are specified, content nested under a component is added to the :markup_root_slot (the top-level element in the markup block).
  6. Embed SVG elements in Glimmer components

    master
    Scalable Vector Graphics (SVG) are fully supported. You can embed SVG elements directly within your markup block using the Glimmer HTML DSL syntax. Use all-lowercase names for SVG elements (e.g., svg, circle, path, rect) just as you would with standard HTML elements.
  7. Implement Content Data-Binding for dynamic HTML

    master

    Content Data-Binding allows you to regenerate HTML element content dynamically based on changes to a Model attribute. This is useful for rebuilding lists or complex structures when the underlying data changes.

    Use the content(model, attribute) method to bind the content of an element to a model attribute. You can use the computed_by: option to specify which attributes should trigger a re-render of the content.

    # Re-render content when address_count or street_count changes
    content(@user, :address_count, computed_by: [:address_count, :street_count])
  8. Configure Glimmer element mounting and rendering

    master

    Custom Mounting Point

    By default, Glimmer elements mount to the <body>. To mount to a specific HTML element, pass the parent: option with a CSS selector:

    div(parent: '#app-container') {
      label(class: 'greeting') { 'Hello, World!' }
    }

    Bulk Rendering vs Piecemeal Rendering

    By default, elements are rendered in bulk for better performance. This means you cannot interact with element objects until the full rendering process is complete.

    If you need to interact with elements immediately, you can disable this behavior by passing bulk_render: false to the top-level component or top-level element.

    include Glimmer
    
    Document.ready? do
      div(parent: '#app-container', bulk_render: false) {
        label(class: 'greeting') { 'Hello, World!' }
      }
    end
  9. Use Component Slots to allow content contribution

    master

    Component slots allow consumers of a component to inject content into specific designated areas.

    1. Define a slot: Inside the component's markup block, designate an element as a slot by passing the slot: :slotname option.
    2. Use a default slot: Use default_slot :slot_name to specify where content added directly to the component block should be placed. There is also a built-in :markup_root_slot for content added to the component's markup root.
    3. Consume a slot: A consumer can contribute content by opening a block matching the slot name (e.g., slotname { ... }) inside the component call.
    # Inside the Component definition
    class AddressForm
      include Glimmer::Web::Component
      markup {
        div(slot: :address_header, class: 'header')
        div(slot: :address_footer, class: 'footer')
      }
    end
    
    # Inside the Consumer
    address_form(address: @my_address) {
      address_header {
        h1('My Header')
      }
      address_footer {
        p('My Footer')
      }
    }
  10. Create a Glimmer Web Component

    master

    To build a reusable component, include Glimmer::Web::Component in your class. You can use lifecycle hooks like before_render to initialize state and after_render for setup tasks. The UI is defined within a markup block, and styles are defined within a style block.

    Example structure:

    class MyComponent
      include Glimmer::Web::Component
    
      before_render do
        @state = MyState.new
      end
    
      markup {
        div { 
          # UI elements here 
        }
      }
    
      style {
        # CSS rules here
      }
    end
    
    MyComponent.render
    class ButtonCounter
      include Glimmer::Web::Component
    
      before_render do
        @counter = Counter.new
      end
    
      markup {
        div {
          button {
            inner_text <= [@counter, :count, on_read: ->(value) { "Click To Increment: #{value}  " }]
            onclick { @counter.count += 1 }
          }
        }
      }
    end
    
    ButtonCounter.render
  11. Use the Glimmer HTML DSL for formatted text paragraphs

    master

    The Glimmer HTML DSL provides an intelligent way to build formatted textual paragraphs. When using specific HTML formatting elements like <br>, <strong>, <em>, <i>, <sub>, <sup>, <del>, <ins>, <small>, <mark>, and <span> (when used inside a <p>), the DSL returns String objects instead of nested Ruby objects. This allows you to use standard Ruby string interpolation to compose complex, formatted text easily.

    Formatting elements can accept text content as either their first argument or as a block return value. You can also pass attributes as the first argument before the text content.

    p {"#{strong('Yesterday, ')}Robert suggested adding a new #{em('feature')} to our software product.#{br}}"
    
    # Alternatively using blocks:
    p {"#{strong{'Yesterday, '}}Robert suggested adding a new #{em{'feature'}} to our software product.#{br}}"
    
    # With attributes:
    p {"#{strong('Yesterday, ', class: 'very-string')}Robert suggested adding a new #{em('feature', class: 'very-emphasized')} to our software product.#{br}}"
  12. Regenerate HTML content with Content Data-Binding

    master

    If you need to dynamically rebuild or re-render HTML elements based on changes to a Model attribute, use the content method. This is useful for scenarios like generating a list of forms where the number of forms depends on a specific count attribute in your model.

    Use content(model, :attribute) to define the scope. The block provided to content will be re-executed (re-rendering the content) whenever the specified attribute changes. You can also use the computed_by option to trigger re-renders based on multiple attributes.

    # Re-renders the block whenever @user.address_count changes
    content(@user, :address_count) do
      @user.addresses.each do |address|
        div { 
          # ... render address form ...
        }
      end
    end
    
    # Re-renders based on multiple attributes
    content(@user, computed_by: [:address_count, :street_count]) do
      # ...
    end