Siegfried Weber
Siegfried Weber

Reputation: 68

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?

Upvotes: 4

Views: 1031

Answers (1)

gb.
gb.

Reputation: 4649

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.

Upvotes: 4

Related Questions