[ANN] miso-css: verified CSS class application via dependent types

Hi,

miso-css is an evolutionary step from css-class-bindings.

CSS class of an atomic selector can be applied to any DOM element, but that is not true for classes used in composite selectors. Rules with partially matched selectors are silently ignored by browser and this opens door bugs during consequent changes. css-class-binding just cannot cope with such problem and miso-css uses dependent types to track what CSS classes can applied to HTML elements.

Composing tags

Before jumping straight to style application lets get familiar with syntax for tag composition because it is different in vanilla miso.

Appending a child

div_ </ p_
<div>
  <p></p>
</div>

Adding a sibling

ul_ </ li_ </ li_
<ul>
  <li></li>
  <li></li>
</ul>

Appending a child to child

body_ </ (section_ </ p_)
<body>
  <section>
    <p></p>
  </section>
</body>

Adding CDATA

a_ <@ "click"
<a>click</a>

Adding a raw miso DOM chunk

import Miso.Html qualified as MH
import Miso.Html.Property qualified as MH

go = div_ =< MH.p_ [] [ "h" ]
<div>
  <p>h</p>
</div>

Adding tag attribute

a_ =<| atr @"href" "http://link.com"
<a href="http://link.com"></a>

Binding event handler

button_ =! onClick YourActionDc

Applying CSS class

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|.red { color: red; }|]

div_ =. red
<div class="red"></div>

Adding tag ID

Handmade tag id:

div_ =# ElementId "footer"

Generated tag id:

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|#footer { color: red; }|]

div_ =# Footer
<div id="footer"></div>

Mix all at once

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
[css|.form .red { color: red; }|]

div_ =. form =# ElementId "footer"
  </ (a_ =. red =<| atr @"href" "/click.php?x=1"
      </ (span_ <@ "Click me"))
<div class="form" id="footer>
  <a class="red" href="/click.php?x=1">
    <span>Click me</span>
  </a>
</div>

Breaking rules

Until now all above samples must be valid and should type
check. This section enumerates HTML snippets with ill-applied
classes, expected errors and comments.

There can be only one

An element ID can be used once in a HTML document.

div_ =# ElementId "Duncan MacLeod"
  </ div_ =# ElementId "Duncan MacLeod"
Couldn't match type: '[DuplicatedId "Duncan MacLeod"]
               with: '[]

Parent class is missing

[css|.a .b {}|]

div_ =. b

The error message is a list of triples where first element is a list of not
applied classes, ids (hashes), tag names or attribute names.

[([C "a"], [], [])]

Class a and b are missing:

[css|.a .b .c {}|]

div_ =. c
[ ([C "b"], [], [])
, ([C "a"], [], [])
]

B element

When selector with a child relation is partially applied the triple
contains B element. It is a synthetic element preventing the failed
rule from matching later somewhere upper in DOM by an accident.

[css|.a > .b {}|]

div_ </ div_ =. b
[([B, C "a"], [], [])]

One of classes is missing

Second element of triple is a list of applied classes. It helps to
understand what worked out and what didn’t in a composite selector.

[css|.a.b > .c {}|]

div_ =. a </ div_ =. c
[([B, C "b"], [C "a"], [])]
Sibling is missing

The third element of triple explains sibling errors.

[css|.a + .b {}|]

div_ </ div_ =. b

Class a is not applied:

[([B], [], [[ [B], [C "a"]]])]

Hello World

{-# LANGUAGE QuasiQuotes #-}
{-# OPTIONS_GHC -Wno-missing-signatures #-}
module Miso.Css.Test.HelloWorld where

import Miso ( component, App, CSS(Style), Component(styles), View )
import Miso.Css
import Prelude

type Model = ()
type Action = ()

-- default name is "cssAsLiteralText"
renameCssTextConst "cssFromQq"

[css|
.c .b .a {
  color: #fc2c2c;
}
|]

-- instead of quasi-quoted CSS
-- the whole CSS file can be included with:
--   includeCss "assets/style.css"

app :: App Model Action
app = (component () pure viewModel)
  { styles = [ Style cssFromQq ] }

{-
viewModel produce following HTML snippet:

    <div class="c">
      <div class="b">
        <button class="a">
          Submit
        </button>
      </div>
    </div>

html_ and body_ don't produce tags,
because miso mount cannot be higher than body tag.

they serve just for type checking purpose
(e.g. html_ satisfies :root pseudo class)

-}
viewModel :: Model -> View Model Action
viewModel () = toView . html_ . body_ $
  div_ =. c
  </ (div_ =. b
       </ (button_ =. a
            <@ "Submit"))
9 Likes