purescripthalogen

How to render purescript-halogen components into the <head> tag


How can I render a specific purescript-halogen component into the <head> tag?

The following example written for Halogen 1.0.0 renders the stylesheet and the paragraph into the HTML body:

module Main where

import Prelude
import Control.Monad.Eff      (Eff)
import Data.Maybe             (Maybe(Nothing))

import CSS                    as C
import Halogen                as H
import Halogen.Aff            as HA
import Halogen.HTML           as HH
import Halogen.HTML.CSS       as HS
import Halogen.Query.HalogenM as HQ
import Halogen.VDom.Driver    as HV

styles :: forall p i. HH.HTML p i
styles = HS.stylesheet $
    C.select C.body $ C.margin C.nil C.nil C.nil C.nil

content :: forall p i. HH.HTML p i
content = HH.p_ [ HH.text "Test" ]

main :: Eff (HA.HalogenEffects ()) Unit
main = HA.runHalogenAff $ HA.awaitBody >>= HV.runUI ui unit
  where
    ui = H.component { initialState : const unit
                     , render       : const render
                     , eval         : const $ HQ.halt "no query"
                     , receiver     : const Nothing
                     }

    render = HH.div_ [ styles, content ]

The DOM is generated as follows:

<html>
    <head>
        <title>Test</title>
        <script async="" type="text/javascript" src="main.js"></script>
    </head>
    <body>
        <div>
            <style type="text/css">
                body { margin: 0 0 0 0 }
            </style>
            <p>
                Test
            </p>
        </div>
    </body>
</html>

This example works but according to the specification the style element is only allowed "where metadata content is expected", i.e. the <head> element. So I want to render the stylesheet there. How do I accomplish this?


Solution

  • Notice the awaitBody function - as well as waiting for it to load, this selects the body element, which is not really what you want in this case. If you want to write to the head then you're going to need to select that and pass it to runUI for a component that renders a stylesheet.

    You'll also need to run two separate components, one for the head, one for the body:

    module Main where
    
    import Prelude
    
    import Control.Monad.Aff.Console (CONSOLE)
    import Control.Monad.Eff (Eff)
    
    import Data.Const (Const)
    import Data.Foldable (traverse_)
    import Data.Maybe (Maybe(..))
    import Data.Newtype (unwrap)
    
    import Halogen as H
    import Halogen.Aff as HA
    import Halogen.HTML as HH
    import Halogen.VDom.Driver (runUI)
    
    bodyComponent :: forall m. H.Component HH.HTML (Const Void) Unit Void m
    bodyComponent =
      H.component
        { initialState: const unit
        , render: const $ HH.div_ [ HH.text "A component" ]
        , eval: absurd <<< unwrap
        , receiver: const Nothing
        }
    
    styleComponent :: forall m. H.Component HH.HTML (Const Void) Unit Void m
    styleComponent =
      H.component
        { initialState: const unit
        , render: const $ HH.style_ [ HH.text "body { background: #222; color: #eee }"]
        , eval: absurd <<< unwrap
        , receiver: const Nothing
        }
    
    main :: Eff (HA.HalogenEffects (console :: CONSOLE)) Unit
    main = HA.runHalogenAff do
      HA.awaitLoad
      traverse_ (runUI styleComponent unit) =<< HA.selectElement "head"
      traverse_ (runUI bodyComponent unit) =<< HA.selectElement "body"
    

    If they need to communicate then you could use the subscribe and driver functions from the HalogenIO record that runUI produces to set up channels between them.

    I had concerns about doing this, as you're supposed to pass through an element that is empty as the target for where a container will render... but when using the Halogen-provided VDom driver it appears to behave at least, in that the <head> contents won't be replaced by the style component (it is appended to the end). This is basically unspecified behaviour though, so I'm not sure it would necessarily hold true with other drivers.