Comments (18)
Basically something like this, if this makes sense
data Respond a b = Respond a | Continue b
listen :: forall a b r p f. (Functor f) => (a -> r) -> (b -> r) -> HTML p (f (Respond a b)) -> HTML p (f r)
Which essentially looks like mapping either
or something. I don't really like using Either
everywhere though for something like this since we already use it for the request handler, but that's just me.
from purescript-halogen.
Here's what I landed on
module App where
import Data.Tuple
import Data.Maybe
import Control.Monad.Eff
import Halogen
import Halogen.Signal
import qualified Halogen.HTML as H
import qualified Halogen.HTML.Attributes as A
import qualified Halogen.HTML.Events as A
import qualified Halogen.HTML.Events.Forms as A
import qualified Halogen.HTML.Events.Handler as E
data Respond a b = Respond (Maybe a) b
listen :: forall a b r. (Maybe a -> b -> r) -> Respond a b -> r
listen f (Respond a b) = f a b
---
type CountState = Number
data CountEvent = Counted
data CountInput = Count
countTil :: Number -> CountState -> H.HTML _ (Respond CountEvent CountInput)
countTil til n =
H.div_
[ H.button (A.onclick \_ -> pure $ handleClick (n + 1)) [ H.text "count" ]
, H.text (show n) ]
where
handleClick n | n == til = Respond (Just Counted) Count
handleClick n | otherwise = Respond Nothing Count
countUpdate :: CountState -> CountInput -> CountState
countUpdate n _ = n + 1
---
type State =
{ counted :: Boolean
, counter :: CountState }
data Input = CInput (Maybe CountEvent) CountInput
view :: PureView Input
view = render <$> stateful { counted: false, counter: 0 } update
where
render state =
H.div_
[ listen CInput <$> countTil 5 state.counter
, if state.counted
then H.text "Counted!"
else H.text "Not counted" ]
update state (CInput e c) = case e of
Just Counted -> state { counter = countUpdate state.counter c, counted = true }
_ -> state { counter = countUpdate state.counter c }
from purescript-halogen.
This looks like a good way of passing information from child to parent. It's a bit bulky but gets the job done.
from purescript-halogen.
Any other suggestions would certainly be welcome. The main problem is that you have to always be able to pass along the component state. I had originally started with an Either
variant, but that means you could only update the state or bubble an event, not both at the same time. Which means that the example above would stop working. Ideally I would like the interface of listen CEvent CInput <$> component
so I wouldn't have to pattern match on state/event at the same time, but that would require you to run the update function twice (once for the component state, and once for the event).
from purescript-halogen.
We were chatting about something like this:
type UI p m req res = exists i. SF1 (Either i req) (HTML p (m (Either i res)))
The idea is that a UI can both:
- Accept "requests" of type
req
, and produce "responses" of typeres
; - Generate events from user input and consume them internally without having to do loopback.
It can effectfully produce internal events and respond to requests with some effect type m
, almost certainly going to be Aff e
or Par e
for some set of effects e
. Could also be pure or a free monad.
Although totally unproven at this point, this approach simplifies a lot of things:
- Eliminates a few abstractions in favor of pushing more power into the
View
AKAUI
. - Allows parents to pass information to children.
- Allows children to pass information to parents.
- Eliminates loop back at the cost of not being easily able to mock out user events (which is a key benefit of the current approach).
Thoughts @garyb, @natefaubion, @cryogenian, @paf31?
from purescript-halogen.
- I am afraid I can't see how this approach can make communication simpler. We have
req
andres
in both input and output ofSF1
. - Not sure about
i req res
. It simplifies request handler, but maybe gets too much power to view. - Using monads in
View
will make it identical withpurescript-signal
Channel
approach 😄 One can in example senddriver
in message to other component, and send any message in any time from any function.
In general, I think that current approach can work very nice with some MVI/MVC other MVstuff architecture. Maybe it's worth to make version of View
that will consume i
and produce Html p r
not Html p (Either i r)
to make MVstuff more clean.
Can typeclasses help to aggregate View
s?
from purescript-halogen.
As a thought project, here's how I would use the typical MV* pattern to propagate requests/inputs/events.
-- Event handlers need to be able to propagate user events, new inputs, and
-- effect requests. An Either isn't sufficient since you could potentially
-- want to do all three at the same time. For example, you might want to signal
-- a user event and kick off a request at the same time. Since event bubbling
-- must propagate with the input, you can't do an either/or.
type EventResult = Tuple3
proxy :: forall e i r e' i' r'. (e -> e') -> (i -> i') -> (r -> r') -> EventResult e i r -> EventResult e' i' r'
proxy f g h = uncurry \e i r -> tuple3 (f e) (g i) (h r)
-- Bundles the event and input together so it can be handled in the transition function
listen :: forall e i r i' r'. (e -> i -> i') -> (r -> r') -> EventResult e i r -> EventResult Unit i' r'
listen f g = uncurry \e i r -> tuple3 Unit (f e i) (g r)
-- Drops any events
ignore :: forall e i r i' r'. (i -> i') -> (r -> r') -> EventResult e i r -> EventResult Unit i' r'
ignore f g = uncurry \e i r -> tuple3 Unit (f i) (g r)
---
data ComponentReq = CR1 | CR2 | CR3 (Component2Req)
data ComponentInput = CI1 | CI2 | CI3 (Component2Input)
data ComponentEvent = CE1 | CE2 | CE3
type ComponentState = { status :: Number, child :: Component2State }
renderComponent :: Number -> ComponentState -> HTML _ (EventResult ComponentEvent ComponentInput ComponentReq)
updateComponent :: ComponentState -> ComponentReq -> ComponentState
handleComponent :: forall eff. (ComponentInput -> Aff eff Unit) -> ParentReq -> Aff eff Unit
initialComponent :: ComponentState
---
data ParentReq = PR1 | PR2 | PR3 ComponentReq
data ParentInput = PI1 | PI2 | PI3 ComponentEvent ComponentInput -- Event Bubbling
data ParentState = { name :: String, component :: ComponentState }
renderParent :: ParentState -> HTML _ (EventResult Unit ParentInput ParentReq)
renderParent state =
div_
[ h3 [text state.name]
-- Bundles up event and state for handling
, listen PI3 PR3 <$> renderComponent 42 state.component ]
updateParent :: ParentState -> ParentInput -> ParentState
updateParent state PI1 = ...
updateParent state PI2 = ...
updateParent state (PI3 event compState) =
-- Must handle events and update child state at the same time
case event of
CE1 -> newState "Foo"
CE2 -> newState "Bar"
CE3 -> newState "Baz"
where
newState = { name: _, updateComponent state.component compState }
handleParent :: forall eff. (ParentInput -> Aff eff Unit) -> ParentReq -> Aff eff Unit
handleParent driver PR1 = ...
handleParent driver PR2 = ...
handleParent driver (PR3 r) =
-- Need to pick an event since its bundled with the child state. I suppose
-- We can use a Maybe for the event, or require every component to have some
-- sort of Nil event.
handleComponent (driver <<< (PI3 CI3)) r
initialParent :: ParentState
initialParent =
{ name: ""
, component: initialComponent }
---
type UI p s i r =
{ render :: s -> HTML p (EventResult Unit i r) -- All events must be handled
, update :: s -> i -> s
, handle :: forall eff. (i -> Aff eff Unit) -> r -> Aff eff Unit }
-- runUI can do whatever it needs, the Signal machinery is no longer exposed
-- to the user as everything is manually composed
runUI :: ...
TLDR;
Either
isn't sufficiently expressive because you need to be able to propagate user events and kick off requests at the same time, so I've switched toTuple3
.- This is nice because you can guarantee that all user events are handled somehow (even if explicitly ignored)
- The
SF
machinery is completely unnecessary, and everything just works by convention. This can be either seen as a win since its just dumb functions, or as a lose because you are manually nesting and dispatching everything and can't use any abstractions to reduce boilerplate (this is essentially the Elm architecture with more boilerplate). - Manually nesting/dispatching is very tedious (though completely safe up to exhaustive checking) and requires various forms of composition.
I don't really know how I feel about it. The second you want to bundle everything together in a SF you have to put effects into it, in which case you might as well just bundle everything up as a Widget
that contains its own runUI
invocation and pass drivers around (this is what I originally asked @paf31 about doing).
from purescript-halogen.
Looks interesting.
Tuple3
implies that every click/key press/focus/whatever emits all three things, which seems a little odd.- Do you mean to use
Aff
inhandle
?Eff
should be enough whenAff
gets something likecallcc
.
I have thought about whether SF
is needed too. It's not, because you can express it using functions and an optional existential, but it would involve more plumbing and make composition harder. That said, I'm all in favor of getting something working and then figuring out the abstractions to layer on top later, even if the something is a giant function.
Something here looks very lens-like.
Finally, one more thing jumps to mind about SF
- it should be possible to destructure it as follows, keeping abstraction via the existential it is isomorphic to:
unpackSF :: forall i o r. (forall s. { initial :: s, step :: i -> s -> { output :: o, next :: s } } -> r) -> SF i o -> r
This should allow us to represent a "state machine with hidden state" as SF
, but still unpack it to compose the state abstractly.
from purescript-halogen.
My effect signatures maybe wrong, I didn't typecheck anything :)
You don't have to emit all three things you just pick Unit or something, just like now you don't have to have requests, and you map some branch of Either
over it. If you create a view that doesn't use them and you choose a different result in the event handler, you'd just need to adapt it wherever you embed it.
from purescript-halogen.
Multiple input from one input is what I previously used callcc
for when using Eff
. It would allow these same sort of patterns I think. You could feasibly say things like
- Emit "set busy to true"
- Do some work
- Emit the result
- Emit "set busy to false"
or provide partial progress updates using several inputs, or whatever. Events could probably be handled similarly.
from purescript-halogen.
I am afraid I can't see how this approach can make communication simpler. We have req and res in both input and output of SF1.
That's actually not true. In the existing definition of View
, the purpose of i
is conflated: i
is used by a view to update its own internal state, via loopback (that is, the i
's it generates from input events are fed back to itself so it has a chance to update state and the view). Also, you can use i
to send information from a parent to the child. You can use the output type Either i r
in the existing definition to send information from child to parent.
The conflation of these two inhibits composition because of the need for loopback. It also adds benign boilerplate in the form of having a single sum type include both input events and messages between parent and child.
As a thought project, here's how I would use the typical MV* pattern to propagate requests/inputs/events.
This is a variation of the React theme (which itself is a variation of MVC for some definition of C), although I'd simplify your definition of UI
to something like:
type UI eff p s i r =
{ render :: s -> HTML p (EventResult Unit i r) -- All events must be handled
, update :: s -> i -> s
, handle :: r -> Aff eff i
Again, the i
channel is overloaded for user input events and communication. Among other things, this makes the UI
invariant in i
(as it appears as a function parameter and return value), which inhibits composition in all the usual ways. It's possible to existentially hide s
, but r
is yet another problem as its invariant, mainly because the view itself does not have the power to execute effects directly.
Let me run through my proposed signature one more time:
type UI p m req res = exists i. SF1 (Either i req) (HTML p (m (Either i res)))
- The user input events are existentially hidden, which means they are not visible in the type signature of
UI
. As a result, they do not inhibit composition. Only theUI
itself knows about the user input events it processes and handles to update its own state. - The
UI
can accept information from a parent (req
), and also push information to a parent (res
) in response to some input or a request.UI
s that do not perform any communication can just useVoid
for these parameters. I believe, but am not certain, thatUI
is aProfunctor
inreq
/res
, and therefore has rich composition properties. - The
UI
is allowed to execute effects, either to process user events into a form where they can be incorporated into the state, or to create some information to communicate to a parent. However, the effects can be limited, e.g. a pureUI
is justUI p Identity req res
, which can be lifted toAff eff
to compose with an effectfulUI
. Or effects can be described using a free algebra, and then composed via coproducts, etc. Because theUI
can handle its own effects, there's no need to be invariant in another type parameter. - Aside from input / output, the only
UI
type parameters arep
for placeholder andm
for effects. Effects combine using the more powerful of the effect, while placeholders combine usingEither
(and their renderers also combine in such a nice fashion). So really,p
andm
do not inhibit composition, andreq
andres
permit very rich compositional properties, as well as typed ways of describingUI
s that do not accept requests or which do not produce information in response to requests.
The ability to return many res
, in this formulation or any other one, does seem to be important; I will think about whether adding something like callcc
is the best way to do that or not.
unpackSF :: forall i o r. (forall s. { initial :: s, step :: i -> s -> { output :: o, next :: s } } -> r) -> SF i o -> r
I'm interested in explicit (existential) state. It can improve performance because the exact same function is used for updating the view (rather than creation of more closures which capture more state). You can constraint the state, for example, requiring it be JSON-serializable, if you want, which will lead to other interesting applications. Also if you know the state of a child, because it hasn't been wrapped up yet, then it gives you a way to get information from the child.
Still, with all that said, not exactly sure how it would work.
from purescript-halogen.
I think we can review and maybe close this in light of merging #50.
from purescript-halogen.
I think this is now fixed, as both parent to child and child to parent communication is now possible, although some use cases will depend on graft
as most components will be embedded in other components and the communication story for them is less clear.
from purescript-halogen.
@natefaubion Any comments here, OK closing this one in favor of other issues?
from purescript-halogen.
I don't really get how the machinery in Component
solves this issue though, as the issue is mainly about bubbling events in nested components, so maybe that should be brought up as a more specific issue. Can someone maybe provide an example of how req
and res
are supposed to work to communicate between components?
My immediate thought is that Either
is not expressive enough for the example brought up earlier. You'll want to be able to update internal state and signal an event at the same time. If you can only do one or the other you can't build my example.
from purescript-halogen.
In fact I think it would be good to start adapting our examples to use the compositional machinery so we can get a feel for how it works. Right now they are all stand alone toy components.
from purescript-halogen.
@natefaubion Part of this depends on the graft
story. A parent component would presumably install a child component into itself, in some placeholder(s), and thereby gain the ability to send to and receive from the child component.
e.g. something like:
install :: forall p p' a a' b b' m node.
Component p m node (Either a a') (Either b b') ->
Component p' m node b' a' ->
Component (Either p p') m node a b
install parent child = ...
parent `install `child
Any evidence that the child existed is gobbled up when the child is installed into the parent, and while the raw parent's types show it can communicate to and from the child, once installed, the types do not contain any evidence of such parent-child communication.
Anyway, there's definitely more work to be done here, but I'd say the issue is more about grafting / combinators than the type signatures, which now permit communication even if there aren't yet the combinators to make that happen.
from purescript-halogen.
Ok, that makes sense.
On Mar 29, 2015, at 9:49 PM, "John A. De Goes" [email protected] wrote:
@natefaubion Part of this depends on the graft story. A parent component would presumably install a child component into itself, in some placeholder(s), and thereby gain the ability to send to and receive from the child component.
e.g. something like:
install :: forall p p' a a' b b' m node.
Component p m node (Either a a') (Either b b') ->
Component p' m node b' a' ->
Component (Either p p') m node a b
install parent child = ...parent
install
child
Any evidence that the child existed is gobbled up when the child is installed into the parent, and while the raw parent's types show it can communicate to and from the child, once installed, the types do not contain any evidence of such parent-child communication.Anyway, there's definitely more work to be done here, but I'd say the issue is more about grafting / combinators than the type signatures, which now permit communication even if there aren't yet the combinators to make that happen.
—
Reply to this email directly or view it on GitHub.
from purescript-halogen.
Related Issues (20)
- `tellAll` function is not re-exported in Halogen module
- Enabling `StateT` with `HalogenM` HOT 1
- Export tellAll from Halogen.Query to Halogen HOT 2
- Reading Effects chapter -> Could not match type ResponseFormat String with type AffjaxDriver HOT 4
- Question: how to handle events coming from a js app? HOT 2
- Communication with JS loaded via CDN HOT 2
- Child component gets rendered outside of parent, being moved to bottom of HTML body HOT 6
- `raise` should not be a blocking operation HOT 8
- Discussion: CSS strategy for halogen applications HOT 20
- Question: body-level events HOT 3
- Order of properties matters when using `value` with `min` / `max` for `InputRange` HOT 3
- Change kind of slots to not be `Type`
- `RenderSpec` doc comment still mentions `h` parameter
- Doc: the examples of "An Aff Example: HTTP Requests" give TypesDoNotUnify Error on Halogen v7 HOT 1
- Select Multiple Selected does not work HOT 4
- A bug? Weird behavior of text input fields. HOT 4
- Child component is destroyed while parent component handles output from that child HOT 5
- Array state updates HOT 4
- open and showModal are missing for dialog HOT 6
- The Component type should have role annotations
Recommend Projects
-
React
A declarative, efficient, and flexible JavaScript library for building user interfaces.
-
Vue.js
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
-
Typescript
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
-
TensorFlow
An Open Source Machine Learning Framework for Everyone
-
Django
The Web framework for perfectionists with deadlines.
-
Laravel
A PHP framework for web artisans
-
D3
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
-
Recommend Topics
-
javascript
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
-
web
Some thing interesting about web. New door for the world.
-
server
A server is a program made to process requests and deliver data to clients.
-
Machine learning
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
-
Visualization
Some thing interesting about visualization, use data art
-
Game
Some thing interesting about game, make everyone happy.
Recommend Org
-
Facebook
We are working to build community through open source technology. NB: members must have two-factor auth.
-
Microsoft
Open source projects and samples from Microsoft.
-
Google
Google ❤️ Open Source for everyone.
-
Alibaba
Alibaba Open Source for everyone
-
D3
Data-Driven Documents codes.
-
Tencent
China tencent open source team.
from purescript-halogen.