rabbibotton / clog

CLOG - The Common Lisp Omnificent GUI
Other
1.48k stars 101 forks source link

Callbacks implementation #265

Open mmontone opened 1 year ago

mmontone commented 1 year ago

Hi,

I'm creating this pull request not so much for you to merge, but for discuss it, or discuss other options. I think I may be stretching the limits of CLOG a bit, and so I need something like this. Or I'm wrong, and there's an alternative way of doing what I need with current CLOG already.

The issue is with the integration of some javascript components, where I need to execute some arbitrary javascript on the client, following some particular JS api, and at the same time bind some function server side. So, I cannot use set-event family functions, I need to follow an api. And also I need to access arbitrary data sent from client side (and the data may be had to be extracted right from the client side event).

So I came up with this implementation of callbacks. Callback functions are defined via defcallback and then invoked from javascript via call-callback (call-callback actually generates the javascript needed to call the callback function).

For example, this is how I'm using it for a graph component I'm trying to integrate:

Define a callback function. args is an arbitrary json object sent from the client:

(clog-callbacks:defcallback graph-double-click (args)
  (let* ((node-id (car (access:access args :nodes)))
     (obj (nth node-id *objects*)))
    (show obj)))

Generate js code following a js component api that calls the server-side callback and sends arbitrary js arguments to it:

(defun graph-set-on-double-click (graph stream)
  "Handle double click events for GRAPH."
  (format stream "~a.on('doubleClick', function (params) {
params.event = '[original event]';
~a
})"
      (js-handle graph)
      (clog-callbacks:call-callback 'graph-double-click "JSON.stringify(params)")))

Unfortunately the implementation needs to touch clog-connection::handle-message. But let's discuss if possible.

Thank you.

mmontone commented 1 year ago

Instead of defining a callback and then calling it, it is possible to do it at once just passing a lambda expression, and so the callback has access to the context in which it was created, something that is very needed, like this:

(defun graph-set-on-double-click (graph stream)
  "Handle double click events for GRAPH."
  (let ((callback-call (clog-callbacks:call-callback
            (lambda (args)
              (let* ((node-id (car (access:access args :nodes))))
                (show (find node-id (graph-nodes graph) :key #'car))))
            "JSON.stringify(params)"))
        (js-handle (js-handle graph)))
    (write-string
     (interpol
      "${js-handle}.on('doubleClick', function (params) {
        params.event = '[original event]';
        ${callback-call}
})")
     stream)))
mmontone commented 1 year ago

One thought: if handle-message were extensible (replace the COND with a table dispatching that matches on the prefix message character). Like #\E for event. I could add #\C for callbacks to that table. And then I could plug this in without modifying Clog.

rabbibotton commented 1 year ago

I keep getting a bit delayed and flying for next 3 days. I will take a good look and give input soon.