10000 Support CSS and keyframes interpolations · shadaj/slinky-styled-components@2facdea · GitHub
[go: up one dir, main page]

Skip to content

Commit

Permalink
Support CSS and keyframes interpolations
Browse files Browse the repository at this point in the history
  • Loading branch information
shadaj committed Jul 11, 2018
1 parent b8d4821 commit 2facdea
Show file tree
Hide file tree
Showing 6 changed files with 201 additions and 25 deletions.
3 changes: 3 additions & 0 deletions src/main/scala/slinky/styledcomponents/InterpolatedCSS.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package slinky.styledcomponents

case class InterpolatedCSS[P](parts: Seq[String], interpolations: Seq[InterpolationPart[P]])
26 changes: 26 additions & 0 deletions src/main/scala/slinky/styledcomponents/InterpolationPart.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package slinky.styledcomponents

import scala.scalajs.js
import js.JSConverters._

import scala.language.implicitConversions

trait InterpolationPart[+P] extends js.Object
trait KeyframesInterpolationPart[P] extends InterpolationPart[P]

object InterpolationPart {
implicit def fromPropsFunction[P, O](fn: P => O)(implicit ev: O => InterpolationPart[P]): InterpolationPart[P] = {
(((o: js.Any) => {
fn(o.asInstanceOf[js.Dynamic].__.asInstanceOf[P])
}): js.Function1[js.Any, js.Any]).asInstanceOf[InterpolationPart[P]]
}

implicit def fromString[P](str: String): InterpolationPart[P] = str.asInstanceOf[InterpolationPart[P]]

implicit def fromCSS[P](css: InterpolatedCSS[P]): InterpolationPart[P] = {
StyledComponentsNamespace.css.call(
null,
css.parts.toJSArray +: css.interpolations: _*
).asInstanceOf[InterpolationPart[P]]
}
}
32 changes: 32 additions & 0 deletions src/main/scala/slinky/styledcomponents/StyledBuilder.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package slinky.styledcomponents

import slinky.core.{AttrPair, ExternalComponentWithAttributes, ExternalPropsWriterProvider, TagElement}
import slinky.readwrite.Writer

import scala.scalajs.js
import scala.scalajs.js.|
import js.JSConverters._

final class StyledBuilder[P, TagType <: TagElement](private val innerObj: js.Object) extends AnyVal {
def attrs(pairs: AttrPair[TagType]*): StyledBuilder[P, TagType] = {
val dictionary = js.Dictionary[js.Any]()
pairs.foreach(p => dictionary(p.name) = p.value)
new StyledBuilder(innerObj.asInstanceOf[js.Dynamic].attrs(dictionary).asInstanceOf[js.Object])
}

def apply(c: InterpolatedCSS[P]): ExternalComponentWithAttributes[TagType] {
type Props = P
} = {
new ExternalComponentWithAttributes[TagType]()(new Writer[P] {
override def write(p: P): js.Object = js.Dynamic.literal(__ = p.asInstanceOf[js.Object])
}.asInstanceOf[ExternalPropsWriterProvider]) {
override type Props = P
override val component: String | js.Object = {
innerObj.asInstanceOf[js.Function].call(
js.undefined,
c.parts.toJSArray +: c.interpolations: _*
).asInstanceOf[js.Object]
}
}
}
}
10 changes: 9 additions & 1 deletion src/main/scala/slinky/styledcomponents/StyledComponents.scala
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package slinky.styledcomponents

import slinky.core.{Tag, TagElement}
import slinky.core.TagElement

import scala.scalajs.js
import scala.scalajs.js.annotation.JSImport
Expand All @@ -15,3 +15,11 @@ trait StyledComponentConstructor[P] extends js.Object {
object StyledComponents extends js.Object {
val button: js.Function = js.native
}

@JSImport("styled-components", JSImport.Namespace)
@js.native
object StyledComponentsNamespace extends js.Object {
val css: js.Function = js.native
val keyframes: js.Function = js.native
val injectGlobal: js.Function = js.native
}
46 changes: 27 additions & 19 deletions src/main/scala/slinky/styledcomponents/package.scala
Original file line number Diff line number Diff line change
@@ -1,33 +1,41 @@
package slinky

import slinky.core.{ExternalComponentWithAttributes, ExternalPropsWriterProvider, Tag, TagElement}
import slinky.readwrite.Writer
import org.scalajs.dom.raw.HTMLElement
import slinky.core.{Attr, AttrPair, CustomAttribute}
import slinky.web.html

import scala.scalajs.js
import js.JSConverters._
import scala.scalajs.js.|

package object styledcomponents {
private def elemHandler[P, T <: TagElement](sc: StringContext, comp: js.Function, fns: Seq[P => js.Any]): StyledComponentConstructor[P] { type TagType = T } = {
val jsFns = fns.map(v => ((o: js.Any) => { v(o.asInstanceOf[js.Dynamic].__.asInstanceOf[P]) }): js.Function1[js.Any, js.Any])
StyledComponents.button.call(
js.undefined,
sc.parts.toJSArray +: jsFns: _*
).asInstanceOf[StyledComponentConstructor[P] { type TagType = T }]
}

implicit class StyledInterpolator(val sc: StringContext) extends AnyVal {
def button[P](fns: (P => js.Any)*) =
elemHandler[P, html.button.tagType](sc, StyledComponents.button, fns)
def css[P](fns: InterpolationPart[P]*): InterpolatedCSS[P] = {
InterpolatedCSS(sc.parts, fns)
}

def injectGlobal[P](fns: InterpolationPart[P]*): Unit = {
StyledComponentsNamespace.injectGlobal.call(
js.undefined,
sc.parts.toJSArray +: fns: _*
)
}

def keyframes[P](fns: InterpolationPart[P]*): KeyframesInterpolationPart[P] = {
StyledComponentsNamespace.keyframes.call(
js.undefined,
sc.parts.toJSArray +: fns: _*
).asInstanceOf[KeyframesInterpolationPart[P]]
}
}

def styled[P](c: StyledComponentConstructor[P]): ExternalComponentWithAttributes[c.TagType] { type Props = P } = {
new ExternalComponentWithAttributes[c.TagType]()(new Writer[P] {
override def write(p: P): js.Object = js.Dynamic.literal(__ = p.asInstanceOf[js.Object])
}.asInstanceOf[ExternalPropsWriterProvider]) {
override type Props = P
override val component: String | js.Object = c
object styled {
def button[P]: StyledBuilder[P, html.button.tagType] = {
new StyledBuilder[P, html.button.tagType](StyledComponents.button.asInstanceOf[js.Object])
}
}

object innerRef extends Attr {
@inline def :=(v: org.scalajs.dom.Element => Unit) = new AttrPair[Any]("innerRef", v)
@inline def :=(v: slinky.core.facade.ReactRef[org.scalajs.dom.Element]) = new AttrPair[Any]("innerRef", v)
}
}
109 changes: 104 additions & 5 deletions src/test/scala/slinky/styledcomponents/StyledComponentTest.scala
Original file line number Diff line number Diff line change
@@ -1,20 +1,47 @@
package slinky.styledcomponents

import org.scalajs.dom
import org.scalajs.dom.raw.HTMLButtonElement
import org.scalajs.dom.Element
import org.scalajs.dom.raw.{HTMLButtonElement, HTMLElement}
import org.scalatest.FunSuite
import slinky.web.ReactDOM

import slinky.web.html.id
import slinky.web.html.`type`

class StyledComponentTest extends FunSuite {
test("Can construct a styled button") {
case class Props(color: String)
val targetElem = dom.document.createElement("div")
val comp = styled[Props](
button"""
val comp = styled.button[Props](
css"""
border-radius: 3px;
color: ${p: Props => p.color}
"""
)

ReactDOM.render(
comp(Props(color = "pink"))(
id := "testComponent"
),
targetElem
)

val buttonElement = targetElem.firstElementChild.asInstanceOf[HTMLButtonElement]

assert(buttonElement.tagName.toLowerCase == "button")
assert(buttonElement.id == "testComponent")
assert(buttonElement.className.nonEmpty)
}

test("Can construct a styled button with attrs") {
case class Props(color: String)
val targetElem = dom.document.createElement("div")
val comp = styled.button[Props].attrs(
`type` := "reset"
)(
css"""
border-radius: 3px;
color: ${_.color}
color: ${p: Props => p.color}
"""
)

Expand All @@ -28,7 +55,79 @@ class StyledComponentTest extends FunSuite {
val buttonElement = targetElem.firstElementChild.asInstanceOf[HTMLButtonElement]

assert(buttonElement.tagName.toLowerCase == "button")
assert(buttonElement.`type` == "reset")
assert(buttonElement.id == "testComponent")
assert(buttonElement.className.nonEmpty)
}

test("Can construct a styled button with interpolated CSS") {
case class Props(color: String)
val targetElem = dom.document.createElement("div")
val comp = styled.button[Props](
css"""
border-radius: 3px;
${
css"color: ${p: Props => p.color}"
}
"""
)

ReactDOM.render(
comp(Props(color = "pink")),
targetElem
)

val buttonElement = targetElem.firstElementChild.asInstanceOf[HTMLButtonElement]

assert(buttonElement.tagName.toLowerCase == "button")
assert(buttonElement.className.nonEmpty)
}

test("Can construct a styled button with keyframes") {
val targetElem = dom.document.createElement("div")
val fadeIn = keyframes"""
0% {
opacity: 0;
}
100% {
opacity: 1;
}
"""

val comp = styled.button[Unit](
css"""
animation: 1s $fadeIn ease-out;
"""
)

ReactDOM.render(
comp(),
targetElem
)

val buttonElement = targetElem.firstElementChild.asInstanceOf[HTMLButtonElement]

assert(buttonElement.tagName.toLowerCase == "button")
assert(buttonElement.className.nonEmpty)
}

test("Can pass in inner ref to get DOM element") {
val targetElem = dom.document.createElement("div")
val comp = styled.button[Unit](
css""
)

var buttonElement: Element = null

ReactDOM.render(
comp()(
innerRef := (e => {
buttonElement = e
})
),
targetElem
)

assert(buttonElement.tagName.toLowerCase == "button")
}
}

0 comments on commit 2facdea

Please sign in to comment.
0