Closed rubyFeedback closed 2 years ago
I am going to answer 2 first.
2-
Below is a modified form_table.rb
example code that supports deletion with an X button (I am using Ruby 3.1 with support for the 3-dot ... operator).
Do not forget that with table data-binding, it is very simple to delete. As long as you delete an element from the data-bound array "self.contacts" (as known from this code cell_rows <=> [self, :contacts]
), Glimmer DSL for LibUI will automatically reflect your change in the table
control (the view). The code responsible for that is under the on_clicked
listener on button_column
with the logic self.contacts.delete_at(row)
(deletes by clicked row index)
By the way, deleting a table row by selecting/right-clicking a row (as opposed to clicking X) or by checkmarking it does not work today (not on all platforms) due to limitations in LibUI, but I believe libui-ng is working on supporting these options in the future.
require 'glimmer-dsl-libui'
class FormTable
Contact = Struct.new(:name, :email, :phone, :city, :state, :delete) do
def initialize(...)
super(...)
self.delete = 'X'
end
end
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) {
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.map(&:to_s).include?('')
msg_box_error('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')
button_column('Delete') {
on_clicked do |row|
self.contacts.delete_at(row)
end
}
editable true
cell_rows <=> [self, :contacts] # explicit data-binding to self.contacts Model Array, auto-inferring model attribute names from underscored table column names by convention
on_changed do |row, type, row_data|
puts "Row #{row} #{type}: #{row_data}"
$stdout.flush # for Windows
end
on_edited do |row, row_data| # only fires on direct table editing
puts "Row #{row} edited: #{row_data}"
$stdout.flush # for Windows
end
}
}
}.show
end
end
FormTable.new.launch
Screenshots where I click the X buttons one by one in random order.
Regarding your first question: "Would it be possible to dump the current dataset into a .yml file and/or hash? This can happen on the commandline too. That way we can quickly copy/paste it. Or perhaps a .yml file, save it into a local file, so we can use that as a real contact form."
1-
I am not sure why you are asking me that question since it is a Ruby matter that is not related to GUI or Glimmer DSL for LibUI. So, you should know the answer already.
At the start of the program, use the YAML
class to load data from a .yaml
/.yml
file (alternatively use the CSV
class to load data from a .csv
file, use JSON
class to load data from a .json
file, or use the File
class to load a flat file), and you're set!
It is recommended you store/load attributes as a hash that can be used to construct Contacts. Otherwise, YAML will complain about classes not being permitted unless you whitelist them, which adds unnecessary complexity.
Also, you have to save back to the YAML (or whatever format) file upon every change the user makes. You can use the table
on_changed
listener for that purpose.
In any case, below is an implementation that loads/stores contacts from/to a YAML file via contact attribute hashes.
Start by creating this directory: ~/.form-table
:
mkdir ~/.form-table
Next, create a YAML file at ~/.form-table/contacts.yml
:
touch ~/.form-table/contacts.yml
Open the YAML file and add to it content like the following:
---
- :name: Lisa Sky
:email: lisa@sky.com
:phone: 720-523-4329
:city: Denver
:state: CO
:delete: X
- :name: Jordan Biggins
:email: jordan@biggins.com
:phone: 617-528-5399
:city: Boston
:state: MA
:delete: X
- :name: Mary Glass
:email: mary@glass.com
:phone: 847-589-8788
:city: Elk Grove Village
:state: IL
:delete: X
- :name: Darren McGrath
:email: darren@mcgrath.com
:phone: 206-539-9283
:city: Seattle
:state: WA
:delete: X
- :name: Melody Hanheimer
:email: melody@hanheimer.com
:phone: 213-493-8274
:city: Los Angeles
:state: CA
:delete: X
Finally, run the following modified Form Table code:
require 'glimmer-dsl-libui'
require 'yaml'
require 'fileutils'
Contact = Struct.new(:name, :email, :phone, :city, :state, :delete, keyword_init: true) do
def initialize(...)
super(...)
self.delete = 'X'
end
end
class FormTable
CONTACT_FILE = File.join(Dir.home, '.form-table', 'contacts.yml')
FileUtils.mkdir_p File.dirname(CONTACT_FILE)
include Glimmer
attr_accessor :contacts, :name, :email, :phone, :city, :state, :filter_value
def initialize
load_contacts
end
def load_contacts
if File.exist?(CONTACT_FILE)
self.contacts = YAML.load(File.read(CONTACT_FILE)).map { |contact_hash| Contact.new(contact_hash) }
else
self.contacts = []
end
end
def save_contacts
File.write(CONTACT_FILE, YAML.dump(self.contacts.map(&:to_h)))
end
def launch
window('Contacts', 600, 600) {
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: name, email: email, phone: phone, city: city, state: state}
if new_row.values.map(&:to_s).include?('')
msg_box_error('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')
button_column('Delete') {
on_clicked do |row|
self.contacts.delete_at(row)
end
}
editable true
cell_rows <=> [self, :contacts] # explicit data-binding to self.contacts Model Array, auto-inferring model attribute names from underscored table column names by convention
on_changed do |row, type, row_data|
# Ensure that changes to contact attributes results in saving contacts
save_contacts
puts "Row #{row} #{type}: #{row_data}"
$stdout.flush # for Windows
end
on_edited do |row, row_data| # only fires on direct table editing
puts "Row #{row} edited: #{row_data}"
$stdout.flush # for Windows
end
}
}
}.show
end
end
FormTable.new.launch
Now, when you run the app, add new contacts, close it, and open again; it remembers your new contacts or any changes to older contacts, including deletion.
1) Would it be possible to dump the current dataset into a .yml file and/or hash? This can happen on the commandline too. That way we can quickly copy/paste it. Or perhaps a .yml file, save it into a local file, so we can use that as a real contact form.
2) Would it be possible to also delete a row? Not sure how easy it is to do so; could simply be a button and a field with the field focusing on a number; or more elegantly perhaps the currently selected row will be deleted if a delete-button is clicked.
The example I refer to is the one provided in the main README e.g . with