Skip to content

Instantly share code, notes, and snippets.

@Daxten
Created December 30, 2015 01:09
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Daxten/93076e98046f38448c09 to your computer and use it in GitHub Desktop.
Save Daxten/93076e98046f38448c09 to your computer and use it in GitHub Desktop.
package components
import japgolly.scalajs.react._
import japgolly.scalajs.react.vdom.prefix_<^._
import scala.collection.mutable.ListBuffer
import scalacss.Defaults._
import scalacss.ScalaCssReact._
import scalacss.mutable.StyleSheet
class TypedReactTable[T, P](name: String) {
import TypedReactTable._
private val headerRenderer: ListBuffer[((Vector[T], P) => ReactElement, P => (T, T) => Boolean)] = ListBuffer.empty
private val footerRenderer: ListBuffer[(Vector[T], P) => ReactElement] = ListBuffer.empty
private val columnRenderer: ListBuffer[(T, P) => ReactElement] = ListBuffer.empty
private val filterFunctions: ListBuffer[(T, P, String) => Boolean] = ListBuffer.empty
def header(renderer: (Vector[T], P) => ReactElement, sorter: P => (T, T) => Boolean) = {
headerRenderer += ((renderer, sorter))
this
}
def column(renderer: (T, P) => ReactElement): TypedReactTable[T, P] = {
columnRenderer += renderer
this
}
def footer(renderer: (Vector[T], P) => ReactElement): TypedReactTable[T, P] = {
footerRenderer += renderer
this
}
def filter(filter: (T, P, String) => Boolean) = {
filterFunctions += filter
this
}
def build(rowKey: T => String, shouldRowUpdate: (T, T, P, P) => Boolean = (old, next, oldP, nextP) => true, style: ReactTableStyle = DefaultReactTableStyle) = {
val component = ReactComponentB[Props[T, P]](name)
.initialState_P(p => State(filterText = "", offset = 0, p.rowsPerPage, p.data, None))
.backend(e => new Backend[T, P](e))
.render(e => e.backend.render(e.props, e.state))
.componentWillReceiveProps(e => Callback.ifTrue(e.$.props.data != e.nextProps.data, e.$.backend.onTextChange(e.nextProps, e.$.state.filterText)))
.build
def apply(
data: Vector[T],
properties: P
) = component(new Props[T, P](data, properties, style, headerRenderer.toList, columnRenderer.toList, footerRenderer.toList, filterFunctions.toList, 10, filterFunctions.nonEmpty, rowKey, shouldRowUpdate))
apply(_: Vector[T], _: P)
}
}
object TypedReactTable {
def apply[T, P](name: String) = new TypedReactTable[T, P](name)
sealed trait SortDirection
object Asc extends SortDirection
object Desc extends SortDirection
case class State[T](filterText: String,
offset: Int,
rowsPerPage: Int,
filteredModels: Vector[T],
sortedState: Option[(Int, SortDirection)]
)
class Props[T, P](val data: Vector[T],
val properties: P,
val style: ReactTableStyle,
val header: List[((Vector[T], P) => ReactElement, P => (T, T) => Boolean)],
val columns: List[(T, P) => ReactElement],
val footer: List[(Vector[T], P) => ReactElement],
val filterFunctions: List[(T, P, String) => Boolean],
val rowsPerPage: Int,
val enableSearch: Boolean,
val rowKey: T => String,
val shouldRowUpdate: (T, T, P, P) => Boolean
)
class ReactTableStyle extends StyleSheet.Inline {
import dsl._
val table = style(
width(100 %%),
boxShadow := "0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 1px 2px 0 rgba(0, 0, 0, 0.24)"
)
val tableRow = style(
padding :=! "0.8rem",
&.hover(
backgroundColor :=! "rgba(244, 244, 244, 0.77)"
)
)
val tableHeader = style(
fontWeight.bold,
borderBottom :=! "1px solid #e0e0e0",
tableRow
)
val settingsBar = style(
margin :=! "15px 0",
justifyContent.spaceBetween
)
val sortIcon = styleF.bool(ascending => styleS(
&.after(
fontSize(9 px),
marginLeft(5 px),
if (ascending) content := "'\\25B2'"
else content := "'\\25BC'"
)
))
}
object DefaultReactTableStyle extends ReactTableStyle
class Backend[T, P]($: BackendScope[Props[T, P], State[T]]) {
def onTextChange(p: Props[T, P], value: String): Callback =
$.modState(s => s.copy(
filteredModels = getFilteredModels(p, s, value, s.sortedState, p.data),
offset = 0,
filterText = value
))
def onPreviousClick: Callback =
$.modState(s => s.copy(offset = s.offset - s.rowsPerPage))
def onNextClick: Callback =
$.modState(s => s.copy(offset = s.offset + s.rowsPerPage))
def setOffset(n: Int): Callback =
$.modState(s => s.copy(offset = n))
def getFilteredModels(props: Props[T, P], s: State[T], text: String, sortState: Option[(Int, SortDirection)], models: Vector[T]) = {
val filteredModel = models
.filter(e => props.filterFunctions.exists(f => f(e, props.properties, text)))
sortState.fold(filteredModel) { sortState =>
if (sortState._2 == Desc)
filteredModel
.sortWith(props.header.zipWithIndex.drop(sortState._1).head._1._2(props.properties))
else
filteredModel
.sortWith(props.header.zipWithIndex.drop(sortState._1).head._1._2(props.properties))
.reverse
}
}
def sort(p: Props[T, P], f: (T, T) => Boolean, column: Int): Callback =
$.modState { s =>
s.sortedState match {
case Some(sorted) if sorted._2 == Desc => s.copy(sortedState = Some(column -> Asc))
case Some(sorted) => s.copy(sortedState = None)
case None => s.copy(sortedState = Some(column -> Desc))
}
} >> $.modState { s =>
s.copy(
filteredModels = getFilteredModels(p, s, s.filterText, s.sortedState, p.data)
)
}
def onPageSizeChange(value: String): Callback = $.modState(_.copy(rowsPerPage = value.toInt))
case class RowProps(element: T, props: Props[T, P])
val tableRow = ReactComponentB[RowProps]("TableRow")
.render { $ =>
val p = $.props
val style = p.props.style
<.tr(style.tableRow)(
p.props.columns.map { column =>
<.td()(column(p.element, p.props.properties))
}
)
}
.shouldComponentUpdate(t => t.nextProps.props.shouldRowUpdate(t.$.props.element, t.nextProps.element, t.$.props.props.properties, t.nextProps.props.properties))
.build
def render(p: Props[T, P], s: State[T]) = {
val style = p.style
val numColumns = p.columns.length
val numCurrentPage = s.offset / s.rowsPerPage + 1
val numPages = Math.ceil(s.filteredModels.length.toDouble / s.rowsPerPage).toInt
<.table(style.table)(
<.thead()(
<.tr(style.tableHeader)(
<.th(^.colSpan := numColumns)(
p.filterFunctions.nonEmpty ?= <.input(^.placeholder := "filter ..", ^.value := s.filterText, ^.onChange ==> { e: ReactEventI => onTextChange(p, e.currentTarget.value) })(),
<.span(^.cls := "count")(s.filteredModels.length)
)),
<.tr(style.tableHeader)(
p.header.zipWithIndex.map { case (header, i) =>
<.th(^.onClick --> sort(p, header._2(p.properties), i))(
header._1(p.data, p.properties),
s.sortedState.find(_._1 == i).map { e => <.span(style.sortIcon(e._2 == Asc)) }
)
}
)
),
<.tbody()(
s.filteredModels.slice(s.offset, s.offset + s.rowsPerPage).map { date =>
tableRow.withKey(p.rowKey(date))(RowProps(date, p))
}
),
<.tfoot()(
<.tr(style.tableRow)(
<.td(^.colSpan := numColumns)(
<.ul()(
<.li(numCurrentPage == 1 ?= (^.cls := "disabled"), numCurrentPage != 1 ?= ^.onClick --> onPreviousClick)("«"),
(1 to numPages).map { i =>
<.li(numCurrentPage == i ?= (^.cls := "active"), ^.onClick --> setOffset((i - 1) * s.rowsPerPage))(
i
)
},
<.li(numCurrentPage == numPages ?= (^.cls := "disabled"), numCurrentPage != numPages ?= ^.onClick --> onNextClick)("»")
)
)
)
)
)
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment