Let’s create a Slider

Let’s create a Slider

Let’s create a Slider

I have started to write code on Scala.js half a year ago and I can’t stop doing it. I should say that I love JavaScript and I wrote hundreds of thousands lines of code. But there is one thing that always frustrates me in JavaScript: It hasn’t many useful things that other languages have. You should use some or other framework (I used yui and jquery in the past). I used CoffeeScript a year ago. Today it’s time for Babel. Nobody wants writing plain JavaScript anymore! I choose Scala.

 

See the Demo to this post.

A Slider is a program that shows slides. Usually it is written on JavaScript. Let’s write our code on Scala and I want to use React for this purpose.

First of all, our slider should have these features:

  • Slides should automatically change one another;
  • Every slide can have its style, class, text (with a link or not), children elements (containers);
  • Containers can have their own children elements;
  • Every children element can be one of three types: block, img or linked-img;
  • We can define appearance of children elements by adding style or className properties;
  • We can animate children elements by adding a special property classIn to their class attributes when a slide is active;
  • The first slide is special. You can use it for “loading” animation. You can specify that it be waiting until all images are completely loaded;

OK. Let’s start!

Dependencies

We need some dependencies


libraryDependencies ++= Seq(
  "org.scala-js" %%% "scalajs-dom" % "0.9.1",
  "com.github.japgolly.scalajs-react" %%% "core" % "0.11.3",
  "com.github.japgolly.scalajs-react" %%% "extra" % "0.11.3"
)

jsDependencies ++= Seq(

  "org.webjars.bower" % "react" % "15.4.1"
    /        "react-with-addons.js"
    minified "react-with-addons.min.js"
    commonJSName "React",

  "org.webjars.bower" % "react" % "15.4.1"
    /         "react-dom.js"
    minified  "react-dom.min.js"
    dependsOn "react-with-addons.js"
    commonJSName "ReactDOM"
)

 

Data Structure

First of all, we need data structure.

I use case class to define Slide Properties:


case class SliderProps(
    list: List[SlideProps],
    generals: SliderGenerals,
    preloads: Seq[String] = Seq.empty
 )

where list is a list of properties for every slide, generals is a set of general properties (for the slider in whole), preloads is a sequence of strings, which specifies images, that we need to load to the browser before our slider starts to show slides.

So we need to define these case classes:


case class SlideProps(
   style: Option[js.Dictionary[Any]] = None,
   text: Option[String] = None,
   className: Option[String] = None,
   link: Option[String] = None,
   containers: Seq[SlideContainer] = Seq.empty,
   active: Boolean = false
 )

case class SliderGenerals(
   controlsType: Option[String] = None,
    delay: Int = 4000,
    firstDelay: Int = 500
)

 

SliderGenerals – general properties. There are no much general properties. ControlsTypes is reserved for controls. I’m sure that control buttons is a good idea for the slider. May be I’ll do them later…

SlideProps – every slide has its own properties. These properties specify appearance and content. For example, you can simply do this:


SlideProps(
   style = Some(js.Dictionary(
      "backgroundImage" -> "url(mycat.png)"
   )),
   text = "I love my cat!"
)

And you get a photo of your cat with a caption.

Of course the power of this slider is in using the containers attribute!


case class SlideContainer(
   elementType: String,
   className: Option[String] = None,
   style: Option[js.Dictionary[Any]] = None,
   children: Seq[SlideChild] = Seq.empty,
   classIn: Option[String] = None,
   src: Option[String] = None,
   link: Option[String] = None,
   text: Option[String] = None
)

case class SlideChild(
   elementType: String,
   style: Option[js.Dictionary[Any]] = None,
   text: Option[String] = None,
   src: Option[String] = None,
   link: Option[String] = None,
   className: Option[String] = None,
   classIn: Option[String] = None
)

elementType is the only required attribute. It can be: block, img, linked-img.

If the type is img or linked-img, you can’t place any content to it.

If the type is block, you can place text or children.

 

State

Our state has only one field:


case class State(active: Int)

It stores an index of an active slide.

 

Slide

We need a React component for a Slide:


  val activeAtr = "data-active".reactAttr

  val Slide = ReactComponentB[SlideProps]("Slide")
    .render_P(p => {

      def renderElement(elementType: String, className: Option[String],
            style: Option[js.Dictionary[Any]],
            classIn: Option[String], src: Option[String],
            link: Option[String], text: Option[String],
            children: Seq[SlideChild] = Seq.empty): ReactElement = {
        elementType match {
          case t if t == "block" => {
            <.div(
              ^.classSet1(
                 className.getOrElse(""),
                 classIn.getOrElse("") -> p.active),
              p.active ?= (^.style := style),
              text,
              children.map(y => {
                renderElement(
                  y.elementType,
                  y.className,
                  y.style,
                  y.classIn,
                  y.src,
                  y.link,
                  y.text)
               }
              )

            )
          }
          case img if img == "img" => {
            <.img(
               ^.src := src,
               ^.classSet1(
                  className.getOrElse(""),
                  classIn.getOrElse("") -> p.active),
               p.active ?= (^.style := style))
          }
          case img if img == "linked-img" => {
            <.a(^.href := link,
              <.img(
                 ^.src := src,
                 ^.classSet1(
                    className.getOrElse(""),
                    classIn.getOrElse("") -> p.active),
                 p.active ?= (^.style := style)))
          }
          case _ =>  true
        ),
        activeAtr := p.active,
        ^.style := p.style,
        <.div(
           ^.className := "slider__slide__text",
           p.text.map(t => <.a(^.href := p.link, t)) ),
           p.containers.map(x => {
              renderElement(
                 x.elementType,
                 x.className,
                 x.style,
                 x.classIn,
                 x.src,
                 x.link,
                 x.text,
                 x.children
              )
           } 
        )
      )
    }
    )
    .build

The function renderElement is a major part of this code. It is called if a slide has a containers property with a sequence of container elements, or if a container has a children property with a sequence of child elements. This function check a type of every element and creates markup in accordance with a type and other parameters of an element. This function can be called recursively.

 

Backend

Let’s look at the Backend:


  class Backend($: BackendScope[SliderProps, State]) {

    var timer: Option[Int] = None

    def nextSlide = Callback {
      val active = ($.state.runNow().active + 1) match {
        case x if x < $.props.runNow().list.length => x
        case 0 => 1
        case _ => 1
      }
      $.setState(State(active)).runNow()
    }

    def previousSlide = Callback {
      val active = ($.state.runNow().active - 1) match {
        case x if x <= 0 => $.props.runNow().list.length - 1
        case x => x
      }
      $.setState(State(active)).runNow()
    }

    def render = {
      val props: SliderProps = $.props.runNow()
      val slides: List[SlideProps] = props.list
      <.div(^.className := "slider", slides.zipWithIndex.map(x => {
          val slide = x._1
          val index = x._2
          Slide(SlideProps(style = slide.style,
            className = slide.className,
            text = slide.text,
            active = index == $.state.runNow().active,
            link = slide.link,
            containers = slide.containers
          ))
        }), props.generals.controlsType match {
          case Some(_) => <.div(
            <.div(^.className := "slider__next", ^.onClick --> nextSlide,
              <.i(^.className := "fa fa-4x fa-arrow-circle-right")
            ),
            <.div(^.className := "slider__previous", ^.onClick --> previousSlide,
              <.i(^.className := "fa fa-4x fa-arrow-circle-left") ) ) case None => <.div()

        }

      )

    }
  }

The mutable timer variable stores information about a currant timeout instance.

Methods nextSlide and previousSlide will be used to switch our slides back and forth.

The method render create markup for our slider. As you can see I included  control buttons to the code, but I don’t use controls in this examples (may be next demo will include controls).

 

 

 

Slider

Now we can create our Slider React component:


  private val Slider = ReactComponentB[SliderProps]("Slider")
    .initialState(State(0))
    .renderBackend[Backend]
    .componentDidUpdate(i => {
      i.$.backend.timer.map(i => window.clearTimeout(i))
      Callback(i.$.backend.timer = Option(window.setTimeout(() =>
         i.$.backend.nextSlide.runNow(), i.$.props.generals.delay)))
    })
    .componentDidMount(i => {
      i.backend.timer.map(c => window.clearTimeout(c))

      def onLoadFuture(img: HTMLImageElement) = {
        if (img.complete) {
          Future.successful(img.src)
        } else {
          val p = Promise[String]()
          img.onload = { (e: Event) => {

            p.success(img.src)
          }
          }
          p.future
        }
      }

      def getInitialInterval(t: Int) = {
        val p = Promise[String]()
        window.setTimeout(() => p.success("!"), t)
        p.future
      }


      val futures = i.props.preloads.map(s => {
        val img = document.createElement("img").asInstanceOf[HTMLImageElement]
        img.src = s
        onLoadFuture(img)
      }) :+ getInitialInterval(i.props.generals.firstDelay)

      Callback(Future.sequence(futures).onComplete(_ => {
        i.backend.nextSlide.runNow()
      }))
    })
    .componentWillUnmount(i => 
        Callback(i.backend.timer.map(c => window.clearTimeout(c))))
    .build

The method componentDidUpdate sets a new timeout for a call of the nextSlide method. It supports continuity of our slideshow.

The method componentDidMount does more sophisticated work: it calls the nextSlide method only if all images are loaded.

See the full code of this example.

As you can see Scala.js works great.

 

 

 

 

Please follow and like us:

About Alexandre Kremlianski

Scala / Scala.js / JavaScript programmer

Leave a Reply

Your email address will not be published. Required fields are marked *