Glimmer DSL for LibUI Bidirectional Data-Binding
Tags: ruby, desktop, gui, libui
Published: 2021-12-01 22:25:37

[Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui) v0.4.11 just wrapped up a very important new feature that has been rolling out gradually over the last few 0.4.x releases: [Bidirectional Data-Binding](https://github.com/AndyObtiva/glimmer-dsl-libui#data-binding)

I am including below an explanation of the [Observer Pattern](https://github.com/AndyObtiva/glimmer-dsl-libui#observer-pattern) and [Data-Binding](https://github.com/AndyObtiva/glimmer-dsl-libui#data-binding) in [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui).

Afterwards, I conclude by sharing an example that takes full advantage of the data-binding features in [Glimmer DSL for LibUI](https://github.com/AndyObtiva/glimmer-dsl-libui), a contact management example app called [Form Table](https://github.com/AndyObtiva/glimmer-dsl-libui#form-table).

Happy [Glimmering](https://github.com/AndyObtiva/glimmer-dsl-libui)!

### Observer Pattern

The [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) (a.k.a. Observer Pattern) is fundamental to building GUIs (Graphical User Interfaces) following the [MVC (Model View Controller) Architectural Pattern](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) or any of its variations like [MVP (Model View Presenter)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter). In the original Smalltalk-MVC, the View observes the Model for changes and updates itself accordingly.

![MVC - Model View Controller](https://camo.githubusercontent.com/0651c35cc70c5ba422876936733b07667368835bbd3cdbf5e706151410bdea04/68747470733a2f2f7777772e7265736561726368676174652e6e65742f70726f66696c652f44616e6e792d5765796e732f7075626c69636174696f6e2f3236393330333631312f6669677572652f666967322f41533a38353831333330353634363238363640313538313630363237323830302f536d616c6c74616c6b38302d4d56432d7061747465726e2d566965772d616e642d436f6e74726f6c6c65722d776f726b2d61732d612d706169722d616c6c6f77696e672d7468652d757365722d746f2d696e7465726163742e70706d)

[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports the [Observer Design Pattern](https://en.wikipedia.org/wiki/Observer_pattern) via the `observe(model, attribute_or_key=nil)` keyword, which can observe `Object` models with attributes, `Hash`es with keys, and `Array`s. It automatically enhances objects as needed to support automatically notifying observers of changes via `observable#notify_observers(attribute_or_key = nil)` method:
- `Object` becomes `Glimmer::DataBinding::ObservableModel`, which supports observing specified `Object` model attributes.
- `Hash` becomes `Glimmer::DataBinding::ObservableHash`, which supports observing all `Hash` keys or a specific `Hash` key
- `Array` becomes `Glimmer::DataBinding::ObservableArray`, which supports observing `Array` changes like those done with `push`, `<<`, `delete`, and `map!` methods (all mutation methods).

Example:

```ruby
  observe(person, :name) do |new_name|
    @name_label.text = new_name
  end
```

That observes a person's name attribute for changes and updates the name `label` `text` property accordingly.

[Learn about Glimmer's Observer Pattern capabilities and options in more detail at the Glimmer project page.](https://github.com/AndyObtiva/glimmer#data-binding-library)

See examples of the `observe` keyword at [Color The Circles](https://github.com/AndyObtiva/glimmer-dsl-libui#color-the-circles), [Method-Based Custom Keyword](https://github.com/AndyObtiva/glimmer-dsl-libui#method-based-custom-keyword), [Snake](https://github.com/AndyObtiva/glimmer-dsl-libui#snake), and [Tetris](https://github.com/AndyObtiva/glimmer-dsl-libui#tetris).

### Data-Binding

[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports both bidirectional (two-way) data-binding and unidirectional (one-way) data-binding.

Data-binding enables writing very expressive, terse, and declarative code to synchronize View properties with Model attributes without writing many lines or pages of imperative code doing the same thing, increasing productivity immensely.

Data-binding automatically takes advantage of the [Observer Pattern](https://github.com/AndyObtiva/glimmer-dsl-libui#observer-pattern) behind the scenes and is very well suited to declaring View property data sources piecemeal. On the other hand, explicit use of the [Observer Pattern](https://github.com/AndyObtiva/glimmer-dsl-libui#observer-pattern) is sometimes more suitable when needing to make multiple View updates upon a single Model attribute change.

Data-binding supports utilizing the [MVP (Model View Presenter)](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93presenter) flavor of [MVC](https://en.wikipedia.org/wiki/Model%E2%80%93view%E2%80%93controller) by observing both the View and a Presenter for changes and updating the opposite side upon encountering them. This enables writing more decoupled cleaner code that keeps View code and Model code disentangled and highly maintainable. For example, check out the Snake game presenters for [Grid](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/presenter/grid.rb) and [Cell](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/presenter/cell.rb), which act as proxies for the actual Snake game models [Snake](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/model/snake.rb) and [Apple](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake/model/apple.rb), mediating synchronization of data between them and the [Snake View GUI](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/snake.rb).

![MVP](https://www.researchgate.net/profile/Gilles-Perrouin/publication/320249584/figure/fig8/AS:668260987068418@1536337243385/Model-view-presenter-architecture.png)

[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports bidirectional (two-way) data-binding of the following controls/properties via the `<=>` operator (indicating data is moving in both directions between View and Model):
- `checkbox`: `checked`
- `check_menu_item`: `checked`
- `color_button`: `color`
- `combobox`: `selected`, `selected_item`
- `date_picker`: `time`
- `date_time_picker`: `time`
- `editable_combobox`: `text`
- `entry`: `text`
- `font_button`: `font`
- `multiline_entry`: `text`
- `non_wrapping_multiline_entry`: `text`
- `radio_buttons`: `selected`
- `radio_menu_item`: `checked`
- `search_entry`: `text`
- `slider`: `value`
- `spinbox`: `value`
- `table`: `cell_rows` (explicit data-binding by using `<=>` and [implicit data-binding](https://github.com/AndyObtiva/glimmer-dsl-libui#table-api) by assigning value directly)
- `time_picker`: `time`

Example of bidirectional data-binding:

```ruby
entry {
  text <=> [contract, :legal_text]
}
```

That is data-binding a contract's legal text to an `entry` `text` property.

Another example of bidirectional data-binding with an option:

```ruby
entry {
  text <=> [self, :entered_text, after_write: ->(text) {puts text}]
}
```

That is data-binding `entered_text` attribute on `self` to `entry` `text` property and printing text after write to the model.

[Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) supports unidirectional (one-way) data-binding of any control/shape/attributed-string property via the `<=` operator (indicating data is moving from the right side, which is the Model, to the left side, which is the GUI View object).

Example of unidirectional data-binding:

```ruby
square(0, 0, CELL_SIZE) {
  fill <= [@grid.cells[row][column], :color]
}
```

That is data-binding a grid cell color to a `square` shape's `fill` property. That means if the `color` attribute of the grid cell is updated, the `fill` property of the `square` shape is automatically updated accordingly.

Another Example of unidirectional data-binding with an option:

```ruby
window {
  title <= [@game, :score, on_read: -> (score) {"Glimmer Snake (Score: #{@game.score})"}]
}
```

That is data-binding the `window` `title` property to the `score` attribute of a `@game`, but converting on read from the Model to a `String`.

To summarize the data-binding API:
- `view_property <=> [model, attribute, *read_or_write_options]`: Bidirectional (two-way) data-binding to Model attribute accessor
- `view_property <= [model, attribute, *read_only_options]`: Unidirectional (one-way) data-binding to Model attribute reader

This is also known as the [Glimmer Shine](https://github.com/AndyObtiva/glimmer-dsl-swt/blob/master/docs/reference/GLIMMER_GUI_DSL_SYNTAX.md#shine) syntax for data-binding, a [Glimmer](https://github.com/AndyObtiva/glimmer)-only unique innovation that takes advantage of [Ruby](https://www.ruby-lang.org/en/)'s highly expressive syntax and malleable DSL support.

Data-bound model attribute can be:
- **Direct:** `Symbol` representing attribute reader/writer (e.g. `[person, :name`])
- **Nested:** `String` representing nested attribute path (e.g. `[company, 'address.street']`). That results in "nested data-binding"
- **Indexed:** `String` containing array attribute index (e.g. `[customer, 'addresses[0].street']`). That results in "indexed data-binding"

Data-binding options include:
- `before_read {|value| ...}`: performs an operation before reading data from Model to update the View.
- `on_read {|value| ...}`: converts value read from Model to update the View.
- `after_read {|converted_value| ...}`: performs an operation after read from Model and updating the View.
- `before_write {|value| ...}`: performs an operation before writing data to Model from View.
- `on_write {|value| ...}`: converts value read from View to update the Model.
- `after_write {|converted_value| ...}`: performs an operation after writing to Model from View.
- `computed_by attribute` or `computed_by [attribute1, attribute2, ...]`: indicates model attribute is computed from specified attribute(s), thus updated when they are updated (see in [Login example version 2](https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/login2.rb)). That is known as "computed data-binding".

Note that with both `on_read` and `on_write` converters, you could pass a `Symbol` representing the name of a method on the value object to invoke.

Example:

```ruby
entry {
  text <=> [product, :price, on_read: :to_s, on_write: :to_i]
}
```

Data-binding gotchas:
- Never data-bind a control property to an attribute on the same view object with the same exact name (e.g. binding `entry` `text` property to `self` `text` attribute) as it would conflict with it. Instead, data-bind view property to an attribute with a different name on the view object or with the same name, but on a presenter or model object (e.g. data-bind `entry` `text` to `self` `legal_text` attribute or to `contract` model `text` attribute)
- Data-binding a property utilizes the control's listener associated with the property (e.g. `on_changed` for `entry` `text`), so you cannot hook into the listener directly anymore as that would negate data-binding. Instead, you can add an `after_write: ->(val) {}` option to perform something on trigger of the control listener instead.

Learn more from data-binding usage in [Login](https://github.com/AndyObtiva/glimmer-dsl-libui#login) (4 data-binding versions), [Basic Entry](https://github.com/AndyObtiva/glimmer-dsl-libui#basic-entry), [Form](https://github.com/AndyObtiva/glimmer-dsl-libui#form), [Form Table](https://github.com/AndyObtiva/glimmer-dsl-libui#form-table) (5 data-binding versions), [Method-Based Custom Keyword](https://github.com/AndyObtiva/glimmer-dsl-libui#method-based-custom-keyword), [Snake](https://github.com/AndyObtiva/glimmer-dsl-libui#snake) and [Tic Tac Toe](https://github.com/AndyObtiva/glimmer-dsl-libui#tic_tac_toe) examples.

### Form Table Example

[https://github.com/AndyObtiva/glimmer-dsl-libui/blob/master/examples/form_table.rb](examples/form_table.rb)

Run with this command from the root of the project if you cloned the project:

```
ruby -r './lib/glimmer-dsl-libui' examples/form_table.rb
```

Run with this command if you installed the [Ruby gem](https://rubygems.org/gems/glimmer-dsl-libui):

```
ruby -r glimmer-dsl-libui -e "require 'examples/form_table'"
```

Mac | Windows | Linux
----|---------|------
![glimmer-dsl-libui-mac-form-table.png](https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-libui/master/images/glimmer-dsl-libui-mac-form-table.png) | ![glimmer-dsl-libui-windows-form-table.png](https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-libui/master/images/glimmer-dsl-libui-windows-form-table.png) | ![glimmer-dsl-libui-linux-form-table.png](https://raw.githubusercontent.com/AndyObtiva/glimmer-dsl-libui/master/images/glimmer-dsl-libui-linux-form-table.png)

New [Glimmer DSL for LibUI](https://rubygems.org/gems/glimmer-dsl-libui) Version (with explicit [data-binding](https://github.com/AndyObtiva/glimmer#data-binding)):

```ruby
require 'glimmer-dsl-libui'

class FormTable
  Contact = Struct.new(:name, :email, :phone, :city, :state)
  
  include Glimmer
  
  attr_accessor :contacts, :name, :email, :phone, :city, :state, :filter_value
  
  def initialize
    @contacts = [
      Contact.new('Lisa Sky', 'lisa@sky.com', '720-523-4329', 'Denver', 'CO'),
      Contact.new('Jordan Biggins', 'jordan@biggins.com', '617-528-5399', 'Boston', 'MA'),
      Contact.new('Mary Glass', 'mary@glass.com', '847-589-8788', 'Elk Grove Village', 'IL'),
      Contact.new('Darren McGrath', 'darren@mcgrath.com', '206-539-9283', 'Seattle', 'WA'),
      Contact.new('Melody Hanheimer', 'melody@hanheimer.com', '213-493-8274', 'Los Angeles', 'CA'),
    ]
  end
  
  def launch
    window('Contacts', 600, 600) { |w|
      margined true
      
      vertical_box {
        form {
          stretchy false
          
          entry {
            label 'Name'
            text <=> [self, :name] # bidirectional data-binding between entry text and self.name
          }
          
          entry {
            label 'Email'
            text <=> [self, :email]
          }
          
          entry {
            label 'Phone'
            text <=> [self, :phone]
          }
          
          entry {
            label 'City'
            text <=> [self, :city]
          }
          
          entry {
            label 'State'
            text <=> [self, :state]
          }
        }
        
        button('Save Contact') {
          stretchy false
          
          on_clicked do
            new_row = [name, email, phone, city, state]
            if new_row.include?('')
              msg_box_error(w, 'Validation Error!', 'All fields are required! Please make sure to enter a value for all fields.')
            else
              @contacts << Contact.new(*new_row) # automatically inserts a row into the table due to explicit data-binding
              @unfiltered_contacts = @contacts.dup
              self.name = '' # automatically clears name entry through explicit data-binding
              self.email = ''
              self.phone = ''
              self.city = ''
              self.state = ''
            end
          end
        }
        
        search_entry {
          stretchy false
          # bidirectional data-binding of text to self.filter_value with after_write option
          text <=> [self, :filter_value,
            after_write: ->(filter_value) { # execute after write to self.filter_value
              @unfiltered_contacts ||= @contacts.dup
              # Unfilter first to remove any previous filters
              self.contacts = @unfiltered_contacts.dup # affects table indirectly through explicit data-binding
              # Now, apply filter if entered
              unless filter_value.empty?
                self.contacts = @contacts.filter do |contact| # affects table indirectly through explicit data-binding
                  contact.members.any? do |attribute|
                    contact[attribute].to_s.downcase.include?(filter_value.downcase)
                  end
                end
              end
            }
          ]
        }
        
        table {
          text_column('Name')
          text_column('Email')
          text_column('Phone')
          text_column('City')
          text_column('State')
    
          editable true
          # explicit data-binding to Model Array (expects model attribute names to be underscored column names by convention [e.g. :state for State], can be customized with :column_attributes option [e.g. {'State/Province' => :state})
          cell_rows <=> [self, :contacts] 
          
          on_changed do |row, type, row_data|
            puts "Row #{row} #{type}: #{row_data}"
          end
        }
      }
    }.show
  end
end

FormTable.new.launch
```