digitallyinduced / ihp

🔥 The fastest way to build type safe web apps. IHP is a new batteries-included web framework optimized for longterm productivity and programmer happiness
https://ihp.digitallyinduced.com/
MIT License
4.89k stars 193 forks source link

SSC: Support Passing In `initialValue` During Instantiation #936

Open s0kil opened 3 years ago

s0kil commented 3 years ago

componentFromState @Counter (Counter { value = 100 }) should use the provided initial state instead of relying on the initialValue defined in the Counter component module.

https://ihpframework.slack.com/archives/C01DQE0F4F8/p1624537056154200

MurakamiKennzo commented 2 years ago

any updated here?

mpscholten commented 2 years ago

Not yet 👍

MurakamiKennzo commented 2 years ago

😭, What's the main problem here, I read some ssc code just now and think It's may not a tricky.

image

I think the main problem is the type of run function, what about change like this:

run :: (?state :: IORef state, ?context :: ControllerContext, ?applicationContext :: ApplicationContext, ?modelContext :: ModelContext, ?connection :: Websocket.Connection) => state -> IO ()

and change initState like this:

initialState :: Maybe State -> State

Help wanted!

fidel-ml commented 2 years ago

What about something as this:

class Component state action configData | state -> action, state -> configData where
  initialState :: configData -> state
  ...

Then you may have both static and dynamic configuration data for the components (as you are able to choose the configData...). For example

instance Component Counter CounterController Int where
  initialState n = Counter { value = n }
mpscholten commented 2 years ago

I like this idea. It's very close to how react.js works as well 👍

s0kil commented 2 years ago

With class Component state action configData | state -> action, state -> configData where it will require ConfigData to be provided on every instance of the component, correct? component @Counter configData, I can no longer do component @Counter, or am I misunderstanding something?

mpscholten commented 2 years ago

yep that's correct. If you set configData = () you could use component @Counter () as a workaround to get the old behaviour

fidel-ml commented 2 years ago

You may also provide a defaultConfig, and let component use it, and another componentWith for the new behaviour...

leobm commented 1 year ago

Hi @fidel-ml, I had tested what you have suggested. But notice that the idea does not work quite as thought. @MurakamiKennzo is right, the problem is the run method where the initialState which is executed at ComponentsController initialization. At that point I don't have the props value yet. Somehow when I call IHP.ServerSideComponent.ViewFunctions componentWithProps the value I pass must be stored in the ControllerContext or somehow? But I have too little understanding of haskell that I probably can't solve this. Who can help?

Here see you what I have: https://github.com/leobm/ihp/commit/13a4de7aae1b682642bf9597e26f91d7cc839f3a

Edit: You could maybe put the props as data attribute into the html and send it somehow with the first websocket call and set it before you change the state. But this is probably a stupid idea....

mpscholten commented 1 year ago

You could maybe put the props as data attribute into the html and send it somehow with the first websocket call and set it before you change the state. But this is probably a stupid idea....

I think this might actually be a good option here.

The ControllerContext cannot be used as we're having two requests here. The first that renders the SSC componentWithProps Counter { value = 100 }. Then the JS connects via the WebSocket which is a second request.

We've had a similiar problem with IHP AutoRefresh (keeping state across multiple requests). AutoRefresh solves this by having a server-global session store. On the first request it creates a new AutoRefreshSession data type and stores it into the session store. It also puts a unique ID of the AutoRefreshSession into the first response via a meta tag. The AutoRefresh JS now passes the ID onto the WebSocket. The WebSocket handler looks up the correct session by the ID. This way we share state across multiple requests. Passing the state around here explicitly via JSON doesn't work here as the state here is the ControllerContext which cannot be serialized as JSON.

leobm commented 1 year ago

Hello Marc @mpscholten ,

I have played around a bit today. I now pass the props in the websocket URL on the first request and then set the state in the ComponentsController over it.

My example runs with it. But I really only have rudimentary Haskell knowledge, so the code is probably really bad. I've somehow managed to get there with a lot of trial and error. Here is the branch with my changes. https://github.com/leobm/ihp/tree/feature-component-props

I also wrote a test counter component. https://github.com/leobm/ihp-test-component-with-props

but somehow I didn't manage to link to my feature branch in default.nix. I have included my feature branch for testing simply over an IHP subdirectory.

Both projects compile. I have only strangely in Visual Code (Haskell plugin) with the language server problems. Get then e.g. Web.Component.Counter File the following error displayed: "- Expected kind '* -> Constraint', but 'Component Counter CounterController' has kind 'Constraint'.

do you know where the problem is?

Edit: The way I solved it is of course a bit of a hack. In the URL I can't send any amount of data, maybe 2000 characters. For all cases this will not be enough of course.