CRUD App using subclassing - kendmaclean/pharo12 GitHub Wiki
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)
Okay, let's create a simple CRUD (Create, Read, Update, Delete) application example using Pharo 12 and the Spec2 UI framework.
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.
SpPresenter << #Person
slots: { #name . #age };
package: 'SpecCRUD-Example-Model'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-Model'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.
SpDialogPresenter << #PersonEditor
slots: { #person . #nameInput . #ageInput . #okButton . #cancelButton . #title };
package: 'SpecCRUD-Example-UI'Implement the UI layout and logic:
Class side:
PersonEditor class >> openModalFor: aPerson title: aString
"Open the editor modally.
If aPerson is nil, we are creating a new Person.
Otherwise, we are editing aPerson.
Returns the edited/created person if OK is pressed, nil otherwise."
| instance result |
instance := self new.
instance title: aString.
instance person: (aPerson ifNil: [ Person new ] ifNotNil: [ aPerson copy ]). "Edit a copy"
result := instance openModal. "Blocks until closed"
^ result ifNotNil: [ instance person ] ifNil: [ nil ] "Return the person only if accepted"Instance side:
PersonEditor >> defaultLayout
"Define the layout: labels and inputs vertically, buttons horizontally at the bottom"
^ 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
PersonEditor >> initializePresenters
"Create the UI components"
nameInput := self newTextInput.
ageInput := self newNumberInput
minimum: 0; "Optional: Set minimum age"
maximum: 150; "Optional: Set maximum age"
step: 1; "Optional: Spinner step"
yourself.
okButton := self newButton
label: 'OK';
icon: (self iconNamed: #smallOk);
action: [ self ok ].
cancelButton := self newButton
label: 'Cancel';
icon: (self iconNamed: #smallCancel);
action: [ self cancel ].
self focusOrder
add: nameInput;
add: ageInput;
add: okButton
PersonEditor >> initializeWindow: aWindowPresenter
"Configure the dialog window"
super initializeWindow: aWindowPresenter.
aWindowPresenter
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"
self accept
PersonEditor >> cancel
"Called when Cancel button is pressed"
super cancel4. The Main Browser (PersonBrowser)
This is the main window showing the list and CRUD buttons.
SpPresenter << #PersonBrowser
slots: { #listPresenter . #addButton . #editButton . #deleteButton . #selectedPerson };
package: 'SpecCRUD-Example-UI'Implement the main UI:
Class side:
PersonBrowser class >> open
"Convenience method to open the browser"
<script>
^ self new openInstance side:
PersonBrowser >> defaultLayout
"List on top, buttons below"
^ SpBoxLayout newVertical
add: listPresenter;
add: (SpBoxLayout newHorizontal
add: addButton expand: false;
add: editButton expand: false;
add: deleteButton expand: false;
yourself)
expand: false;
yourself
PersonBrowser >> initializePresenters
"Create and configure the UI components"
listPresenter := self newList.
listPresenter display: [ :item | item displayString ]. "How to show persons in the list"
listPresenter whenSelectionChangedDo: [ :selection | self updateSelection: selection ].
addButton := self newButton
label: 'Add';
icon: (self iconNamed: #smallAdd);
action: [ self addPerson ].
editButton := self newButton
label: 'Edit';
icon: (self iconNamed: #smallEdit);
action: [ self editPerson ];
disable. "Initially disabled"
deleteButton := self newButton
label: 'Delete';
icon: (self iconNamed: #smallDelete);
action: [ self deletePerson ];
disable. "Initially disabled"
self updatePersonList "Load initial data"
PersonBrowser >> initializeWindow: aWindowPresenter
"Configure the main window"
aWindowPresenter
title: 'Person CRUD Example';
initialExtent: 400 @ 300
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"
(SpUIManager default confirm: 'Are you sure you want to delete ', personToDelete displayString, '?' title: 'Confirm Deletion')
ifTrue: [
PersonRepository current removePerson: personToDelete.
selectedPerson := nil. "Clear selection reference"
self updatePersonList "Refresh the list"
]
ifFalse: [ ^ nil ]How to Use:
To run the Application: Execute the following code in a Playground:
smalltalk PersonBrowser open
Or, select the expression PersonBrowser open and press Cmd+Shift+O (macOS) or Ctrl+Shift+O (Windows/Linux) for the "Open" command.