Let’s create a Slider. Again…
There have been three posts about creating a Slider with Scala.js already. The first post was about how to do it in principal. The second post was about the ScalaCSS library in general and how one could use it with our Slider in particular. The third post was about how to use a JSON object with our Slider.
Now it’s time to compare Scala.js with its closest alternative: I mean TypeScript (a typed superset of JavaScript that compiles to plain JavaScript).
It’s fun: the first version of this Slider was written in CoffeeScript. It took me about 3 hours. I spent five times more doing the same with Scala.js (to tell the truth, I was novice in Scala.js). I think it would be interesting to make the same Slider in TypeScript and to see what happens.
Let’s start!
Preparing
To set the project in TypeScript is not so trivial as in Scala.js. You should set several configuration files:
- package.json – the main configuration file for any npm project
- tsconfig.json – the typescript configuration file that specifies the root files and the compiler options required to compile the project
- gulpfile.js – I’ll use Gulp as a build tool.
Besides that, you should download the Definitely Typed files for React and ReactDOM. I used Typings – The TypeScript Definition Manager.
So, if you make a TypeScript project from scratch, you need:
- Install Node.js
- Install TypeScript
- Make configuration files
- Install Typings
- Download all dependencies
- Download the Definitely Typed files
- Install one of recommended editors (I use Visual Studio Code)
Don’t panic! It seems terrible only for the first time!
To start editing my Slider:
- Install Node.js
- Clone the project from GitHub:
git clone https://github.com/Kremlianski/typescript-slider-demo.git - Install TypeScript:
npm install typescript -g - cd typescript-slider-demo
- npm install
Data Structure
From TypeScript official handbook:
One of TypeScript’s core principles is that type-checking focuses on the shape that values have. This is sometimes called “duck typing” or “structural subtyping”. In TypeScript, interfaces fill the role of naming these types, and are a powerful way of defining contracts within your code as well as contracts with code outside of your project.
So we need to specify interfaces. Here is a structure.ts file:
export interface SlideContainer {
elementType: string
className?: string
style?: {[index: string]: string}
children?: SlideChild[]
classIn?: string
src? : string
link?: string
text?: string
}
export interface SlideChild {
elementType: string
style?: {[index: string]: string}
text?: string
src? : string
link?: string
className?: string
classIn?: string
}
export interface SliderControls {
controlsType: string
}
export interface SlideProps {
style?: {[index: string]: string}
text?: string
className?: string
link?: string
containers?: SlideContainer[]
active: boolean
}
export interface SliderProps {
list: SlideProps[]
generals: SliderGenerals
preloads: string[]
}
export interface SliderGenerals {
controlsType?: string
delay: number
firstDelay: number
}
export interface State {
active: number
}
These interfaces do the same that case classes do in our Scala.js example:
...
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
)
...
There is no Option
type in TypeScript, but you can use optional members instead. More important: you can’t use default values in TypeScript Interfaces.
Let’s return to our structure. I need to pass to Slider
Element an object of type SliderProps
:
interface SliderProps {
list: SlideProps[]
generals: SliderGenerals
preloads: string[]
}
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 I need to define these interfaces:
interface SlideProps {
style?: {[index: string]: string}
text?: string
className?: string
link?: string
containers?: SlideContainer[]
active: boolean
}
interface SliderGenerals {
controlsType?: string
delay: number
firstDelay: number
}
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.
Of course the power of this slider is in using the containers
attribute!
interface SlideContainer {
elementType: string
className?: string
style?: {[index: string]: string}
children?: SlideChild[]
classIn?: string
src? : string
link?: string
text?: string
}
interface SlideChild {
elementType: string
style?: {[index: string]: string}
text?: string
src? : string
link?: string
className?: string
classIn?: string
}
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.
Our state has only one field:
interface State {
active: number
}
It stores an index of an active slide.
Slide Element
We need a React component for a Slide:
class Slide extends React.Component<SlideProps, {}> {
constructor(props:SlideProps) {
super(props)
}
render() {
const props = this.props
function renderElement(
i: number,
elementType: string,
className?: string,
style?: {[index: string]: string},
classIn?: string,
src?: string,
link?: string,
text?: string,
children: SlideChild[] = []): JSX.Element {
let result: JSX.Element
let classString = ""
if (className) classString += className
if (className && classIn && props.active) classString += " "
if (classIn && props.active) classString += classIn
let style0: {[index: string]: string} = {}
if (props.active) style0 = style
switch(elementType) {
case 'block':
return (
<div key={i} className={classString} style={style0}>{text} {
children.map ((y, i) =>
renderElement(i, y.elementType, y.className,
y.style,y.classIn, y.src, y.link, y.text)
)
}
</div>)
case 'img': return (
<img key={i} src={src} className={classString} style={style0} />
)
case 'linked-img': return <a key={i} href={link}>
<img src={src} className={classString} style={style0} />
</a>
default: return <div />
}
}
let classString = "slider__slide"
if(props.className) classString += " " + props.className
return (
<div className={classString} data-active={props.active} style={props.style}>
<div className="slider__slide__text">
{
props.text && <a href={props.link}>{props.text}</a>
}
</div>
{props.containers && props.containers.map ((x, i) =>
renderElement(i, x.elementType, x.className,
x.style,x.classIn, x.src, x.link, x.text, x.children))}
</div>
)
}
}
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.
You can compare this code with the code of the Slide
element from Scala.js example:
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
Slider Element
The code of the Slider
Element:
export default class Slider extends React.Component<SliderProps, State> {
timer: number
constructor(props:SliderProps) {
super(props)
this.previousSlide = this.previousSlide.bind(this)
this.nextSlide = this.nextSlide.bind(this)
this.state = { active: 0 }
}
nextSlide(){
let slide
const activeSlide = this.state.active
if (activeSlide + 1 < this.props.list.length) slide = activeSlide + 1
else slide = 0
if (slide == 0) slide = 1
this.setState({active: slide})
}
previousSlide() {
let slide
const activeSlide = this.state.active
if (activeSlide - 1 <= 0) slide = this.props.list.length - 1
else slide = activeSlide - 1
this.setState({active: slide})
}
componentDidMount(){
if(this.timer) window.clearTimeout(this.timer)
function onLoadPromise(img:HTMLImageElement) {
if (img.complete) {
return Promise.resolve(img.src);
} else {
const p = new Promise((success) => {
img.onload = (e) => {
success(img.src);
};
});
return p;
}
}
function getInitialInterval(t: number):Promise<string> {
const f = new Promise(success =>{
window.setTimeout(() => success("!"), t)
})
return f
}
if (this.props.preloads && this.props.preloads.length > 0) {
let promises: Promise<string>[] = this.props.preloads.map(s => {
const img = document.createElement("img")
img.src = s
return onLoadPromise(img)
})
promises.push(getInitialInterval(this.props.generals.firstDelay))
Promise.all(promises).then((ignore) => this.nextSlide())
} else this.timer = window.setTimeout(this.nextSlide, this.props.generals.firstDelay)
}
componentDidUpdate() {
if(this.timer) window.clearTimeout(this.timer)
this.timer = window.setTimeout(this.nextSlide, this.props.generals.delay)
}
componentWillUnmount() {
if(this.timer) window.clearTimeout(this.timer)
}
render() {
const slides = this.props.list
return <div className="slider">
{slides.map((slide, index, array) =>
<Slide key={index}
style={slide.style}
className={slide.className}
text={slide.text}
containers={slide.containers}
active={index == this.state.active}
link={slide.link} />
)}
{
this.props.generals.controlsType && <div className="controls">
<div className="slider__next" onClick={this.nextSlide}>
<i className="fa fa-4x fa-arrow-circle-right"></i>
</div>
<div className="slider__previous" onClick={this.previousSlide}>
<i className="fa fa-4x fa-arrow-circle-left"></i>
</div>
</div>
}
</div>
}
}
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. Compare this with componentDidMount
method from Scala.js example:
.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()
}))
})
Both examples have very similar code of this method. In Scala.js the Future
is used in cases where in JavaScript the Promise
is used . But to convert a callback into a Future
in Scala you need to use a Promise.
There is no the Backend class in the TypeScript example. But in general examples have very similar code. It seems to me, that the TypeScript variant is more elegant, but it’s the matter of taste.
The main difference occurs when you try to pass a JavaScript object as a Slider
property. As you can remember I wrote a facade in Scala.js example (additional 130 lines of Scala code). From the other side, TypeScript uses “duck typing” and it works well: you don’t need any additional code. But of course you can use case classes in your Scala.js projects and be happy.
Size
Scala.js resulting JavaScript compressed files with all dependencies weight 344KB.
TypeScript bundle file weights 175KB.
It’s not crucial but much less.
Editor
I use IntelliJ IDEA for Scala and Visual Studio Code for TypeScript. Both editors are very good.
It seems to me that VS Code is faster. And there are no annoying bugs. For example:
I reported this issue three months ago, but nothing happened yet.
Conclusion
I think that Scala is the best programming language in the world. TypeScript possesses less expressiveness. It’s a fact.
From the other hand, TypeScript is JavaScript by nature. And it has almost absolute compatibility with a vast bulk of JavaScript libraries. Scala.js doesn’t. For example, look at Scala.js React library. It’s very good. But don’t you think, it’s cumbersome a bit? Compare, for instance:
componentWillUnmount() {
if(this.timer) window.clearTimeout(this.timer)
}
and
.componentWillUnmount(i =>
Callback(i.backend.timer.map(c => window.clearTimeout(c))))
And at last. There is an illusion that someone can successfully write Scala.js code without knowledge of JavaScript. No, he can’t. But if a Scala coder should learn JavaScript when he want to become a front-end developer, why wouldn’t he try TypeScript as well.
See the full code of this example.
Related
Page with Comments
Leave a Reply Cancel reply
This site uses Akismet to reduce spam. Learn how your comment data is processed.
I didn’t read the whole article , but in your conclusion
componentWillUnmount() {
if(this.timer) window.clearTimeout(this.timer)
}
vs
.componentWillUnmount(i =>
Callback(i.backend.timer.map(c => window.clearTimeout(c))))
hmm thats one way of writing, if you want ES6 classes use sri : https://github.com/chandu0101/sri or just write your own facade(hardly 12 LOC)
Using Sri :
class Slider extends Component[Props,State] {
def componentWillUnmount() = {
if(this.timer) window.clearTimeout(this.timer)
}
}