Strongly typed declarative markup for the DOM and CSS
npm install --save @ristostevcev/bs-declaredom
Make sure to set package-specs.module
in your project's bsconfig.json
to
es6-global
to compile to ES modules for the browser.
If you're using declaredom to generate static HTML or CSS on the backend, add an
additional package-spec for commonjs
in your bsconfig.json
. If you're generating
static HTML on the backend, make sure to also install and initialize jsdom
and set global.window
and global.document
, like so.
The examples in this README can be found in the example/ folder.
Declaredom is intentionally simple. It's intended to do only one thing and one thing well, which is to provide good declarative markup for HTML and CSS.
That being said, it's also intended to be combined with other tools so that you get all of the functionality that you would get from a framework.
There are several advantages to combining several small libraries that only do one thing instead of a large monolithic framework:
See the example MVC todo app for more details.
This library provides sound static typing guarantees for HTML and CSS. It ensures that you write correct HTML and CSS in your app with good conventions like CSS modules. This library is based off of and fully compliant with the HTML and CSS specs (see docs).
The HTML that's generated are actual DOM nodes that can be converted into bs-webapi-incubator's Dom.element
types using Html.Node.to_element
, or to a Dom.node
using Html.Node.to_node
if it's a text node or document fragment.
See the Html and Css module docs
Use either JSX or vanilla Ocaml depending on which style you prefer. Example JSX:
let foo: Html.Node.t([> Html.Node.div]) =
<div>
<span style=Css.inline(~color=`green, ())>
<text>"Hello, world!"</text>
</span>
<br/>
</div>
let foo: [> Html.Node.div] Html.Node.t =
((div
~children:[((span ~style:(Css.inline ~color:`green ())
~children:[((text ~children:["Hello, world!"] ())
[@JSX ])] ())[@JSX ]);
((br ~children:[] ())[@JSX ])] ())[@JSX ])
The <text>
markup refers to a text node.
Don't worry anymore about CSS silently failing! this library ensures that you only apply CSS styles and attributes that are valid for the element
let _ =
div ~id:"some_div"
(* This fails because div elements are block elements - the vertical-align
* property applies to inline elements
~style:(Css.inline ~vertical_align:`baseline ())
*)
~style:(Css.block ~color:`red ())
[||]
You can override the CSS display styles, but this library intentionally restricts this functionality to the generic flow (<div>
), phrasing (<span>
) and sectioning (<section>
) containers as other use cases are usually a CSS antipattern
let _ =
Div.flex ~style:(Css.flexbox ~justify_content:`center ()) [|
Span.inline_block ~style:(Css.inline_block ~color:`blue ()) [|text "foo"|];
Section.inline_flex [|text "bar"|]
|]
Only valid children are allowed for each element
let _ =
html [|
(* This fails because the <html> tag only takes a <head> or <body> element
* as children
span [|text "foo"|]
*)
head [||]
|]
Only valid attributes are allowed for each element. The anchor tag accepts a href
attribute, and the link
aria role is also allowed
let anchor =
a ~id:"link" ~href:"#"
(* Anchor elements can accept the `link` aria role *)
~aria:(Html.Attributes.Aria.link ~aria_hidden:() ~aria_label:"foo" ())
~on_click:(fun _ -> Js.log "clicked!")
[|text "some link"|]
Make functions that only take a specific kind of element or group of elements
let f (_: Html.Node.span Html.Node.t): unit = ()
let _ =
f (span [|text "hello"|])
Add custom types. This creates a custom type called foo
. This also works
very well with web components
let custom_foo: [> [> `foo] Html.Node.custom] Html.Node.t = Obj.magic @@
span [|text "foo"|]
You can even give your custom element the ability to take only specific children! OCaml's powerful polymorphic variants will correctly unify the elements you pass into the array.
This example constructs a custom foo
element that only takes other custom foo
elements, custom bar
elements, and span
and br
elements:
let make_custom_bar
(children: [[`bar | `foo] Html.Node.custom | Html.Node.span | Html.Node.br] Html.Node.t array):
[> [> `bar] Html.Node.custom] Html.Node.t =
(* TODO: stubbed, needs implementation *)
Obj.magic children
let _: [`bar] Html.Node.custom Html.Node.t =
make_custom_bar
[|
span [|text "Custom foo:"|];
br ();
custom_foo;
(* This fails because <p> tags aren't allowed
p [|text "foobar"|]
*)
|]
You can also typecheck based on your custom type
let f' (_: [`foo] Html.Node.custom Html.Node.t): unit = ()
let _ = f' custom_foo
And since the markup produces real HTML elements, it works very well with web components.
CSS modules deal with a lot of the pitfalls of CSS in a large scale app. Provide one CSS module per component and no longer worry about precedence rules, or enforcing conventions like BEM, or applying silly hacks and refactoring if the dev team painted themselves into a corner. Keep it simple.
let my_title = Css.Module.make @@
Css.block ~color:`coral ~font_size:(`em 24.) ()
Instead of mucking around with inheritance using CSS's inheritance model, you can build up abstractions using composition instead by merging rulesets, which is more explicit and easier to understand and predict.
You can then apply these to your elements, but make sure you serve the CSS module in a stylesheet (inline or served as a CSS file)
let _ =
h1 ~css_module:my_title [|text "This is my title"|]
These work much like postcss modules except that you get to use Ocaml's type system to ensure that you only reference a module that actually exists, and no preprocessing is required.
And you can still build these if you aren't using Ocaml on the backend by generating the CSS stylesheets as part of your build, and then you can even apply other transformations like autoprefixer if you need to.
This library is possible only because of Ocaml's powerful type system that is both strong, sound, and flexible. Speficially, it's Ocaml's module system and polymorphic variants that make this library possible.
This library generates es6-global
and commonjs
output so you can use the es6
output for tree-shaking using rollup or webpack and the commonjs output for a nodejs backend.
The resulting bundle won't include any exports that you aren't using, such as HTML nodes or CSS.
Use rollup + google closure compiler on your final bundle if you want to get the most out of tree shaking and dead code elimination.
See LICENSE