CRUD App using composition - kendmaclean/pharo12 GitHub Wiki
DRAFT April 18, 2024
Note: Pharo is continuously evolving - therefore make sure you use a Pharo 12 image with this example.
(Created with the help of Google AI Studio)
Let's create a simple CRUD (Create, Read, Update, Delete) application example using Pharo 12 and the Spec2 UI framework, and composition rather than inheritance (Note: Pharo recommends using subclassing)
We'll create an application to manage a list of simple "Person" objects, each having a name and an age.
1. The Domain Model (Person)
First, define the object we want to manage.
Object << #Person
slots: { #name . #age };
package: 'SpecCRUD-Example-UI-Composition'Add accessors and a basic initializer:
Person >> initialize
super initialize.
name := ''.
age := 0
Person >> name
^ name
Person >> name: aString
name := aString
Person >> age
^ age
Person >> age: anInteger
age := anInteger
Person >> displayString
"Used for displaying in lists"
^ name, ' (', age asString, ')'
Person >> printOn: aStream
"Standard printing"
super printOn: aStream.
aStream nextPut: $(.
self displayString printOn: aStream.
aStream nextPut: $)2. The Repository (PersonRepository)
We need a place to store our Person objects. For simplicity, we'll use an in-memory repository implemented as a Singleton.
Instance side:
Object << #PersonRepository
slots: { #persons };
package: 'SpecCRUD-Example-UI-Composition'Class side:
Object class << PersonRepository class
slots: { #SoleInstance }Implement the Singleton pattern and CRUD methods:
Class side:
PersonRepository class >> initialize
"Initialize the singleton instance"
SoleInstance := nil
PersonRepository class >> current
"Answer the singleton instance, creating it if necessary"
^ SoleInstance ifNil: [ SoleInstance := self new ]
PersonRepository class >> reset
"Reset the repository (useful for testing)"
SoleInstance := nilInstance side:
PersonRepository >> initialize
"Initialize the instance with an empty collection"
super initialize.
persons := OrderedCollection new
PersonRepository >> allPersons
"Answer all persons, sorted by name"
^ persons sorted: [ :a :b | a name < b name ]
PersonRepository >> addPerson: aPerson
"Add a new person"
persons add: aPerson
PersonRepository >> removePerson: aPerson
"Remove a person"
persons remove: aPerson ifAbsent: []
PersonRepository >> updatePerson: oldPerson with: newPersonData
"Find oldPerson and update its state with data from newPersonData.
Note: This assumes newPersonData is a Person object holding the new values.
A more robust approach might involve finding by a unique ID if names/ages can change."
| personToUpdate |
personToUpdate := persons detect: [ :p | p == oldPerson ] ifNone: [ ^ self ]. "Find the exact instance"
personToUpdate name: newPersonData name.
personToUpdate age: newPersonData age.3. The Editor Dialog (PersonEditor)
This dialog will be used for both creating and editing Person objects.
Object << #PersonEditor
slots: { #person . #nameInput . #ageInput . #okButton . #cancelButton . #title . #dialogPresenter};
package: 'SpecCRUD-Example-UI-Composition'Implement the UI layout and logic:
Class side:
PersonEditor class >> openModalFor: aPerson title: aString
"Open the editor modally.
Uses composition: Creates a PersonEditor instance which manages a SpDialogPresenter.
Returns the edited/created person if OK is pressed, nil otherwise."
| editor result |
editor := self new.
editor title: aString.
editor person: (aPerson ifNil: [ Person new ] ifNotNil: [ aPerson copy ]). "Edit a copy"
result := editor openModal. "Calls the instance-side openModal"
^ result ifNotNil: [ editor person ] ifNil: [ nil ] Instance side:
PersonEditor >> initialize
"Initialize the editor logic and create the managed presenters"
super initialize.
self initializePresenters.
self initializeLayout.
"self initializeWindowAspects." "does not work with openModal"
PersonEditor >> initializeLayout
"Define the layout and set it on the managed dialog presenter"
| layout |
layout := SpBoxLayout newVertical
add: 'Name:' expand: false;
add: nameInput;
add: 'Age:' expand: false;
add: ageInput;
add: (SpBoxLayout newHorizontal add: okButton; add: cancelButton; yourself) expand: false;
yourself.
dialogPresenter layout: layout
PersonEditor >> initializePresenters
"Create the UI components - these are now regular Spec presenters"
dialogPresenter := SpDialogPresenter new.
nameInput := SpTextInputFieldPresenter new. "same as 'self newTextInput' when subclassing SpDialogPresenter "
ageInput := SpNumberInputFieldPresenter new "same as 'self newNumberInput' when subclassing SpDialogPresenter "
minimum: 0;
maximum: 150;
yourself.
okButton := SpButtonPresenter new "same as 'self newButton' when subclassing SpDialogPresenter "
label: 'OK';
icon: self getIconOk;
action: [ self ok ].
cancelButton := SpButtonPresenter new
label: 'Cancel';
icon: self getIconCancel;
action: [ self cancel ].
dialogPresenter focusOrder
add: nameInput;
add: ageInput;
add: okButton
PersonEditor >> initializeWindowAspects
"Configure the dialog presenter's window properties"
"does not work with openModal"
dialogPresenter
title: self title;
initialExtent: 300 @ 180
PersonEditor >> person
"Answer the person being edited/created"
^ person
PersonEditor >> person: aPerson
"Set the person and update the input fields"
person := aPerson.
nameInput text: person name.
ageInput number: person age
PersonEditor >> ok
"Called when OK button is pressed. Validate and apply changes."
| name age |
name := nameInput text trimBoth.
age := ageInput number.
(name isEmpty) ifTrue: [
^ UIManager default inform: 'Name cannot be empty.'.
].
(age isNil or: [ age < 0 ]) ifTrue: [
^ UIManager default inform: 'Please enter a valid non-negative age.'.
].
"Update the person object"
person name: name.
person age: age.
"Close the dialog successfully"
dialogPresenter accept
PersonEditor >> cancel
"Called when Cancel button is pressed"
dialogPresenter cancel4. The Main Browser (PersonBrowser)
This is the main window showing the list and CRUD buttons.
Object << #PersonBrowser
slots: { #listPresenter . #addButton . #editButton . #deleteButton . #selectedPerson . #mainPresenter . #windowPresenter};
package: 'SpecCRUD-Example-UI-Composition'Implement the main UI:
Class side:
PersonBrowser class >> open
"Convenience method to open the browser"
<script>
^ self new openInstance side:
PersonBrowser >> initialize
"Initialize the browser logic and create managed presenters"
super initialize.
self initializePresenters.
self initializeLayout.
self updatePersonList. "Load initial data"
PersonBrowser >> initializeLayout
"Define the layout and set it on the managed main presenter"
| layout |
layout := SpBoxLayout newVertical
add: listPresenter;
add: (SpBoxLayout newHorizontal
add: addButton expand: false;
add: editButton expand: false;
add: deleteButton expand: false;
yourself)
expand: false;
yourself.
mainPresenter layout: layout.
PersonBrowser >> initializePresenters
"Create the main presenter and the UI components it will contain"
mainPresenter := SpPresenter new. "The presenter holding the layout"
listPresenter := SpListPresenter new.
listPresenter display: [ :item | item displayString ].
listPresenter whenSelectionChangedDo: [ :selection | self updateSelection: selection ].
addButton := SpButtonPresenter new
label: 'Add';
icon: self getIconAdd;
action: [ self addPerson ].
editButton := SpButtonPresenter new
label: 'Edit';
icon: self getIconEdit;
action: [ self editPerson ];
disable. "Initially disabled"
deleteButton := SpButtonPresenter new
label: 'Delete';
icon: self getIconDelete;
action: [ self deletePerson ];
disable. "Initially disabled"
self updatePersonList "Load initial data"
PersonBrowser >> updatePersonList
"Refresh the list from the repository and maintain selection if possible"
| previouslySelected |
previouslySelected := selectedPerson.
listPresenter items: PersonRepository current allPersons.
"Try to re-select the previously selected item"
(previouslySelected notNil and: [ listPresenter items includes: previouslySelected ])
ifTrue: [ listPresenter selectItem: previouslySelected ]
ifFalse: [ self updateSelection: listPresenter selection ] "Update button states even if selection cleared"
PersonBrowser >> updateSelection: aSelectionPresenter
"Called when the list selection changes"
selectedPerson := aSelectionPresenter selectedItem.
editButton enabled: selectedPerson notNil.
deleteButton enabled: selectedPerson notNil
PersonBrowser >> addPerson
"Open the editor to add a new person"
| newPerson |
newPerson := PersonEditor openModalFor: nil title: 'Add New Person'.
newPerson ifNotNil: [
PersonRepository current addPerson: newPerson.
self updatePersonList.
listPresenter selectItem: newPerson "Select the newly added item"
]
PersonBrowser >> editPerson
"Open the editor to edit the selected person"
| editedPerson originalPerson |
selectedPerson ifNil: [ ^ self ]. "Should not happen if button is enabled, but be safe"
originalPerson := selectedPerson. "Keep track of the original instance"
editedPerson := PersonEditor openModalFor: originalPerson title: 'Edit Person'.
editedPerson ifNotNil: [
"Update the original person object in the repository with the new data"
PersonRepository current updatePerson: originalPerson with: editedPerson.
self updatePersonList. "Refresh the list"
listPresenter selectItem: originalPerson "Re-select the edited item (it's the same instance)"
]
PersonBrowser >> deletePerson
"Delete the selected person after confirmation"
| personToDelete |
personToDelete := selectedPerson.
personToDelete ifNil: [ ^ self ]. "Should not happen"
(UIManager default confirm: 'Are you sure you want to delete ', personToDelete displayString, '?')
ifTrue: [
PersonRepository current removePerson: personToDelete.
selectedPerson := nil. "Clear selection reference"
self updatePersonList "Refresh the list"
]
ifFalse: [ ^ nil ]