pkamenarsky / concur-replica

Server-side VDOM UI framework for Concur
BSD 3-Clause "New" or "Revised" License
139 stars 20 forks source link

Using concur-replica for components #34

Open LukaHorvat opened 4 years ago

LukaHorvat commented 4 years ago

What would be the easiest way to do this?

Say I'm writing an app in Haskell but a part of the app calls for a code editor. I'm not going to reimplement that in concur-replica so I'd like to just include that JS component where from the concur perspective it's just a widget that finishes once the user submits what's written. What I'd also like is a way to then have smaller concur components inside the JS code editor. For example, autocomplete dropdowns or things like that. So a Haskell wrapper for JS components and a JS wrapper for Haskell components.

Do you think this is feasible?

pkamenarsky commented 4 years ago

Yeah, I've thought about this as well, here are some experiments regarding this in the Replica repo. The basic idea is to piggyback on the evend dispatch -> server -> VDOM patch -> client loop, so that components send custom events to the server (e.g. a ZoomIn event) and then the server sends an updated custom patch to the component (e.g. "update yourself with these location pins").

However, I suspect it would be now possible to do that reusing the new JS FFI functionality, without having to patch Replica.

EDIT:

then have smaller concur components inside the JS code editor

That's a cool idea! Shouldn't be too hard in principle, but needs some thought.

LukaHorvat commented 4 years ago

I'd love it if you gave it a shot. I took a look at the JS code but it seemed just a bit too involved to do without knowing the details.

pkamenarsky commented 4 years ago

Note to self, investigate custom components.

YuMingLiao commented 4 years ago

Hi, I have tried custom components with this code and found something.

Only connectedCallback (when custom element show without previous same custom element) and disconnectedCallback (when custom element is not included in new doms.) works. I can't trigger attributeChangedCallback and adoptedCallback by concur-replica syntax.

{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
import Concur.Core 
import Concur.Replica hiding (i)
import Replica.VDOM (defaultIndex) 
import Prelude hiding (div, id)
import Data.Text 
import Network.WebSockets.Connection (defaultConnectionOptions)
import Network.Wai as Wai
import Network.Wai.Middleware.Static as Static
import Data.String.Interpolate

customElement :: Widget HTML a
customElement = do
  _ <- orr [el "custom-element" [textProp "port" "1"] [], () <$ button [onClick] [text "click me"]] -- connectedCallback is called when the element appears first time. getAttribute gets 1.
  _ <- orr [el "custom-element" [textProp "port" "2"] [], () <$ button [onClick] [text "click me"]] -- nothing is called.
  _ <- orr [el "undefined-name" [textProp "port" "3"] [], () <$ button [onClick] [text "click me"]] -- disconnectedCallback is called. getAttribute gets 2.
  el "custom-element" [textProp "port" "4"] []                                                      -- connectedCallback is called. getAttributes gets 4.

defineCustomElement :: Text
defineCustomElement = [i|
customElements.define('custom-element',
  class extends HTMLElement {
    constructor() { super(); }
    connectedCallback() { this.updateElement(); console.log('connected');}
    attributeChangedCallback() { this.updateElement(); console.log('attributeChanged');}
    disconnectedCallback() { this.updateElement(); console.log('disconnected');}
    adoptedCallback() { this.updateElement(); console.log('adopted');}
    static get observedattributes() { return ['port']; }

    updateElement()
    {
      const port = this.getAttribute('port');
      alert(port);
    }
  }
);
|]
main :: IO ()
main = do
        run 8080
           (defaultIndex "custom element testing" [])
           defaultConnectionOptions
           static
           $ \_ -> div [] [customElement,script [] [text defineCustomElement]]
pkamenarsky commented 4 years ago

Ah, I think you misspelled observedattributes (it should be observedAttributes). Also, if my understanding is correct,adoptedCallback will never be called - replica only inserts or deletes elements, it doesn't move them.

YuMingLiao commented 4 years ago

@pkamenarsky , Oops. Thanks for spotting that. Now attributedChangedCallback is called before every connectedCallback, but not before disconnectedCallback, as expected.

pkamenarsky commented 4 years ago

I suspect this is because you haven't registered undefined-name as a custom component. In your example above, you should get these callbacks in this order:

connectedCallback, attributeChangedCallback, disconnectedCallback, connectedCallback.

YuMingLiao commented 4 years ago

Actually, after I registered the second element, it becomes:

(before clicking any button) attributeChanged connected

(first click) attributeChanged

(second click) disconnected second attributeChanged second connected

(third click) second disconnected attributeChanged connected

it seems it will always call attributeChangedCallback before it calls connectCallback, even in the first show when there is no any button clicking.

I don't know if it is right. But it makes sense to me. I can live with that.

YuMingLiao commented 4 years ago

And if I don't register the second element, I get: (click 0) attributeChanged connected

(click 1) attributeChanged

(click 2) disconnected

(click 3) attributeChanged connected

I will always get one more attributeChangedCallback before it connects. Other than that, I get the order you specified.

pkamenarsky commented 4 years ago

Yeah, that seems about right - see https://github.com/pkamenarsky/replica/blob/master/js/client.ts#L370. When inserting a new node we first attach its children, then set the attributes and as a last step add it to the DOM.

It's somewhat strange that custom components will call the attributeChanged callback even before being added to the DOM, but I guess that's what the spec says.