|
73 | 73 | -- This is the templating function that is used to construct a new virtual DOM |
74 | 74 | -- (or HTML if rendering on the server). |
75 | 75 | -- |
76 | | --- * __update__: @'update' :: action -> 'Effect' parent model action@ |
| 76 | +-- * __update__: @'update' :: action -> 'Effect' parent props model action@ |
77 | 77 | -- The 'update' function handles how the 'model' evolves over time in response |
78 | 78 | -- to events that are raised by the application. This function takes any @action@, |
79 | 79 | -- updating the @model@ and optionally introduces 'IO' into the system. |
|
97 | 97 | -- |
98 | 98 | -- @ |
99 | 99 | -- data 'SomeComponent' parent |
100 | | --- = forall model action . Eq model |
101 | | --- => 'SomeComponent' ('Component' parent model action) |
| 100 | +-- = forall model action props . (Eq model, Eq props) |
| 101 | +-- => 'SomeComponent' props ('Component' parent model action) |
102 | 102 | -- @ |
103 | 103 | -- |
104 | 104 | -- The smart constructors: |
|
127 | 127 | -- import qualified Miso.Html.Property as HP |
128 | 128 | -- ----------------------------------------------------------------------------- |
129 | 129 | -- * - The type of the parent Component 'model' |
130 | | --- | * - The type of the current Component's 'model' |
131 | | --- | | * - The type of the action that updates the 'model' |
132 | | --- | | | |
133 | | --- counter :: 'Component' parent Int Action |
| 130 | +-- | * - The type of the parent Component 'props' accessible to the child |
| 131 | +-- | | * - The type of the current Component's 'model' |
| 132 | +-- | | | * - The type of the action that updates the 'model' |
| 133 | +-- | | | | |
| 134 | +-- counter :: 'Component' ROOT () Int Action |
134 | 135 | -- counter = 'vcomp' m u v |
135 | 136 | -- where |
136 | 137 | -- m :: Int |
137 | 138 | -- m = 0 |
138 | 139 | -- |
139 | | --- u :: Action -> 'Effect' parent Int Action |
| 140 | +-- u :: Action -> 'Effect' ROOT () Int Action |
140 | 141 | -- u = \\case |
141 | 142 | -- Add -> 'this' += 1 |
142 | 143 | -- Subtract -> 'this' -= 1 |
143 | 144 | -- |
144 | | --- v :: Int -> 'View' Int Action |
145 | | --- v x = 'vfrag' |
| 145 | +-- v :: () -> Int -> 'View' Int Action |
| 146 | +-- v _ x = 'vfrag' |
146 | 147 | -- [ H.button_ [ HE.onClick Add, HP.id_ "add" ] [ "+" ] |
147 | 148 | -- , text (ms x) |
148 | 149 | -- , H.button_ [ HE.onClick Subtract, HP.id_ "subtract" ] [ "-" ] |
|
203 | 204 | -- ('+>') |
204 | 205 | -- :: forall child model action a . Eq child |
205 | 206 | -- => 'MisoString' |
206 | | --- -> 'Component' model child action |
| 207 | +-- -> 'Component' model () child action |
207 | 208 | -- -> 'View' model a |
208 | 209 | -- key '+>' vcomp = 'VComp' (Just (toKey key)) ('SomeComponent' vcomp) |
209 | 210 | -- @ |
210 | 211 | -- |
211 | 212 | -- Practically, using this combinator looks like: |
212 | 213 | -- |
213 | 214 | -- @ |
214 | | --- view :: Int -> 'View' Int action |
| 215 | +-- view :: () -> Int -> 'View' Int action |
215 | 216 | -- view x = 'div_' [ 'id_' "container" ] [ "counter" '+>' counter ] |
216 | 217 | -- @ |
217 | 218 | -- |
|
233 | 234 | -- The 'App' type signature is a synonym for 'Component' 'ROOT' |
234 | 235 | -- |
235 | 236 | -- @ |
236 | | --- type 'App' model action = 'Component' 'ROOT' model action |
| 237 | +-- type 'App' model action = 'Component' 'ROOT' () model action |
237 | 238 | -- @ |
238 | 239 | -- |
239 | 240 | -- 'ROOT' is a type tag that encodes a 'Component' as top-level. Which means it has no @parent@, hence we mark @parent@ as 'ROOT'. |
|
244 | 245 | -- |
245 | 246 | -- 'startApp' and 'miso' will always infer @parent@ as 'ROOT'. |
246 | 247 | -- |
| 248 | +-- = Props |
| 249 | +-- |
| 250 | +-- Inspired by [React props](https://react.dev/learn/passing-props-to-a-component), |
| 251 | +-- @miso@ allows a parent 'Component' to pass read-only data down to a child 'Component' |
| 252 | +-- via a mechanism called /props/ (short for /properties/). |
| 253 | +-- |
| 254 | +-- == Props vs. Component-local state |
| 255 | +-- |
| 256 | +-- * __model__: Component-local state. It is owned and mutated exclusively by the 'Component' |
| 257 | +-- itself through its 'Miso.Types.update' function. No other 'Component' can write to it directly. |
| 258 | +-- |
| 259 | +-- * __props__: Data /inherited/ from the @parent@ 'Component'. Props flow downward through |
| 260 | +-- the component hierarchy and are read-only from the child's perspective. The parent decides |
| 261 | +-- what props to pass at mount time; the child cannot mutate them. Props that change in a |
| 262 | +-- the parent cause the child to re-render. |
| 263 | +-- |
| 264 | +-- This mirrors the distinction in React between component state (@useState@) and props |
| 265 | +-- received from above (@function MyComponent({ name }) { ... }@). |
| 266 | +-- |
| 267 | +-- === When to use props |
| 268 | +-- |
| 269 | +-- Props are best suited for /metadata/ — contextual or configuration data that the child needs |
| 270 | +-- to know about but should not own. Good examples: a user's display name, a theme token, a |
| 271 | +-- locale string, or a read-only identifier used to customise rendering. |
| 272 | +-- |
| 273 | +-- If the data drives the child's own business logic — counters it increments, form fields it |
| 274 | +-- edits, async state it manages — that data belongs in the child's @model@ instead. Putting |
| 275 | +-- mutable business-logic state in @props@ would require the parent to own and thread through |
| 276 | +-- every change, creating unnecessary coupling. Prefer @props@ for \"what the child should |
| 277 | +-- know\" and @model@ for \"what the child should do\". |
| 278 | +-- |
| 279 | +-- == Props in 'Miso.Types.view' |
| 280 | +-- |
| 281 | +-- The 'Miso.Types.view' field of a 'Component' always takes @props@ as its first argument: |
| 282 | +-- |
| 283 | +-- @ |
| 284 | +-- view :: props -> model -> 'View' model action |
| 285 | +-- @ |
| 286 | +-- |
| 287 | +-- Top-level applications have no parent, so @props@ is always @()@: |
| 288 | +-- |
| 289 | +-- @ |
| 290 | +-- view :: () -> model -> 'View' model action |
| 291 | +-- view _props model = … |
| 292 | +-- @ |
| 293 | +-- |
| 294 | +-- == Props in 'Effect' \/ 'Miso.Types.update' |
| 295 | +-- |
| 296 | +-- Use 'getProps' inside the 'Effect' monad to read the current value of @props@: |
| 297 | +-- |
| 298 | +-- @ |
| 299 | +-- update :: Action -> 'Effect' parent props Model Action |
| 300 | +-- update = \\case |
| 301 | +-- SomeAction -> do |
| 302 | +-- p <- 'getProps' |
| 303 | +-- 'io_' ('consoleLog' (ms (show p))) |
| 304 | +-- @ |
| 305 | +-- |
| 306 | +-- Alternatively, use the 'Miso.Lens.view' combinator with the 'props' lens: |
| 307 | +-- |
| 308 | +-- @ |
| 309 | +-- update = \\case |
| 310 | +-- SomeAction -> do |
| 311 | +-- p <- 'Miso.Lens.view' 'props' |
| 312 | +-- … |
| 313 | +-- @ |
| 314 | +-- |
| 315 | +-- == 'ROOT' — the top-level 'Component' |
| 316 | +-- |
| 317 | +-- When a 'Component' is passed to 'startApp' (or 'miso') it has no parent. |
| 318 | +-- The @parent@ type is specialized to 'ROOT' and @props@ is fixed to @()@: |
| 319 | +-- |
| 320 | +-- @ |
| 321 | +-- type 'App' model action = 'Component' 'ROOT' () model action |
| 322 | +-- @ |
| 323 | +-- |
| 324 | +-- Because there is no parent to inherit from, @props@ will always be @()@ for a |
| 325 | +-- root-level 'Component'. You can simply ignore the first argument in 'view' and |
| 326 | +-- skip 'getProps' in 'Miso.Types.update'. |
| 327 | +-- |
| 328 | +-- == Passing props to a child 'Component' |
| 329 | +-- |
| 330 | +-- Use 'mountWithProps_' (keyed) or 'mountWithProps' (unkeyed) in the parent's 'view' to |
| 331 | +-- mount a child and supply its props: |
| 332 | +-- |
| 333 | +-- @ |
| 334 | +-- 'mountWithProps_' |
| 335 | +-- :: ('Eq' child, 'Eq' props) |
| 336 | +-- => 'MisoString' |
| 337 | +-- -> props |
| 338 | +-- -> 'Component' parent props child action |
| 339 | +-- -> 'View' parent a |
| 340 | +-- @ |
| 341 | +-- |
| 342 | +-- == Example: child reading parent-supplied props |
| 343 | +-- |
| 344 | +-- The following shows a parent 'Component' that maintains a greeting string in its |
| 345 | +-- @model@ and passes it as @props@ to a child 'Component'. The child renders the |
| 346 | +-- greeting and can also read it from within its 'Miso.Types.update' function. |
| 347 | +-- |
| 348 | +-- @ |
| 349 | +-- ----------------------------------------------------------------------------- |
| 350 | +-- -- The props type: what the parent shares with the child |
| 351 | +-- newtype Greeting = Greeting 'MisoString' deriving ('Eq') |
| 352 | +-- ----------------------------------------------------------------------------- |
| 353 | +-- -- Child component |
| 354 | +-- -- |
| 355 | +-- -- parent props model action |
| 356 | +-- -- | | | | |
| 357 | +-- child :: 'Component' ParentModel Greeting () ChildAction |
| 358 | +-- child = 'vcomp' () updateChild viewChild |
| 359 | +-- where |
| 360 | +-- viewChild :: Greeting -> () -> 'View' () ChildAction |
| 361 | +-- viewChild (Greeting g) _ = |
| 362 | +-- 'div_' [] [ 'text' ("Hello, " <> g <> "!") ] |
| 363 | +-- |
| 364 | +-- updateChild :: ChildAction -> 'Effect' ParentModel Greeting () ChildAction |
| 365 | +-- updateChild = \\case |
| 366 | +-- ReadGreeting -> do |
| 367 | +-- Greeting g <- 'getProps' |
| 368 | +-- 'io_' ('consoleLog' g) |
| 369 | +-- ----------------------------------------------------------------------------- |
| 370 | +-- -- Parent component: owns the greeting, passes it to the child as props |
| 371 | +-- parent :: 'App' ParentModel ParentAction |
| 372 | +-- parent = 'vcomp' (ParentModel "World") 'noop' viewParent |
| 373 | +-- where |
| 374 | +-- viewParent :: () -> ParentModel -> 'View' ParentModel ParentAction |
| 375 | +-- viewParent _ (ParentModel g) = |
| 376 | +-- 'mountWithProps_' "child" (Greeting g) child |
| 377 | +-- ----------------------------------------------------------------------------- |
| 378 | +-- newtype ParentModel = ParentModel 'MisoString' deriving ('Eq') |
| 379 | +-- data ChildAction = ReadGreeting |
| 380 | +-- data ParentAction |
| 381 | +-- @ |
| 382 | +-- |
| 383 | +-- A few things to notice: |
| 384 | +-- |
| 385 | +-- * The child's @parent@ type parameter is @ParentModel@. This must match the |
| 386 | +-- parent 'Component' @model@ type — 'mountWithProps_' enforces this at compile time. |
| 387 | +-- * 'getProps' inside the child's 'Miso.Types.update' yields a @Greeting@, not |
| 388 | +-- the full @ParentModel@. The child only sees what the parent explicitly chose to share. |
| 389 | +-- * The root 'App' always has @props ~ ()@; no extra plumbing is needed when calling 'startApp'. |
| 390 | +-- |
247 | 391 | -- = 'VComp' lifecycle hooks |
248 | 392 | -- |
249 | 393 | -- 'Component' are mounted on the fly during diffing. All t'Component` are equipped with `mount` and `unmount` hooks. This allows the defining of custom actions that will be processed in response to lifecycle events. |
|
271 | 415 | -- |
272 | 416 | -- data Action = Highlight DOMRef |
273 | 417 | -- |
274 | | --- update :: Action -> 'Effect' parent model Action |
| 418 | +-- update :: Action -> 'Effect' parent props model Action |
275 | 419 | -- update = \\case |
276 | 420 | -- Highlight domRef -> 'io_' $ do |
277 | 421 | -- ['js'| hljs.highlight(${domRef}) |] |
|
344 | 488 | -- , 'li_' [ 'key_' "key-2" ] [ "b" ] |
345 | 489 | -- , "key-3" '+>' counter |
346 | 490 | -- , 'textKey' "key-4" "text here" |
347 | | --- , vfrag_ "key-5" [ "foo", "bar" ] |
| 491 | +-- , 'vfrag_' "key-5" [ "foo", "bar" ] |
348 | 492 | -- ] |
349 | 493 | -- @ |
350 | 494 | -- |
|
420 | 564 | -- The 'Effect' type is defined as a 'RWS'. |
421 | 565 | -- |
422 | 566 | -- @ |
423 | | --- type 'Effect' parent model action = 'RWS' ('ComponentInfo' parent) ['Schedule' action] model () |
| 567 | +-- type 'Effect' parent props model action = 'RWS' ('ComponentInfo' parent props) ['Schedule' action] model () |
424 | 568 | -- @ |
425 | 569 | -- |
426 | 570 | -- * The 'Control.Monad.Reader' portion of 'Effect' is 'ComponentInfo'. 'ask', 'asks', 'Miso.Lens.view' can be used to access its fields. |
|
587 | 731 | -- |
588 | 732 | -- import Miso.FFI.QQ ('js') |
589 | 733 | -- |
590 | | --- update :: Action -> 'Effect' parent model Action |
| 734 | +-- update :: Action -> 'Effect' parent props model Action |
591 | 735 | -- update = \\case |
592 | 736 | -- Log msg -> io_ [js| console.log(${msg}) |] |
593 | 737 | -- |
|
669 | 813 | -- A simple example of static prerendering would be an @index.html@ page with some HTML |
670 | 814 | -- |
671 | 815 | -- @ |
672 | | --- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>" > index.html |
| 816 | +-- echo "\<html\>\<head\>\<\/head\>\<body\>hello world\<\/body\>\<html\>" > index.html |
673 | 817 | -- @ |
674 | 818 | -- |
675 | 819 | -- And a miso application that looks like: |
|
0 commit comments