miyamoen/elm-origami/1.0.1

NOTE: the latest version is 2.0.0

Origami

Define CSS in Elm.

input
    [ type_ "checkbox"
    , name "toggle-all"
    , checked allCompleted
    , onClick (CheckAll (not allCompleted))
    , css
        [ property "position" "absolute"
        , property "top" "-55px"
        , property "left" "-12px"
        , property "width" "60px"
        , property "height" "34px"
        , property "text-align" "center"
        , property "border" "none" -- Mobile Safari
        , withPseudoElement "before"
            [ property "content" <| qt "❯"
            , property "font-size" "22px"
            , property "color" "#e6e6e6"
            , property "padding" "10px 27px 10px 27px"
            ]
        , with (pseudoElement [ pseudoClass "checked" ] "before")
            [ property "color" "#737373" ]
        , -- Hack to remove background from Mobile Safari.
          -- Can't use it globally since it destroys checkboxes in Firefox
          withMedia "screen and (-webkit-min-device-pixel-ratio:0)"
            [ property "background" "none"
            , property "-webkit-transform" "rotate(90deg)"
            , property "transform" "rotate(90deg)"
            , property "-webkit-appearance" "none"
            , property "appearance" "none"
            ]
        ]
    ]
    []

How to Use

  • ここからOrigamiの使い方を説明していきます
  • elm-cssとほぼ同じようなAPIを定義しています
    • 型付けされたproperty関数は定義していないことが大きな違いです

css and basic functions

label
    [ css
        [ property "white-space" "pre-line"
        , property "word-break" "break-all"
        , property "padding" "15px 60px 15px 15px"
        , property "margin-left" "45px"
        , property "display" "block"
        , property "line-height" "1.2"
        , property "transition" "color 0.4s"
        , if todo.completed then
            batch
                [ property "color" "#d9d9d9"
                , property "text-decoration" "line-through"
                ]


          else
            noStyle
        ]
    , onDoubleClick (EditingEntry todo.id True)
    ]
    [ text todo.description ]
  • Origami.Html.Attributes.cssを使ってDOMにStyleを適用します
  • Style型は大体CSS Propertyのようなものです
  • 基本的な関数が3つあります
    • propertyでpropertyを記述します
      • Origamiでは型安全なproperty関数は提供しません
    • batchでpropertyをまとめることができます
    • noStyleは条件分岐などで使えます
type alias Style =
Style
property : String -> String -> Style

Create a property style.

css [ property "-webkit-font-smoothing" "none" ]

...outputs

-webkit-font-smoothing: none;
batch : List Style -> Style

Create a batched style.

underlineOnHover =
    batch
        [ textDecoration none
        , hover
            [ textDecoration underline ]
        ]


css
    [ color (rgb 128 64 32)
    , underlineOnHover
    ]

...has the same result as:

css
    [ color (rgb 128 64 32)
    , textDecoration none
    , hover
        [ textDecoration underline ]
    ]
noStyle : Style

Create an empty style.

if predicate then
    property "key" "value"


else
    noStyle

Create a Style Tag

  • Origami.Html.HtmlはHtml.Htmlと同じように扱えます
  • Origami.Html.toHtmlなどを使ってHtml.Htmlに変換してください
    • toHtmlなどでcssで適用されているstyleが収集されてstyle tagを生成します
  • 上記labelに適用されたstyleは以下のようにstyle tag内に書き出されます
._1e7eb6f2 {
    white-space: pre-line;
    word-break: break-all;
    padding: 15px 60px 15px 15px;
    margin-left: 45px;
    display: block;
    line-height: 1.2;
    transition: color 0.4s;
}

label側は以下のように記述されたのと同様になります

label
    [ class "_1e7eb6f2"
    , onDoubleClick (EditingEntry todo.id True)
    ]
    [ text todo.description ]
  • _1e7eb6f2というクラス名が付与されています
    • この値は適用されているstyleから計算されたhash値です

Nested Style

  • CSS preprocessorのようにセレクターを入れ子にしてstyleを定義できます
  • 入れ子にする関数は簡易的なAPIとカスタム可能なAPIに分けられています
    • 簡易的なAPIはelm-cssを参考にしています
button
    [ css
        [ property "display" "none"
        , property "position" "absolute"
        , property "top" "0"
        , property "right" "10px"
        , property "bottom" "0"
        , property "width" "40px"
        , property "height" "40px"
        , property "margin" "auto 0"
        , property "font-size" "30px"
        , property "color" "#cc9a9a"
        , property "margin-bottom" "11px"
        , property "transition" "color 0.2s ease-out"
        , withPseudoClass "hover" [ property "color" "#af5b5e" ]
        , withPseudoElement "after" [ property "content" "'×'" ]
        ]
    , onClick (Delete todo.id)
    ]
    []

これは以下のように書き出されます

._b75a75af {
    display: none;
    position: absolute;
    top: 0;
    right: 10px;
    bottom: 0;
    width: 40px;
    height: 40px;
    margin: auto 0;
    font-size: 30px;
    color: #cc9a9a;
    margin-bottom: 11px;
    transition: color 0.2s ease-out;
}

_b75a75af:hover {
    color: #af5b5e;
}

._b75a75af::after {
    content: '×';
}
  • hash値に続く形でセレクターが付加されます
  • 更にネストすることもできます
withClass : String -> List Style -> Style

Nest with a class selector.

css [ withClass "classname" [ property "key" "value" ] ]

...outputs

_xxx.classname {
    key: value;
}
withAttribute : String -> List Style -> Style

Nest with a attribute selector.

css [ withAttribute """href="https://example.org"""" [ property "key" "value" ] ]

...outputs

_xxx[href="https://example.org"] {
    key: value;
}
withPseudoClass : String -> List Style -> Style

Nest with a pseudo class.

css [ withPseudoClass "hover" [ property "key" "value" ] ]

...outputs

_xxx:hover {
    key: value;
}
withPseudoElement : String -> List Style -> Style

Nest with a pseudo element.

css [ withPseudoElement "after" [ property "content" <| qt "×" ] ]

...outputs

_xxx::after {
    content: "×";
}

note: withPseudoElementを使ったらそれ以上ネストできません

css
    [ withPseudoElement "after"
        [ property "content" <| qt "×"
        , withClass "notAllowed"
            [ property "color" "white" ]
        ]
    ]

...outputs

_xxx::after {
    content: "×";
}
  • 型で制限できていませんがこのように無効な部分は書き出されません

CSS Combinators

結合子も使用することができます

footer
    [ css
        [ property "margin" "65px auto 0"
        , property "color" "#bfbfbf"
        , property "font-size" "10px"
        , property "text-shadow" "0 1px 0 rgba(255, 255, 255, 0.5)"
        , property "text-align" "center"
        , withDescendants [ tag "p" ] [ property "line-height" "1" ]
        , withDescendants [ tag "a" ]
            [ property "color" "inherit"
            , property "text-decoration" "none"
            , property "font-weight" "400"
            , withPseudoClass "hover" [ property "text-decoration" "underline" ]
            ]
        ]
    ] [...]

...outputs

._89167f0f {
    margin: 65px auto 0;
    color: #bfbfbf;
    font-size: 10px;
    text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5);
    text-align: center;
}

._89167f0f p {
    line-height: 1;
}

._89167f0f a {
    color: inherit;
    text-decoration: none;
    font-weight: 400;
}
  • Origami supports descendant, child, general sibling and adjacent sibling combinators
  • tag関数を使って対象のタグを複数指定することができます
withDescendants : List Tag -> List Style -> Style

Nest with descendant combinators.

css [ withDescendants [ tag "p", tag "li" ] [ property "key" "value" ] ]

...outputs

_xxx p, _xxx li {
    key: value;
}
withChildren : List Tag -> List Style -> Style

Nest with child combinators.

css [ withChildren [ tag "p", tag "li" ] [ property "key" "value" ] ]

...outputs

_xxx > p, _xxx > li {
    key: value;
}
withGeneralSiblings : List Tag -> List Style -> Style

Nest with general sibling combinators.

css [ withGeneralSiblings [ tag "p", tag "li" ] [ property "key" "value" ] ]

...outputs

_xxx ~ p, _xxx ~ li {
    key: value;
}
withAdjacentSiblings : List Tag -> List Style -> Style

Nest with adjacent sibling combinators.

css [ withAdjacentSiblings [ tag "p", tag "li" ] [ property "key" "value" ] ]

...outputs

_xxx + p, _xxx + li {
    key: value;
}
type alias Tag =
Tag
tag : String -> Tag

Represent an element selector.

tag "p"

...outputs

p
everyTag : Tag

Represent a universal selector.

everyTag

...outputs

*

note: もし.parent > .childのようなselectorを使用したいと思っているなら.parent > *.childのように暗黙に全称セレクターが指定されていると思ってeveryTagを使ってください

Media Query

ul
    [ css
        [ property "margin" "0"
        , property "padding" "0"
        , property "list-style" "none"
        , property "position" "absolute"
        , property "right" "0"
        , property "left" "0"
        , withMedia "(max-width: 430px)" [ property "bottom" "10px" ]
        ]
    ] [..]

...outputs

._a85c725b {
    margin: 0;
    padding: 0;
    list-style: none;
    position: absolute;
    right: 0;
    left: 0;
}

@media (max-width: 430px) {
    ._a85c725b {
        bottom: 10px;
    }
}
  • セレクターと同じようにMedia Queryを使用することができます
withMedia : String -> List Style -> Style

Nest with a media query.

css [ withMedia "screen" [ property "key" "value" ] ]

...outputs

@media screen {
    _xxx {
        key: value;
    }
}

note: Media Queryを複数回入れ子にすることはできません

css
    [ withMedia "screen"
        [ property "key" "value"
        , withMedia "not nestable" [ property "key" "value" ]
        , withClass "nestable" [ property "key" "value" ]
        ]
    ]

..outputs

@media screen {
    _xxx {
        key: value;
    }
}

@media screen {
    _xxx.nestable {
        key: value;
    }
}
  • 無効になったstyleは書き出されません
  • Media Queryの中でネストすると@mediaが複数書き出されます

Custom Nested Style

  • ここまでのwithXxx関数で大体のことには事足りるかと思います
  • もしもっと柔軟にstyleを入れ子にしたくなったのならOrigamiにおけるSelectorを学びましょう
             ┌ child              ┌ descendant                       ┌ adjacent sibling
     class   │   pseudo class     │          attribute               │  pseudo element
     ┌────┐ ┌─┐┌──────────────┐┌──────┐┌───────────────────┐        ┌─┐┌─────┐
._xxx.class > *:nth-child(2n+1) section[aria-hidden="false"], ._xxx + *::after {...}
└──────────────────────────────────────────────────────────┘  └──────────────┘
                          selector                                selector
  • See an above diagram.
    • class selector, pseudo class, attribute selectorが並んでいます
      • また各種結合子とtype selectorかuniversal selectorがまとめられています
      • これらをRepeatableSelectorとしてまとめています
    • Selector型を生成するselector関数はRepeatableSelectorのListを受け取るようになっています
      • またpseudo elementを付加する場合はpseudoElementを使ってください
    • 生成したSelectorはwith, withEach, withCustomでcss関数に繋げられます
      • 図のようにSelectorを列挙したければwithEachかwithCustomを使ってください
      • withCustomにはMedia Queryも付加することができます
with : Selector -> List Style -> Style

Nest with a Selector.

css
    [ with (selector [ class "classname" ]) [ property "key" "value" ] ]

...outputs

_xxx.classname {
    key: value;
}
withEach : List Selector -> List Style -> Style

Nest with Selector list.

css
    [ withEach
        [ selector [ class "classname" ]
        , selector [ attribute "title" ]
        ]
        [ property "key" "value" ]
    ]

...outputs

_xxx.classname, _xxx[title] {
    key: value;
}

note: もし空リストを与えたらそのstyleは霧視されます

css
    [ withEach [] [ property "key" "empty-value" ]
    , property "key" "value"
    ]

...outputs

_xxx {
    key: value;
}
withCustom : String -> List Selector -> List Style -> Style

Nest with Selector list and a media query.

css
    [ withCustom "(max-width: 430px)"
        [ selector [ class "classname" ]
        , selector [ attribute "title" ]
        ]
        [ property "key" "value" ]
    ]

...outputs

@media (max-width: 430px) {
    _xxx.classname, _xxx[title] {
        key: value;
    }
}
type alias Selector =
Single
selector : List RepeatableSelector -> Selector
pseudoElement : List RepeatableSelector -> String -> Selector
type alias RepeatableSelector =
Repeatable
type RepeatableSelector
    = ClassSelector String
    | PseudoClassSelector String
    | AttributeSelector String
    | DescendantCombinator Tag
    | ChildCombinator Tag
    | GeneralSiblingCombinator Tag
    | AdjacentSiblingCombinator Tag
class : String -> RepeatableSelector

Represent a class selector.

class "classname"

...outputs

.classname
pseudoClass : String -> RepeatableSelector

Represent a pseudo class.

pseudoClass "hover"

...outputs

:hover
attribute : String -> RepeatableSelector

Represent an attribute selector.

attribute "aria-hidden=\"false\""

...outputs

[aria-hidden="false"]
descendant : Tag -> RepeatableSelector

Represent a descendant combinator with a type selector or universal selector.

descendant (tag "p")

...outputs

 p
  • descendant combinatorは空白で表せられるので見た目的に分かりにくい
child : Tag -> RepeatableSelector

Represent a child combinator with a type selector or universal selector.

child (tag "li")

...outputs

 > li
generalSibling : Tag -> RepeatableSelector

Represent a general sibling combinator with a type selector or universal selector.

generalSibling everyTag

...outputs

 ~ *
adjacentSibling : Tag -> RepeatableSelector

Represent a adjacent sibling combinator with a type selector or universal selector.

adjacentSibling (tag "section")

...outputs

 + section

Animation Style

batch
    [ property "border-radius" "50px"
    , property "width" "50px"
    , -- [Copyright (c) 2019 Epicmax LLC](https://epic-spinners.epicmax.co/)
      withEach [ pseudoElement [] "after", pseudoElement [] "before" ]
        [ property "content" <| qt ""
        , property "position" "absolute"
        , property "width" "60%"
        , property "height" "60%"
        , property "border-radius" "100%"
        , property "border" "calc(30px / 10) solid transparent"
        , animation
            [ ( ( from, [] ), [ propertyA "transform" "rotate(0deg)" ] )
            , ( ( to, [] ), [ propertyA "transform" "rotate(360deg)" ] )
            ]
        , property "animation-duration" "1s"
        , property "animation-iteration-count" "infinite"
        ]
    , withPseudoElement "after" [ property "border-top-color" "#ffe9ef" ]
    , withPseudoElement "before"
        [ property "border-bottom-color" "#ffe9ef"
        , property "animation-direction" "alternate"
        ]
    ]
  • animation関数を使ってDOMに直接アニメーションを定義することができます
  • propertyA関数を使ってpropertyを記述します
    • 型の都合でproperty, batch, noStyleは使えません
  • animationプロパティのみ暗黙に生成されるのでその他のアニメーションプロパティは別に定義します
List
( ( KeyframesSelector, List KeyframesSelector )
, List Property
)
-> Style

Define a animation.

css
    [ animation
        [ ( ( from, [] ), [ propertyA "key" "value" ] )
        , ( ( pct 20, [ pct 80 ] ), [ propertyA "key" "value" ] )
        , ( ( to, [] ), [ propertyA "key" "value" ] )
        ]
    ]

...outputs

.hashed-class-name {
    animation-name: hashed-animation-name;
}

@keyframes hashed-animation-name {
    from {
        key: value;
    }

    20%, 80% {
        key: value;
    }

    to {
        key: value;
    }
}
  • animation-name propertyが暗黙に生成されます
  • keyframesブロックが生成されます
type alias Property =
Property
propertyA : String -> String -> Property
  • animation用のproperty
  • propertyとは型が違う
type alias KeyframesSelector =
KeyframesSelector

Selectors for animation.

from, to and pct(%).

from : KeyframesSelector

from token.

to : KeyframesSelector

to token.

pct : Float -> KeyframesSelector

30%, 70% and so on.

Helper

qt : String -> String

For use with font-family and so on.

property "font-family" (qt "Gill Sans Extrabold")

Advanced topics

CSSの重複除去は行われる?

div []
    [ div [ css [ property "key" "value" ] ] []
    , div [ css [ property "key" "value" ] ] []
    , div [] [ div [ css [ property "key" "value" ] ] [] ]
    ]

...outputs only

._xxx {
    key: value;
}
  • 重複除去はcssに適用したstyleの単位で行われます

css関数を複数使ったらどうなる?

div
    [ css [ property "display" "none" ]
    , css [ property "display" "block" ]
    ]
    [ text "表示されますか?" ]
  • このdivは表示されるでしょうか?
    • 答えは「わかりません」
    • styleはcss関数でまとめられた単位で扱われ、Dictを経由して書き出されます
    • そのためccsを使う順番に依存した実装は行わないほうがいいでしょう
  • もし既に適用されているcssの値を上書きしたいのならばwithClassなどを使ってセレクターの詳細度を上げるとよいでしょう

クラス名がハッシュ値で読みにくい

cssWithClass : String -> List Style -> Attribute msg
cssWithClass classname styles =
    batchAttributes [ Attributes.class classname, css [ withClass classname styles ] ]
  • クラス名を付加してみてください
  • 上記のようなヘルパーを定義するとよいでしょう