
Requirements:
1- Store contacts having the following attributes:
- First Name
- Last Name
- Phone
- Street
- City
- State/Province
- Zip/Postal Code
- Country
2- List stored contacts
3- Search stored contacts by any of the contact attributes.
4- Sort contacts by First Name, Last Name, Email, Phone, or Full Address
5- Edit a stored contact
6- Delete a stored contact
7- Delete all stored contacts
Implementation:
1- Gem Install
gem install glimmer-dsl-swt -v4.24.4.3
% gem install glimmer-dsl-swt -v4.24.4.3
Fetching text-table-1.2.4.gem
Fetching rouge-3.28.0.gem
Fetching super_module-1.4.1.gem
Fetching wisper-2.0.1.gem
Fetching method_source-1.0.0.gem
Fetching tty-screen-0.8.1.gem
Fetching tty-cursor-0.7.1.gem
Fetching tty-reader-0.9.0.gem
Fetching tty-color-0.6.0.gem
Fetching pastel-0.8.0.gem
Fetching tty-prompt-0.23.1.gem
Fetching rake-tui-0.2.3.gem
Fetching awesome_print-1.9.2.gem
Fetching os-1.1.4.gem
Fetching nested_inherited_jruby_include_package-0.3.0.gem
Fetching jruby-win32ole-0.8.5.gem
Fetching facets-3.1.0.gem
Fetching array_include_methods-1.4.0.gem
Fetching glimmer-2.7.3.gem
Fetching puts_debuggerer-0.13.5.gem
Fetching glimmer-dsl-swt-4.24.4.3.gem
Fetching concurrent-ruby-1.1.10.gem
Successfully installed text-table-1.2.4
Successfully installed method_source-1.0.0
Successfully installed super_module-1.4.1
Successfully installed rouge-3.28.0
Successfully installed wisper-2.0.1
Successfully installed tty-screen-0.8.1
Successfully installed tty-cursor-0.7.1
Successfully installed tty-reader-0.9.0
Successfully installed tty-color-0.6.0
Successfully installed pastel-0.8.0
Successfully installed tty-prompt-0.23.1
Successfully installed rake-tui-0.2.3
Successfully installed awesome_print-1.9.2
Successfully installed puts_debuggerer-0.13.5
Successfully installed os-1.1.4
Successfully installed nested_inherited_jruby_include_package-0.3.0
Successfully installed jruby-win32ole-0.8.5
Successfully installed facets-3.1.0
Successfully installed array_include_methods-1.4.0
Successfully installed glimmer-2.7.3
Successfully installed concurrent-ruby-1.1.10
You are ready to use `glimmer` and `girb` commands on Windows and Linux.
On the Mac, run `glimmer-setup` command to complete setup of Glimmer DSL for SWT (it will configure a Mac required jruby option globally `-J-XstartOnFirstThread` so that you do not have to add manually), making `glimmer` and `girb` commands ready for use:
glimmer-setup
Successfully installed glimmer-dsl-swt-4.24.4.3
22 gems installed
glimmer-setup
2- Scaffolding
glimmer "scaffold[contact_manager]"
% glimmer "scaffold[contact_manager]"
Fetching highline-2.0.3.gem
Fetching rack-2.2.3.gem
Fetching semver2-3.4.2.gem
Fetching kamelcase-0.0.2.gem
Fetching nokogiri-1.13.4-java.gem
Fetching multi_xml-0.6.0.gem
Fetching multi_json-1.15.0.gem
Fetching jwt-2.3.0.gem
Fetching ruby2_keywords-0.0.5.gem
Fetching faraday-retry-1.0.3.gem
Fetching faraday-rack-1.0.0.gem
Fetching faraday-patron-1.0.0.gem
Fetching faraday-net_http-1.0.1.gem
Fetching faraday-net_http_persistent-1.2.0.gem
Fetching multipart-post-2.1.1.gem
Fetching faraday-multipart-1.0.3.gem
Fetching faraday-httpclient-1.0.1.gem
Fetching faraday-excon-1.1.0.gem
Fetching faraday-em_synchrony-1.0.0.gem
Fetching faraday-em_http-1.0.0.gem
Fetching hashie-3.6.0.gem
Fetching thread_safe-0.3.6-java.gem
Fetching descendants_tracker-0.0.4.gem
Fetching public_suffix-4.0.6.gem
Fetching addressable-2.8.0.gem
Fetching github_api-0.19.0.gem
Fetching faraday-1.10.0.gem
Fetching oauth2-1.4.9.gem
Fetching rchardet-1.8.0.gem
Fetching git-1.10.2.gem
Fetching builder-3.2.4.gem
Fetching juwelier-2.4.9.gem
Successfully installed semver2-3.4.2
Successfully installed nokogiri-1.13.4-java
Successfully installed kamelcase-0.0.2
Successfully installed highline-2.0.3
Successfully installed rack-2.2.3
Successfully installed multi_xml-0.6.0
Successfully installed multi_json-1.15.0
Successfully installed jwt-2.3.0
Successfully installed ruby2_keywords-0.0.5
Successfully installed faraday-retry-1.0.3
Successfully installed faraday-rack-1.0.0
Successfully installed faraday-patron-1.0.0
Successfully installed faraday-net_http_persistent-1.2.0
Successfully installed faraday-net_http-1.0.1
Successfully installed multipart-post-2.1.1
Successfully installed faraday-multipart-1.0.3
Successfully installed faraday-httpclient-1.0.1
Successfully installed faraday-excon-1.1.0
Successfully installed faraday-em_synchrony-1.0.0
Successfully installed faraday-em_http-1.0.0
Successfully installed faraday-1.10.0
Successfully installed oauth2-1.4.9
Successfully installed hashie-3.6.0
Successfully installed thread_safe-0.3.6-java
Successfully installed descendants_tracker-0.0.4
Successfully installed public_suffix-4.0.6
Successfully installed addressable-2.8.0
Successfully installed github_api-0.19.0
Successfully installed rchardet-1.8.0
Successfully installed git-1.10.2
Successfully installed builder-3.2.4
Successfully installed juwelier-2.4.9
32 gems installed
create .gitignore
create Rakefile
create Gemfile
create LICENSE.txt
create README.markdown
create .document
create lib
create lib/contact_manager.rb
create .rspec
Juwelier has prepared your gem in ./contact_manager
Created contact_manager/.gitignore
Created contact_manager/.ruby-version
Created contact_manager/.ruby-gemset
Created contact_manager/VERSION
Created contact_manager/LICENSE.txt
Created contact_manager/Gemfile
Created contact_manager/Rakefile
Created contact_manager/app/contact_manager.rb
Created contact_manager/app/contact_manager/view/app_view.rb
Created contact_manager/icons/windows/Contact Manager.ico
Created contact_manager/icons/macosx/Contact Manager.icns
Created contact_manager/icons/linux/Contact Manager.png
Created contact_manager/app/contact_manager/launch.rb
Created contact_manager/bin/contact_manager
jruby-9.3.7.0 - #gemset created /Users/andymaleh/.rvm/gems/jruby-9.3.7.0@contact_manager
jruby-9.3.7.0 - #generating contact_manager wrappers..............
Fetching gem metadata from https://rubygems.org/.........
Resolving dependencies...................
Using rake 13.0.6
Fetching public_suffix 4.0.6
Fetching array_include_methods 1.4.0
Fetching awesome_print 1.9.2
Fetching builder 3.2.4
Installing builder 3.2.4
Installing array_include_methods 1.4.0
Installing awesome_print 1.9.2
Installing public_suffix 4.0.6
Using bundler 2.3.9
Fetching concurrent-ruby 1.1.10
Fetching thread_safe 0.3.6 (java)
Installing concurrent-ruby 1.1.10
Fetching diff-lcs 1.5.0
Installing thread_safe 0.3.6 (java)
Installing diff-lcs 1.5.0
Fetching docile 1.4.0
Installing docile 1.4.0
Fetching facets 3.1.0
Fetching faraday-em_http 1.0.0
Fetching faraday-em_synchrony 1.0.0
Installing faraday-em_http 1.0.0
Installing facets 3.1.0
Installing faraday-em_synchrony 1.0.0
Fetching faraday-excon 1.1.0
Fetching faraday-httpclient 1.0.1
Installing faraday-excon 1.1.0
Installing faraday-httpclient 1.0.1
Fetching multipart-post 2.1.1
Fetching faraday-net_http 1.0.1
Installing multipart-post 2.1.1
Installing faraday-net_http 1.0.1
Fetching faraday-net_http_persistent 1.2.0
Fetching faraday-patron 1.0.0
Fetching faraday-rack 1.0.0
Installing faraday-net_http_persistent 1.2.0
Installing faraday-rack 1.0.0
Installing faraday-patron 1.0.0
Fetching faraday-retry 1.0.3
Installing faraday-retry 1.0.3
Fetching ruby2_keywords 0.0.5
Fetching rchardet 1.8.0
Installing ruby2_keywords 0.0.5
Installing rchardet 1.8.0
Fetching hashie 3.6.0
Fetching jwt 2.3.0
Installing hashie 3.6.0
Installing jwt 2.3.0
Fetching multi_json 1.15.0
Installing multi_json 1.15.0
Fetching multi_xml 0.6.0
Fetching rack 2.2.3
Installing multi_xml 0.6.0
Installing rack 2.2.3
Fetching jruby-win32ole 0.8.5
Fetching nested_inherited_jruby_include_package 0.3.0
Installing nested_inherited_jruby_include_package 0.3.0
Installing jruby-win32ole 0.8.5
Fetching os 1.1.4
Installing os 1.1.4
Fetching tty-color 0.6.0
Fetching tty-cursor 0.7.1
Fetching tty-screen 0.8.1
Installing tty-color 0.6.0
Installing tty-cursor 0.7.1
Installing tty-screen 0.8.1
Fetching wisper 2.0.1
Fetching rouge 3.28.0
Fetching method_source 1.0.0
Installing wisper 2.0.1
Installing method_source 1.0.0
Installing rouge 3.28.0
Fetching text-table 1.2.4
Installing text-table 1.2.4
Fetching highline 2.0.3
Installing highline 2.0.3
Using jar-dependencies 0.4.1
Fetching jruby-jars 9.3.7.0
Fetching jruby-rack 1.1.21
Installing jruby-rack 1.1.21
Fetching semver2 3.4.2
Installing semver2 3.4.2
Fetching racc 1.6.0 (java)
Installing racc 1.6.0 (java)
Fetching rspec-support 3.5.0
Installing rspec-support 3.5.0
Fetching rubyzip 1.3.0
Fetching simplecov-html 0.12.3
Installing rubyzip 1.3.0
Installing simplecov-html 0.12.3
Fetching simplecov_json_formatter 0.1.4
Installing simplecov_json_formatter 0.1.4
Fetching addressable 2.8.0
Fetching puts_debuggerer 0.13.5
Installing puts_debuggerer 0.13.5
Installing addressable 2.8.0
Fetching descendants_tracker 0.0.4
Installing descendants_tracker 0.0.4
Fetching faraday-multipart 1.0.3
Installing faraday-multipart 1.0.3
Fetching git 1.10.2
Fetching pastel 0.8.0
Fetching super_module 1.4.1
Installing git 1.10.2
Installing pastel 0.8.0
Installing super_module 1.4.1
Fetching tty-reader 0.9.0
Fetching psych 4.0.3 (java)
Installing tty-reader 0.9.0
Fetching kamelcase 0.0.2
Installing kamelcase 0.0.2
Installing jruby-jars 9.3.7.0
Installing psych 4.0.3 (java)
Fetching nokogiri 1.13.4 (java)
Fetching rspec-core 3.5.4
Installing rspec-core 3.5.4
jar dependencies for psych-4.0.3-java.gemspec . . .
Installing gem 'ruby-maven' . . .
Fetching rspec-expectations 3.5.0
Installing rspec-expectations 3.5.0
Installing nokogiri 1.13.4 (java)
Fetching rspec-mocks 3.5.0
Installing rspec-mocks 3.5.0
Fetching simplecov 0.21.2
Installing simplecov 0.21.2
Fetching faraday 1.10.0
Installing faraday 1.10.0
Fetching glimmer 2.7.3
Installing glimmer 2.7.3
Fetching tty-prompt 0.23.1
Installing tty-prompt 0.23.1
Fetching rspec 3.5.0
Fetching warbler 2.0.5
Installing rspec 3.5.0
Installing warbler 2.0.5
Fetching oauth2 1.4.9
Fetching rake-tui 0.2.3
Installing rake-tui 0.2.3
Installing oauth2 1.4.9
Fetching glimmer-dsl-swt 4.24.4.3
Fetching github_api 0.19.0
using maven for the first time results in maven
downloading all its default plugin and can take time.
as those plugins get cached on disk and further execution
of maven is much faster then the first time.
Installing github_api 0.19.0
Installing glimmer-dsl-swt 4.24.4.3
2022-04-11T20:47:17.877-04:00 [main] WARN FilenoUtil : Native subprocess control requires open access to the JDK IO subsystem
Pass '--add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED' to enable.
org.yaml:snakeyaml:1.28:compile
Fetching rdoc 6.4.0
Installing rdoc 6.4.0
Fetching juwelier 2.4.9
Installing juwelier 2.4.9
Bundle complete! 5 Gemfile dependencies, 74 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
Post-install message from glimmer-dsl-swt:
["\nYou are ready to use `glimmer` and `girb` commands on Windows and Linux.\n\nOn the Mac, run `glimmer-setup` command to complete setup of Glimmer DSL for SWT (it will configure a Mac required jruby option globally `-J-XstartOnFirstThread` so that you do not have to add manually), making `glimmer` and `girb` commands ready for use:\n\nglimmer-setup\n\n"]
exist .rspec
create spec/spec_helper.rb
Created contact_manager/spec/spec_helper.rb
Generated: contact_manager.gemspec
Locking gem jar-dependencies by downloading and storing in vendor/jars...
lock_jars --vendor-dir vendor/jars
Installing gem 'ruby-maven' . . .
Installing gem 'ruby-maven-libs' . . .
using maven for the first time results in maven
downloading all its default plugin and can take time.
as those plugins get cached on disk and further execution
of maven is much faster then the first time.
-- jar root dependencies --
2022-04-11T20:48:09.018-04:00 [main] WARN FilenoUtil : Native subprocess control requires open access to the JDK IO subsystem
Pass '--add-opens java.base/sun.nio.ch=ALL-UNNAMED --add-opens java.base/java.io=ALL-UNNAMED' to enable.
org.yaml:snakeyaml:1.28:compile
vendor jars:
- create vendor/jars/org/yaml/snakeyaml/1.28/snakeyaml-1.28.jar
Jars.lock created
Generating JAR configuration (config/warble.rb) to use with Warbler...
/Users/andymaleh/.rvm/gems/jruby-9.3.7.0@contact_manager/bin/warble
cp /Users/andymaleh/.rvm/gems/jruby-9.3.7.0@contact_manager/gems/warbler-2.0.5/warble.rb config/warble.rb
Generating JAR with Warbler...
/Users/andymaleh/.rvm/gems/jruby-9.3.7.0@contact_manager/bin/warble
application directory `db' does not exist or is not a directory; skipping
application directory `docs' does not exist or is not a directory; skipping
application directory `fonts' does not exist or is not a directory; skipping
application directory `images' does not exist or is not a directory; skipping
application directory `lib' does not exist or is not a directory; skipping
application directory `package' does not exist or is not a directory; skipping
application directory `script' does not exist or is not a directory; skipping
application directory `sounds' does not exist or is not a directory; skipping
application directory `videos' does not exist or is not a directory; skipping
rm -f dist/contact_manager.jar
Creating dist/contact_manager.jar
Generating native executable with jpackage...
Java Version 18 Detected!
jpackage --type app-image --dest 'packages/bundles' --input 'dist' --main-class JarMain --main-jar 'contact_manager.jar' --name 'Contact Manager' --vendor 'Contact Manager' --icon 'icons/macosx/Contact Manager.icns' --java-options '-XstartOnFirstThread' --mac-package-name 'Contact Manager' --mac-package-identifier 'org.contact_manager.application.contact_manager' --app-version "1.0.0" --copyright "Copyright (c) 2022 contact_manager" --name 'Contact Manager' --description 'Contact Manager'
Launching Glimmer Application: ./bin/contact_manager
Launching Glimmer Application: /Users/andymaleh/code/contact_manager/app/contact_manager/launch.rb
3- Add ActiveRecord gems to Gemfile
gem 'activerecord', '~> 6.1.5' | |
gem 'activerecord-jdbcsqlite3-adapter', '~> 61.1', :platform => :jruby |
bundle install
Fetching gem metadata from https://rubygems.org/.......
Resolving dependencies..........................
Using rake 13.0.6
Using concurrent-ruby 1.1.10
Fetching zeitwerk 2.5.4
Fetching minitest 5.15.0
Using public_suffix 4.0.6
Fetching jdbc-sqlite3 3.28.0
Using array_include_methods 1.4.0
Using awesome_print 1.9.2
Using builder 3.2.4
Using bundler 2.3.9
Using thread_safe 0.3.6 (java)
Using diff-lcs 1.5.0
Using docile 1.4.0
Using facets 3.1.0
Using faraday-em_http 1.0.0
Using faraday-em_synchrony 1.0.0
Using faraday-excon 1.1.0
Using faraday-httpclient 1.0.1
Using multipart-post 2.1.1
Using faraday-net_http 1.0.1
Using faraday-net_http_persistent 1.2.0
Using faraday-patron 1.0.0
Using faraday-rack 1.0.0
Using faraday-retry 1.0.3
Using ruby2_keywords 0.0.5
Using rchardet 1.8.0
Using hashie 3.6.0
Using jwt 2.3.0
Using multi_json 1.15.0
Using multi_xml 0.6.0
Using rack 2.2.3
Using jruby-win32ole 0.8.5
Using nested_inherited_jruby_include_package 0.3.0
Using os 1.1.4
Using tty-color 0.6.0
Using tty-cursor 0.7.1
Using tty-screen 0.8.1
Using wisper 2.0.1
Using rouge 3.28.0
Using method_source 1.0.0
Using text-table 1.2.4
Using highline 2.0.3
Using jar-dependencies 0.4.1
Using jruby-jars 9.3.7.0
Using jruby-rack 1.1.21
Using semver2 3.4.2
Using racc 1.6.0 (java)
Using rspec-support 3.5.0
Using rubyzip 1.3.0
Using simplecov-html 0.12.3
Using simplecov_json_formatter 0.1.4
Fetching i18n 1.10.0
Installing zeitwerk 2.5.4
Installing minitest 5.15.0
Installing i18n 1.10.0
Fetching tzinfo 2.0.4
Using addressable 2.8.0
Using puts_debuggerer 0.13.5
Using descendants_tracker 0.0.4
Using glimmer 2.7.3
Using faraday-multipart 1.0.3
Using git 1.10.2
Using pastel 0.8.0
Using tty-reader 0.9.0
Using super_module 1.4.1
Using psych 4.0.3 (java)
Using kamelcase 0.0.2
Using nokogiri 1.13.4 (java)
Using rspec-core 3.5.4
Using rspec-expectations 3.5.0
Using rspec-mocks 3.5.0
Using warbler 2.0.5
Using simplecov 0.21.2
Using faraday 1.10.0
Using tty-prompt 0.23.1
Using rdoc 6.4.0
Using rspec 3.5.0
Using oauth2 1.4.9
Using rake-tui 0.2.3
Using github_api 0.19.0
Using glimmer-dsl-swt 4.24.4.3
Using juwelier 2.4.9
Installing tzinfo 2.0.4
Fetching activesupport 6.1.5
Installing activesupport 6.1.5
Fetching activemodel 6.1.5
Installing activemodel 6.1.5
Fetching activerecord 6.1.5
Installing activerecord 6.1.5
Fetching activerecord-jdbc-adapter 61.1 (java)
Installing jdbc-sqlite3 3.28.0
Installing activerecord-jdbc-adapter 61.1 (java)
Fetching activerecord-jdbcsqlite3-adapter 61.1 (java)
Installing activerecord-jdbcsqlite3-adapter 61.1 (java)
Bundle complete! 7 Gemfile dependencies, 84 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
4- Add an ActiveRecord migration for the contacts database table
migrate_dir = File.expand_path('../migrate', __FILE__) | |
Dir.glob(File.join(migrate_dir, '**', '*.rb')).each {|migration| require migration} | |
ActiveRecord::Migration[6.1].descendants.each do |migration| | |
begin | |
migration.migrate(:up) | |
rescue => e | |
raise e unless e.full_message.match(/table "[^"]+" already exists/) | |
end | |
end |
require 'active_record' | |
class CreateContacts < ActiveRecord::Migration[6.1] | |
def change | |
create_table :contacts do |t| | |
t.string :first_name | |
t.string :last_name | |
t.string :email | |
t.string :phone | |
t.string :street | |
t.string :city | |
t.string :state_or_province | |
t.string :zip_or_postal_code | |
t.string :country | |
end | |
end | |
end |
5- Add a SQLite database with ActiveRecord
require 'active_record' | |
require 'jdbc/sqlite3' | |
Jdbc::SQLite3.load_driver | |
require 'activerecord-jdbcsqlite3-adapter' if defined? JRUBY_VERSION | |
@connection = ActiveRecord::Base.establish_connection( | |
adapter: 'sqlite3', | |
database: File.join(Dir.home, 'db/contact_manager.sqlite3') | |
) | |
require 'db/migrate' |
6- Add the Contact ActiveRecord model
require 'db/db' | |
class Contact < ActiveRecord::Base | |
STATES = [ 'AK', 'AL', 'AR', 'AZ', 'CA', 'CO', 'CT', 'DC', 'DE', 'FL', 'GA', | |
'HI', 'IA', 'ID', 'IL', 'IN', 'KS', 'KY', 'LA', 'MA', 'MD', 'ME', | |
'MI', 'MN', 'MO', 'MS', 'MT', 'NC', 'ND', 'NE', 'NH', 'NJ', 'NM', | |
'NV', 'NY', 'OH', 'OK', 'OR', 'PA', 'RI', 'SC', 'SD', 'TN', 'TX', | |
'UT', 'VA', 'VT', 'WA', 'WI', 'WV', 'WY'] | |
PROVINCES = ['NL', 'PE', 'NS', 'NB', 'QC', 'ON', 'MB', 'SK', 'AB', 'BC', 'YT', 'NT', 'NU'] | |
validates :first_name, presence: true | |
validates :last_name, presence: true | |
validates :email, format: {with: /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/i, message: 'must be a valid email address'}, allow_blank: true | |
validates :phone, format: {with: /\A[(]?[0-9]{3}[)]?[ -\.\/0-9]{7,9}\z/, message: 'must be a valid phone number'}, allow_blank: true | |
validates :zip_or_postal_code, format: {with: /\A[a-z0-9][0-9][a-z0-9][ 0-9][a-z0-9][a-z0-9]?[0-9]?\z/i, message: 'must be a valid phone number'}, allow_blank: true | |
validate :email_or_phone_is_present | |
def address | |
address_fields = [street, city, state_or_province, zip_or_postal_code, country] | |
address_fields.map { |field| field.blank? ? nil : field }.compact.join(', ') | |
end | |
def country_options | |
['', 'Canada', 'USA'] | |
end | |
def state_or_province_options | |
(PROVINCES + [''] + STATES) | |
end | |
def reset! | |
return unless persisted? && changed? | |
attributes.each do |attribute, value| | |
# calling attribute writers explicitly notifies Glimmer GUI observers indirectly | |
send("#{attribute}=", send("#{attribute}_was")) if send("#{attribute}_changed?") | |
end | |
end | |
private | |
def email_or_phone_is_present | |
if email.blank? && phone.blank? | |
errors.add(:email, 'must be present unless phone is present') | |
errors.add(:phone, 'must be present unless email is present') | |
end | |
end | |
end |
7- Add the ContactRepository model
require 'singleton' | |
require 'contact_manager/model/contact' | |
class ContactRepository | |
include Singleton | |
def all | |
Contact.all | |
end | |
def search(query_value) | |
if query_value.present? | |
attribute_names = Contact.new.attributes.keys | |
conditions = attribute_names.reduce('') do |conditions, attribute| | |
if conditions.blank? | |
conditions += "lower(#{attribute}) like ?" | |
else | |
conditions += " OR lower(#{attribute}) like ?" | |
end | |
end | |
Contact.where(conditions, *(["%#{query_value.downcase}%"]*attribute_names.count)) | |
else | |
all | |
end | |
end | |
def destroy_all_contacts | |
Contact.destroy_all | |
end | |
end |
8- Add the ContactPresenter model
require 'contact_manager/model/contact' | |
require 'contact_manager/model/contact_repository' | |
# ContactPresenter is an enhanced Controller that also | |
# enables bidirectional data-binding of contact attributes | |
class ContactPresenter | |
attr_accessor :contacts, :query, :current_contact | |
def initialize | |
renew_current_contact | |
refresh_contacts | |
# Monitor Contact collection changes | |
# the after_commit hook executes the block under a different object | |
# binding, so we must use `this` to access self | |
this = self | |
Contact.after_commit(on: [:create, :update, :destroy]) do | |
this.refresh_contacts | |
end | |
end | |
def query=(query_value) | |
@query = query_value | |
refresh_contacts | |
end | |
def current_contact=(new_contact) | |
# first, reset current contact in case it was changed but not saved | |
@current_contact&.reset! | |
# next, update current contact | |
@current_contact = new_contact | |
end | |
def refresh_contacts | |
if query | |
new_contacts = ContactRepository.instance.search(query) | |
else | |
new_contacts = ContactRepository.instance.all | |
end | |
if new_contacts != contacts | |
self.contacts = new_contacts.to_a | |
refresh_current_contact | |
end | |
end | |
def refresh_current_contact | |
current_contact_index = contacts.index(current_contact) | |
self.current_contact = contacts[current_contact_index] if current_contact_index | |
end | |
def renew_current_contact | |
self.current_contact = Contact.new | |
end | |
def save_current_contact | |
current_contact.save.tap do |saved| | |
renew_current_contact if saved | |
end | |
end | |
def destroy_current_contact | |
if current_contact&.persisted? | |
current_contact.destroy | |
renew_current_contact | |
end | |
end | |
def destroy_all_contacts | |
ContactRepository.instance.destroy_all_contacts | |
end | |
end |
9- Add the ContactForm custom widget view
class ContactManager | |
module View | |
class ContactForm | |
include Glimmer::UI::CustomWidget | |
options :contact_presenter | |
body { | |
composite { | |
grid_layout { | |
num_columns 2 | |
make_columns_equal_width true | |
margin_width 0 | |
margin_height 0 | |
vertical_spacing 0 | |
} | |
form_column { | |
form_field(:first_name) | |
form_field(:last_name) | |
form_field(:email) | |
form_field(:phone) | |
} | |
form_column { | |
form_field(:street) | |
form_field(:city) | |
form_field(:state_or_province, editor: :combo, editor_args: :read_only, property: :selection) | |
form_field(:zip_or_postal_code) | |
form_field(:country, editor: :combo, editor_args: :read_only, property: :selection) | |
} | |
composite { # having a composite ensures padding around button | |
layout_data { | |
horizontal_span 2 | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
} | |
grid_layout { | |
margin_height 0 | |
} | |
button { | |
layout_data { | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
} | |
text 'Save Contact' | |
on_widget_selected do | |
save_contact | |
end | |
} | |
} | |
} | |
} | |
def form_column(&content) | |
composite { |the_composite| | |
layout_data { | |
horizontal_alignment :fill | |
vertical_alignment :fill | |
grab_excess_horizontal_space true | |
grab_excess_vertical_space true | |
} | |
grid_layout { | |
num_columns 2 | |
make_columns_equal_width false | |
} | |
content.call(the_composite) | |
} | |
end | |
def form_field(field, editor: :text, editor_args: [], property: :text) | |
@form_field_labels ||= {} | |
@form_field_labels[field] = label { | |
layout_data { | |
width_hint 120 | |
} | |
text field.to_s.gsub('_', ' ').titlecase | |
} | |
@form_field_texts ||= {} | |
@form_field_texts[field] = send(editor, *editor_args) { | |
layout_data { | |
width_hint 150 | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
} | |
# use nested data-binding to monitor change of contact | |
# in addition to contact field | |
send(property) <=> [contact_presenter, "current_contact.#{field}"] | |
on_key_pressed do |event| | |
save_contact if event.keyCode == swt(:cr) | |
end | |
} | |
end | |
def save_contact | |
if contact_presenter.save_current_contact | |
reset_validations | |
else | |
show_validations | |
end | |
end | |
def reset_validations | |
contact_presenter.current_contact.attributes.keys.each do |attribute_name| | |
@form_field_labels[attribute_name.to_sym]&.foreground = :black | |
@form_field_labels[attribute_name.to_sym]&.tool_tip_text = nil | |
end | |
focus_first_field | |
end | |
def show_validations | |
contact_presenter.current_contact.errors.errors.each do |error| | |
@form_field_labels[error.attribute].foreground = :red | |
@form_field_labels[error.attribute].tool_tip_text = error.full_message | |
end | |
end | |
def focus_first_field | |
@form_field_texts[:first_name].set_focus | |
end | |
end | |
end | |
end |
10- Add the ContactTable custom widget view
Create app/contact_manager/view/contact_table.rb and add the `contact_table` custom widget view code to it:
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
class ContactManager
module View
class ContactTable
include Glimmer::UI::CustomWidget
options :contact_presenter, :reset_validations_action
body {
composite {
grid_layout {
margin_height 0
}
text(:search) {
layout_data {
horizontal_alignment :fill
grab_excess_horizontal_space true
}
text <=> [contact_presenter, :query]
}
table {
layout_data {
height_hint 250
horizontal_alignment :fill
grab_excess_horizontal_space true
vertical_alignment :fill
grab_excess_vertical_space true
}
table_column {
text 'First Name'
width 120
}
table_column {
text 'Last Name'
width 120
}
table_column {
text 'Email'
width 180
}
table_column {
text 'Phone'
width 120
}
table_column {
text 'Address'
width 320
}
items <=> [contact_presenter, :contacts]
selection <=> [contact_presenter, :current_contact, after_write: reset_validations_action]
menu {
menu_item {
text '&Delete...'
on_widget_selected do
result = message_box(:yes, :no) {
text 'Delete'
message 'Are you sure you want to delete the selected contact?'
}.open
contact_presenter.destroy_current_contact if result == swt(:yes)
end
}
}
}
}
}
end
end
end
class ContactManager | |
module View | |
class ContactTable | |
include Glimmer::UI::CustomWidget | |
options :contact_presenter, :reset_validations_action | |
body { | |
composite { | |
grid_layout { | |
margin_height 0 | |
} | |
text(:search) { | |
layout_data { | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
} | |
text <=> [contact_presenter, :query] | |
} | |
table { | |
layout_data { | |
height_hint 250 | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
vertical_alignment :fill | |
grab_excess_vertical_space true | |
} | |
table_column { | |
text 'First Name' | |
width 120 | |
} | |
table_column { | |
text 'Last Name' | |
width 120 | |
} | |
table_column { | |
text 'Email' | |
width 180 | |
} | |
table_column { | |
text 'Phone' | |
width 120 | |
} | |
table_column { | |
text 'Address' | |
width 320 | |
} | |
items <=> [contact_presenter, :contacts] | |
selection <=> [contact_presenter, :current_contact, after_write: reset_validations_action] | |
menu { | |
menu_item { | |
text '&Delete...' | |
on_widget_selected do | |
result = message_box(:yes, :no) { | |
text 'Delete' | |
message 'Are you sure you want to delete the selected contact?' | |
}.open | |
contact_presenter.destroy_current_contact if result == swt(:yes) | |
end | |
} | |
} | |
} | |
} | |
} | |
end | |
end | |
end |
11- Add the ContactManagerMenuBar custom widget view
Create app/contact_manager/view/contact_manager_menu_bar.rb and add the `contact_manager_menu_bar` custom widget view code to it:
12- Update the AppView
Update app/contact_manager/view/app_view.rb by replacing all code with the following:
require 'contact_manager/model/contact_presenter' | |
require 'contact_manager/view/contact_manager_menu_bar' | |
require 'contact_manager/view/contact_form' | |
require 'contact_manager/view/contact_table' | |
class ContactManager | |
module View | |
class AppView | |
include Glimmer::UI::Application | |
before_body do | |
@contact_presenter = ContactPresenter.new | |
@display = display { | |
on_about do | |
display_about_dialog | |
end | |
on_preferences do | |
display_about_dialog | |
end | |
} | |
end | |
body { | |
shell { | |
grid_layout | |
image File.join(APP_ROOT, 'icons', 'windows', "Contact Manager.ico") if OS.windows? | |
image File.join(APP_ROOT, 'icons', 'linux', "Contact Manager.png") unless OS.windows? | |
text "Contact Manager" | |
@contact_form = contact_form(contact_presenter: @contact_presenter) { | |
layout_data { | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
} | |
} | |
contact_table( | |
contact_presenter: @contact_presenter, | |
reset_validations_action: @contact_form.method(:reset_validations), | |
) { | |
layout_data { | |
horizontal_alignment :fill | |
grab_excess_horizontal_space true | |
vertical_alignment :fill | |
grab_excess_vertical_space true | |
} | |
} | |
contact_manager_menu_bar( | |
contact_presenter: @contact_presenter, | |
about_action: method(:display_about_dialog), | |
save_contact_action: @contact_form.method(:save_contact), | |
reset_validations_action: @contact_form.method(:reset_validations), | |
) | |
} | |
} | |
def display_about_dialog | |
message_box(body_root) { | |
text 'About' | |
message "Contact Manager #{VERSION}\n\n#{LICENSE}" | |
}.open | |
end | |
end | |
end | |
end |
13- Update the ContactManager entry point
$LOAD_PATH.unshift(File.expand_path('..', __FILE__)) | |
$LOAD_PATH.unshift(File.expand_path('../..', __FILE__)) | |
begin | |
require 'bundler/setup' | |
Bundler.require(:default) | |
rescue | |
# this runs when packaged as a gem (no bundler) | |
require 'glimmer-dsl-swt' | |
# add more gems if needed | |
require 'active_record' | |
require 'activerecord-jdbcsqlite3-adapter' | |
end | |
class ContactManager | |
include Glimmer | |
APP_ROOT = File.expand_path('../..', __FILE__) | |
VERSION = File.read(File.join(APP_ROOT, 'VERSION')) | |
LICENSE = File.read(File.join(APP_ROOT, 'LICENSE.txt')) | |
Display.app_name = 'Contact Manager' | |
Display.app_version = ContactManager::VERSION | |
end | |
require 'contact_manager/view/app_view' |
14- Replace app icons
15- Run
glimmer run
./bin/contact_manager
16- Package Native Executable
glimmer "package[dmg]"
glimmer "package[msi]"
glimmer "package[deb]"
glimmer "package[rpm]"
Software Architecture & Design
The View uses contact_form, contact_table, and contact_manager_menu_bar custom widgets (components).
The Contact Manager graphical user interface leverages the Master-Detail Interface Pattern by displaying a master list via contact_table and allowing navigation by selecting a Contact and displaying its details for editing in contact_form.
The Model layer includes a Contact and ContactRepository (DDD Repository Pattern) in addition to ContactPresenter (which is both a Controller and a Model at a higher level).
Contact follows the Active Record Pattern for Object Relational Mapping to store objects in a SQLite relational database table called contacts via a migration. It also implements ActiveRecord Validations for first_name, last_name, email, phone, and zip_or_postal_code fields.
ContactRepository provides the ability to search through all Contacts using the ActiveRecord Query Interface, triggered indirectly by ContactPresenter when typing into a text field that is on top of the contact_table .
The database is stored at File.join(Dir.home, 'db/contact_manager.sqlite3')
4 comments:
Thanks for sharing these!
Great post! I appreciate all the linked references, too. Thanks a lot.
Cool, working great. Thanks a lot!
You're welcome.
By the way, I just updated the article and project to improve and fix a few issues.
Details are in the project Change Log.
In summary, the glimmer-dsl-swt gem has been upgraded to v4.24.4.1, which includes table data-binding improvements and optimizations like auto-inferred column attributes by convention.
And, the following files were updated:
- app/contact_manager/model/contact.rb
- app/contact_manager/model/contact_presenter.rb
- app/contact_manager/view/contact_table.rb
Post a Comment