Created
December 30, 2015 01:09
-
-
Save Daxten/93076e98046f38448c09 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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