[ANN] css-class-bindings library generates Haskell bindings for CSS classes

Recently I migrated a frontend to Miso library (WASM backend), and I noticed that DOM functions (e.g. div_) accept CSS class names as plain strings.

Quasi-quote input

{-# LANGUAGE QuasiQuotes #-}
module Css where
import CssClassBindings ( css )

[css|
.foo-bar {
  color: #fc2c2c;
}
|]
module Main where

import Css (fooBar, cssAsLiteralText)
import CssClassBindings qualified as C
import Miso
import Miso.Html.Element (div_, button_)
import Miso.Html.Property qualified as P

class_ :: C.CssClass MisoString -> Attribute action
class_ = P.class_ . C.class_

app :: App Model Action
app = (component emptyModel updateModel viewModel)
  { styles = [ Style cssAsLiteralText ]
  }

viewModel :: Model -> View Model Action
viewModel m = div_ [] [ button_ [ class_ fooBar ] [ "Submit" ] ]

File input

import CssClassBindings ( includeCss )
includeCss "assets/style.css"
module Main where
import Css (fooBar, style)
-- ...
5 Likes

nice! Shpadoinkle has something similar. It was kind of unreasonably effective in practice lol.

Could you share a link? I did researched on hackage before starting the library, but haven’t found anything suitable.

1 Like

I can’t find the module we use in the codebase (we used a very early version since the author was my cofounder) but I think it evolved into shpadoinkle-html.

Yours being its own standalone thing is great though. Bookmarked!

2 Likes

This is cool. I’ve been using Miso with static stylesheets, but been uneasy about the “stringly-typed” nature of linking them together the naive way.

What stops me from using this straight away is that I’d want support for IDs as well as classes. I see no reason not to also generate bindings for those, except that it would ideally mean renaming the library (css-bindgen?), which would be a bit annoying at this stage.

1 Like

Also, includeCss should really be using addDependentFile so that bindings are regenerated when the file changes.

Thanks for your feedback. This motivated me to release v0.0.3 with CSS id support and marking included files with addDependentFile.
Id support didn’t get into initial version because my project don’t have any and e.g. whole bulma library (+600kb) dito.

I think of CSS id as a stricter version of CSS class (linear CSS class).

2 Likes

I’m curious why you chose to generate data constructors for IDs, instead of variables of a fixed type like you do for classes?

Also, what’s your approach to parsing the CSS? An ID appearing in a position like *:not(#a) {} isn’t picked up, even though a class in the same place is.

Anyway, I don’t think I can really use this library in practice for the foreseeable future, since the Wasm backend’s GHCI browser mode’s Template Haskell support isn’t able to read files.

Using variables was the first idea to pop in my head, but I decided to put it aside because ids and classes exist in separate namespaces and merging them into one would require providing disambiguation logic such as a suffixes to avoid compilation errors.

It is folding a list of CSS tokens from css-syntax package with pattern matching. Class syntax is easy to catch and using something like attoparsec here is definitely overkill.

What version of GHC are you talking about?

TH in 9.12.2 with WASM Backend works fine.
includeCss is used in vpn-router. The project build is sealed with Nix. You should not have an issue with reproducing the results.

I fixed the bug you reported in v0.0.4.

I am curious how many ids do you expect on in an app you maintain?

As for me using id in CSS selectors seems as an anti-pattern, because:

  • ids are global and make composition of HTML components harder (sort of global variable in a classic programming language)
  • modern web development “single page app” do not need ids (the whole page is a view conditionally composed from other views). An exception is a migration of a legacy jquery-like project where injection places of “react” components are specified with ids, and this is not about styles. Modern CSS support layers feature expanding priority spectrum.

That’s a fair point. I’m not sure what the best solution would be. I think I’d probably want to be able to generate two separate Haskell modules, so that I could qualify the names in the (relatively unlikely) event I’d need to disambiguate. But this isn’t great while GHC doesn’t allow defining multiple modules in one file. It would require two separate TH invocations. I’ve actually been having the same annoyance recently while using hs-bindgen and wanting separate modules for safe and unsafe bindings.

Is this an upstream issue with css-syntax then?

Compiled Wasm builds work fine, but are you using -fghci-browser, for REPL-based live-reloading?

I tend to automatically use IDs instead of classes for things that I know will only appear at one position in the DOM. In theory at least, this makes CSS selectors more efficient. It’s also a slight aid to reading the CSS as a human. This is the first time I’ve found any downside to it. I don’t quite see the analogy with global variables.

No

No, I didn’t have to. Imho WASM backend is faster than native one. Rebuild takes about a second.

So I imagine a TH function generating UUID id for an HTML component, extracting CSS classes from the component, discovering CSS selectors mentioning these classes and rewriting them with the UUID prefix, stripping a bigger chunk of unrelated CSS.
That’s very cool, but there is no need for type enforcement, because it’s automatic algorithm. The algorithm would work fine with plain strings for ids and classes. The algorithm requires testing and ideally verification.

really neat project! i’ve often wondered about a Haskell DSL that compiles to css/html … it’d be sick to see something like that come to life

1 Like