A Fullstack Example

a CRUD fullstack demo

_images/fullstack.png

An on-line demo is available here. It uses a mock data source (see below) instead of an actual backend.

Datasources

So, by now we have talked about Qt-style widgets that have signals and slots.

Datasources are things that handle the interface between the backend and the frontend and we can use the same signal/slot paradigm in their case. Consider the following:

  • We send a signal to datasource’s slot in order to request data

  • Datasource sends a signal carrying the data

  • Datasource has slots for CRUD operations

  • Datasource sends signals indicating error in the intercom with the backend

  • You can switch between an actual live datasource and a hard-coded dummy/mock datasource: they have exactly the same API

And last but not least:

  • Datasource sends a signal carrying the datamodel, i.e. metadata describing the actual data (fields, types, etc.)

  • Downstream widgets receiving the datamodel signal adjust their fields and forms to the datamodel, reducing the need to hard-code CRUD operations for different kinds of data

Here we provide a fullstack example featuring:

  • A FastAPI Python backend (works with both postgresql and litesql)

  • A frontend capable of doing CRUD operations on the backend

  • Frontend features three widgets: list, details card and three CRUD buttons (new, delete, update)

  • A datasource with the nice features just discussed above

Backend has a script that uses the backend’s pydantic data schemas to produce automagically CuteFront datasources which you can integrate directly into the frontend code

Said that, documenting these features in detail is out of the scope of this (frontend) library, so you’ll need to study the code instead.

Installing the Backend

For Plainfile (1) and Native (2) development modes (see below), you need first to install the backend into to your system:

sudo apt-get install sqlite3 nginx
virtualenv -p python3 venv
source venv/bin/activate
cd backend
pip3 install --user -e .
cd ..
my-backend db --drop --init --revision --upgrade

i.e., installing sqlite3 and nginx, creating a python virtualenv (skip if you feel so), installing the backend package and finally, initializing the database

1. Plainfile Run

Please read first about the “Plainfile” development environment in here and create a dedicated development profile.

Here we will just access bare .html and .js files. Typically you wouldn’t start a backend either, but work with mock data (in early development phases).

Run the backend with (emphasis on --cors):

my-backend run --cors

Backend API endpoints are now available in http://0.0.0.0:8080/docs

Point your browser to frontend/app.html (i.e. with the file://abs/path/to/ file url):

file://abs/path/to/app.html?source=http://0.0.0.0:8080

You can also use a mock (hardcoded debug datasource) instead of an actual backend with

file://abs/path/to/app.html

(i.e. dropping that ?source=http://0.0.0.0:8080 in the end)

Using mock data is a good option in the first stages of development

2. Native Run

Start backend with:

my-backend run

And in another terminal frontend with:

cd frontend
./nginx.py

Go with browser to http://0.0.0.0:8086/?source=

Backend API endpoints are avail in http://0.0.0.0:8086/docs

3. Docker Run

No extra installations (except for having docker-compose installed into your system) is required.

Please use the no-brainer bash scripts provided in directory script:

script/dev.bash build
script/devreset.bash
script/dev.bash up

That builds the images, inits the database and starts the containers

Go with your browser to http://localhost:8086/?source=

Backend API endpoints are available in http://0.0.0.0:8086/docs

Since we’re using very generic container names (“frontend” & “backend”), you might need to do this:

docker container prune

in order to get rid of earlier containers with identical names.

Internals

Here’s a nice HIMO diagram for understanding the fullstack example: it shows the signal-slot connections between the different widgets in the code.

Please also note how the global state is partitioned and cached into the widgets themselves (this was discussed also here):

Main html

    - Creates object instances
    - Create signal/slot connections (see ## below)
    - Calls DataSource.read_slot()

    - CardWidget & FormWidget adapt to the Datamodel
    - ListWidget is insensitive to the Datamodel
    - ListItemWidget must be subclassed for each Datamodel

    PersonListWidget (subclassed from ListWidget)
        // Shows a list of datums
        // A single datum can be (un)chosen
        STATE:
            - ListItemWidget(s)
            - Reference to active ListItemWidget
        UP:
            - signals.current_datum // carries complete datum (with uuid)
                // of currently chosen element.  null = nothing chosen
                ## connect to CardWidget.current_datum_slot
                ## connect to FormWidget.current_datum_slot
                ## connect to CrudButtonsWidget.current_datum_slot
        IN:
            - datums_slot // input: array of all datums
                - Recreate all ListItemWidget(s)
                - Set all ListItemWidget(s) to deactivated
                - Activate one of them if there was a chosen ListWidget item & uuids match (by calling activate_slot)
            - activate_slot
                - For all items, call ListItemWidget.deactivate_slot
                - For one item, call ListItemWidget.activate_slot
                - signals.current_datum.emit

        PersonListItemWidget (internal, subclassed from ListItemWidget)
            // Data and html code corresponding to a single datum
            // if chosen, highlighted
            STATE:
                - cached datum
            UP: signals.clicked // carries uuid of activated item
                ## connect to parent ListWidget.activate_slot
            IN:
                - deactivate_slot
                - activate_slot
            getters:
                - getDatum() // returns cached datum
        ListItemWidget
            ...
        ListItemWidget
            ...
        ...

    CardWidget
        // Shows detailed / all data of a datum
        STATE: cached datum
        IN:
            - datamodel_slot // tells cardwidget to which datamodel to adapt
            - current_datum_slot // input: complete datum (or null)

    FormWidget
        // A dialog that can be used to create or update datums
        STATE:
            - cached datum data in html form fields
            - cached uuid of the datum.  uuid == null means "create" mode
        UP:
            - signals.create // carries new datum
                ## connect to DataSource.create_slot
            - signals.update // carries updated datum (with uuid)
                ## connect to DataSource.update_slot
        IN:
            - datamodel_slot // tell form widget to which datamodel to adapt
            - create_slot // open the popup in "create" mode (check uuid == null)
            - update_slot // open the popup in "update" mode (check uuid != null)
            - current_datum_slot // input: complete datum (or null)
                - alters the state by setting data into the html form fields
                - null input clears the fields

    DataSource
        // Handles all CRUD operations with the backend
        UP: signals.data // carries array of datums
            ## connect to ListWidget.datums_slot
        IN:
            - create_slot // input: a new datum (without uuid)
            - read_slot // tell data source to refresh itself.  no input
            - update_slot // input: updated datum (with uuid)
            - delete_slot // input: datum uuid
        // NOTE: all slots do signals.datums.emit

    CrudButtonsWidget
        // A group of buttons: "new", "update", "delete"
        STATE: visilibity of the buttons: delete & update are deactivated if current_datum_slot got null
        UP:
            - signals.create
                ## connect to FormWidget.create_slot
            - signals.update
                ## connect to FormWidget.update_slot
            - signals.delete
                ## connect to DataSource.delete_slot
        IN: current_datum_slot // input: currently chosen datum (or null)