18alantom / strawberry

Zero-dependency, build-free framework for the artisanal web.
https://strawberry.quest
MIT License
681 stars 15 forks source link

Golang example ? #26

Open gedw99 opened 1 year ago

gedw99 commented 1 year ago

@18alantom

The new docs are really good.

I am keen to try to do an example that integrates a golang backend with Strawberry.

Could you possible outline how the API should work between the 2 layers ?

I am keen to see how the state between the 2 layers are synergistic. My plan is for all mutations to go to backend , and somehow update the json on the frontend when it changes. SSE or web sockets…

I was thinking of a nice simple example like a kanban that has projects and items and tags . A tag is the kanban lane.

If you want a different example just let me know also.

gedw99 commented 1 year ago

The concept I am working with is to use htmx to update the frontend with html or Json Data.

typically html is pushed, and api wanted to try pushing new templates at runtime

18alantom commented 1 year ago

Thanks!

I'll attempt sketching a super simple kanban. Starting with the shape of the reactive object:


type Item = {
  title: string;
  details: string;
};

type Lane = {
  title: string;
  items: Item[];
};

type Project = {
  title: string;
  lanes: Lane[];
};

declare const sb: { data: Project };

Note, I've normalized the shape. It can be denormalized to have everything stored on a single Item object which would map to a single table, but that wouldn't match the a kanban UI.

Initialization

Considering that only one project would be open at a time, the main reactive object can be the Project. Loading an app sets up an empty project:

sb.data = {
  title: '' /* placeholder title */,
  lanes: []
}

A project is fetched from the server on connecting. It can be directly set onto sb.data.

sb.data = {
  title: 'The Board',
  lanes: [
    {
      title: 'To Do',
      items: [
        { title: '...', details: '...' },
      ],
    },
    { 
      title: 'Done',
      items: [] 
    },
  ],
};

Propagating updates

There'll be a couple of functions to update the data, a few examples:


function addItem(item: Item, laneTitle: string);
function shiftItem(itemTitle: string, toLane: string);
function deleteItem(itemTitle: string);

These function will do just one thing and that is update the reactive object, for example:

function addItem(item: Item, laneTitle: string) {
  const lane = data.lanes.find(({ title }) => title === laneTitle)
  lane?.items.push(item)
}

This way, they can be used for both:

  1. UI event listener handlers.
  2. WebSocket message event listeners.

For example:


addItemButton.addEventListener('click', () => {
  const item = getNewItem()        // Read from input elements
  const laneTitle = getLaneTitle() // Read from input element/parent

  // UI to reactive object
  addItem(item, lane)

  // UI to socket
  socket.send(
    JSON.stringify({
      message: 'add-item',
      data: {item, laneTitle}
    })
  )
})

socket.addEventListener('message',  (event)  => {
  const data = JSON.parse(event.data)

  // socket to reactive object (by extention UI)
  if (data.message = 'add-item') {
    const {item, laneTitle} = data.data
    addItem(item, laneTitle)
  }
})

In the above example, changes applied to the reactive object propagate to the UI. But not to the socket. This is done in the UI event listeners

Complications

A couple of complications, if required.

Propagate from reactive object to socket

The reactive object can be made the center of updates such that any changes to it are propagated to the socket too, using sb.watch (yet to be documented).

sb.watch('lanes.0.items', () => { /* do something */})

The issue here is that since sb.watch captures updates at the parent object level, if any child element—irrespective of nesting—is updated, it's hard to pinpoint the nature of the update.

For instance a shiftItem operation consists of several atomic operations, for example:

1. remove item from source lane   O(1)
2. adjust source lane             O(n_source)
3. set item in dest lane          O(1)
4. adjust dest lane               O(n_dest)

It'd be highly imprudent to transmit all of those changes, depending on the kanban structure in the database, it could be as simple as just updating a single cell.

And so it's better to handle UI to socket propagation in the in the UI event listener.

Optimistic updates

The example above uses optimistic updates. UI is updated (by updating the reactive object) after which the the socket message is sent.

If the update fails on the server side, then a message will be regarding this failure will be sent.

Failure can be handled in multiple ways, for instance sending the old state which can be assigned to the reactive object (which'll update the UI). Along with a toast indicating the nature of the error.

Denormalizing items, lanes, projects to a single object

If the data is to be denormalized to single objects to say, have a simpler table structure, the UI side can be dependent on computed values. This would simplify updates.


I'm not sure how this factors in with the usage of HTMX, as per my understanding when using HTMX, data operations such as addItem, shiftItem, etc would be handled server side and the updated HTML is returned to update the UI.

W.r.t pushing templates, you can register them using sb.register (yet to be documented), example:

sb.register(`
  <template name="blue-p">
    <p style="color: blue;">
      <slot />
    </p>
   </template>
`);

but you can't re-register them, limitation of the WebComponent spec.