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 := nil

Instance 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 cancel

4. 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 open

Instance 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.

⚠️ **GitHub.com Fallback** ⚠️