├── docs ├── .gitignore ├── script │ └── repl.clj ├── src │ └── cljs │ │ └── re_frame_datatable_docs │ │ ├── config.cljs │ │ ├── events.cljs │ │ ├── core.cljs │ │ ├── subs.cljs │ │ ├── db.cljs │ │ ├── formatters.cljs │ │ ├── components.cljs │ │ ├── sections │ │ ├── usage.cljs │ │ ├── cell_rendering.cljs │ │ ├── sorting.cljs │ │ ├── rows_selection.cljs │ │ ├── styling.cljs │ │ ├── additional_structure.cljs │ │ └── pagination.cljs │ │ ├── table_views.cljs │ │ └── views.cljs ├── resources │ └── public │ │ ├── js │ │ ├── css │ │ │ └── site.css │ │ └── index.html │ │ ├── css │ │ └── site.css │ │ └── index.html ├── README.md ├── css │ └── site.css ├── index.html ├── project.clj └── re-frame-datatable-docs.iml ├── .gitignore ├── test └── re_frame_datatable │ └── core_test.clj ├── project.clj ├── CHANGELOG.md ├── src └── re_frame_datatable │ ├── views.cljs │ └── core.cljs ├── re-frame-datatable.iml ├── README.md └── LICENSE /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /*.log 2 | /target 3 | /*-init.clj 4 | /resources/public/js/compiled 5 | out 6 | /.idea 7 | -------------------------------------------------------------------------------- /docs/script/repl.clj: -------------------------------------------------------------------------------- 1 | (use 'figwheel-sidecar.repl-api) 2 | (start-figwheel!) ;; <-- fetches configuration 3 | (cljs-repl) 4 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/config.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.config) 2 | 3 | (def debug? 4 | ^boolean js/goog.DEBUG) 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /classes 3 | /checkouts 4 | pom.xml 5 | pom.xml.asc 6 | *.jar 7 | *.class 8 | /.lein-* 9 | /.nrepl-port 10 | .hgignore 11 | .hg/ 12 | /.idea 13 | -------------------------------------------------------------------------------- /docs/resources/public/js/css/site.css: -------------------------------------------------------------------------------- 1 | .ui.section { 2 | margin-bottom: 2.5em; 3 | } 4 | 5 | .inline-code { 6 | background-color: rgba(0, 0, 0, 0.04); 7 | border-radius: 3px; 8 | } 9 | -------------------------------------------------------------------------------- /test/re_frame_datatable/core_test.clj: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable.core-test 2 | (:require [clojure.test :refer :all] 3 | [re-frame-datatable.core :refer :all])) 4 | 5 | (deftest a-test 6 | (testing "FIXME, I fail." 7 | (is (= 0 1)))) 8 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/events.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.events 2 | (:require [re-frame.core :as re-frame] 3 | [re-frame-datatable-docs.db :as db])) 4 | 5 | (re-frame/reg-event-db 6 | ::initialize-db 7 | (fn [_ _] 8 | db/default-db)) 9 | 10 | 11 | (re-frame/reg-event-db 12 | ::set-active-section 13 | (fn [db [_ section]] 14 | (assoc db :active-section section))) 15 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject re-frame-datatable "0.6.0" 2 | :description "DataTable component for re-frame 0.8.0+" 3 | :url "https://github.com/kishanov/re-frame-datatable" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | :dependencies [[org.clojure/clojure "1.8.0"] 7 | [org.clojure/clojurescript "1.9.908"] 8 | [reagent "0.7.0"] 9 | [re-frame "0.10.1"]]) 10 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # re-frame-datatable-docs 2 | 3 | A [re-frame](https://github.com/Day8/re-frame) application designed to ... well, that part is up to you. 4 | 5 | ## Development Mode 6 | 7 | ### Run application: 8 | 9 | ``` 10 | lein clean 11 | lein figwheel dev 12 | ``` 13 | 14 | Figwheel will automatically push cljs changes to the browser. 15 | 16 | Wait a bit, then browse to [http://localhost:3449](http://localhost:3449). 17 | 18 | ## Production Build 19 | 20 | 21 | To compile clojurescript to javascript: 22 | 23 | ``` 24 | lein clean 25 | lein cljsbuild once min 26 | ``` 27 | -------------------------------------------------------------------------------- /docs/css/site.css: -------------------------------------------------------------------------------- 1 | table.re-frame-datatable > thead th.sorted-by:after { 2 | display: inline-block; 3 | content: '\f0dc'; 4 | } 5 | 6 | table.re-frame-datatable > thead th.sorted-by.asc:after { 7 | content: '\f0d8'; 8 | } 9 | 10 | table.re-frame-datatable > thead th.sorted-by.desc:after { 11 | content: '\f0d7'; 12 | } 13 | 14 | 15 | table.re-frame-datatable > thead th:after { 16 | display: none; 17 | font-family: Icons; 18 | margin-left: .5em; 19 | } 20 | 21 | .ui.section { 22 | margin-bottom: 2.5em; 23 | } 24 | 25 | .inline-code { 26 | background-color: rgba(0, 0, 0, 0.04); 27 | border-radius: 3px; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /docs/resources/public/css/site.css: -------------------------------------------------------------------------------- 1 | table.re-frame-datatable > thead th.sorted-by:after { 2 | display: inline-block; 3 | content: '\f0dc'; 4 | } 5 | 6 | table.re-frame-datatable > thead th.sorted-by.asc:after { 7 | content: '\f0d8'; 8 | } 9 | 10 | table.re-frame-datatable > thead th.sorted-by.desc:after { 11 | content: '\f0d7'; 12 | } 13 | 14 | 15 | table.re-frame-datatable > thead th:after { 16 | display: none; 17 | font-family: Icons; 18 | margin-left: .5em; 19 | } 20 | 21 | .ui.section { 22 | margin-bottom: 2.5em; 23 | } 24 | 25 | .inline-code { 26 | background-color: rgba(0, 0, 0, 0.04); 27 | border-radius: 3px; 28 | } 29 | 30 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/core.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.core 2 | (:require [reagent.core :as reagent] 3 | [re-frame.core :as re-frame] 4 | [re-frame-datatable-docs.events :as events] 5 | [re-frame-datatable-docs.subs] 6 | [re-frame-datatable-docs.views :as views] 7 | [re-frame-datatable-docs.config :as config])) 8 | 9 | 10 | (defn dev-setup [] 11 | (when config/debug? 12 | (enable-console-print!) 13 | (println "dev mode"))) 14 | 15 | (defn mount-root [] 16 | (reagent/render [views/main-panel] 17 | (.getElementById js/document "app"))) 18 | 19 | (defn ^:export init [] 20 | (re-frame/dispatch-sync [::events/initialize-db]) 21 | (dev-setup) 22 | (mount-root)) 23 | -------------------------------------------------------------------------------- /docs/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/resources/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/resources/public/js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 |
22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/subs.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.subs 2 | (:require-macros [reagent.ratom :refer [reaction]]) 3 | (:require [re-frame.core :as re-frame] 4 | [re-frame-datatable.core :as dt])) 5 | 6 | 7 | (re-frame/reg-sub 8 | ::songs-list 9 | (fn [db] 10 | (get-in db [:sample-data :songs]))) 11 | 12 | 13 | (re-frame/reg-sub 14 | ::basic-definition-data 15 | (fn [] 16 | (re-frame/subscribe [::songs-list])) 17 | (fn [songs-list] 18 | (->> songs-list 19 | (take 5) 20 | (map #(select-keys % [:name :index :stats]))))) 21 | 22 | 23 | (re-frame/reg-sub 24 | ::total-play-count 25 | (fn [] 26 | (re-frame/subscribe [::basic-definition-data])) 27 | (fn [songs-list] 28 | (->> songs-list 29 | (map #(get-in % [:stats :play_count])) 30 | (apply +)))) 31 | 32 | 33 | 34 | (re-frame/reg-sub 35 | ::pagination-data 36 | (fn [] 37 | (re-frame/subscribe [::songs-list])) 38 | (fn [songs-list] 39 | (->> songs-list 40 | (map #(select-keys % [:name :index :stats]))))) 41 | 42 | 43 | 44 | (re-frame/reg-sub 45 | ::cell-rendering-data 46 | (fn [] 47 | (re-frame/subscribe [::songs-list])) 48 | (fn [songs-list] 49 | (->> songs-list 50 | (take 5) 51 | (map #(select-keys % [:index :name :artist :duration :album]))))) 52 | 53 | 54 | 55 | (re-frame/reg-sub 56 | ::marking-elements-data 57 | (fn [] 58 | (re-frame/subscribe [::songs-list])) 59 | (fn [songs-list] 60 | (->> songs-list 61 | (take 7) 62 | (map #(select-keys % [:index :name :stats]))))) 63 | 64 | 65 | (re-frame/reg-sub 66 | ::empty-dataset 67 | (fn [] 68 | [])) 69 | 70 | 71 | (re-frame/reg-sub 72 | ::active-section 73 | (fn [db] 74 | (:active-section db))) 75 | 76 | -------------------------------------------------------------------------------- /docs/project.clj: -------------------------------------------------------------------------------- 1 | (defproject re-frame-datatable-docs "0.1.0-SNAPSHOT" 2 | :dependencies [[org.clojure/clojure "1.8.0"] 3 | [org.clojure/clojurescript "1.9.908"] 4 | [figwheel-sidecar "0.5.13"] 5 | [reagent "0.7.0"] 6 | [re-frame "0.10.1"] 7 | [re-frame-datatable "0.6.0"]] 8 | 9 | :plugins [[lein-cljsbuild "1.1.7"]] 10 | 11 | :min-lein-version "2.5.3" 12 | 13 | :source-paths ["src/clj" "script"] 14 | 15 | :clean-targets ^{:protect false} ["resources/public/js/compiled" "target"] 16 | 17 | :figwheel {:css-dirs ["resources/public/css"]} 18 | 19 | :profiles 20 | {:dev 21 | {:dependencies [[binaryage/devtools "0.8.2"]] 22 | :plugins [[lein-figwheel "0.5.13"]]}} 23 | 24 | :cljsbuild 25 | {:builds 26 | [{:id "dev" 27 | :source-paths ["src/cljs"] 28 | :figwheel {:on-jsload "re-frame-datatable-docs.core/mount-root"} 29 | :compiler {:main re-frame-datatable-docs.core 30 | :output-to "resources/public/js/compiled/app.js" 31 | :output-dir "resources/public/js/compiled/out" 32 | :asset-path "js/compiled/out" 33 | :source-map-timestamp true 34 | :preloads [devtools.preload] 35 | :external-config {:devtools/config {:features-to-install :all}}}} 36 | 37 | {:id "min" 38 | :source-paths ["src/cljs"] 39 | :compiler {:main re-frame-datatable-docs.core 40 | :output-to "resources/public/js/compiled/app.js" 41 | :optimizations :whitespace 42 | :closure-defines {goog.DEBUG false} 43 | :pretty-print false}}]}) 44 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/db.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.db) 2 | 3 | (def default-db 4 | {:sample-data {:songs 5 | (->> 6 | [{:index 1 :name "Black Porch Bogie" :duration 122 :stats {:play_count 2 :rating 5}} 7 | {:index 2 :name "Georgia on my Mind" ::duration 260 :stats {:play_count 0}} 8 | {:index 3 :name "Salty Dog Rag" :duration 107 :stats {:play_count 1 :rating 2}} 9 | {:index 4 :name "Rattenbury Rag" :duration 164 :stats {:play_count 2 :rating 3}} 10 | {:index 5 :name "Steppin' Out" :duration 87 :stats {:play_count 0}} 11 | {:index 6 :name "Black Mountain Rag" :duration 139 :stats {:play_count 3 :rating 4}} 12 | {:index 7 :name "Flight of the Humminbird" :duration 191 :stats {:play_count 0}} 13 | {:index 8 :name "Limehouse Blues" :duration 104 :stats {:play_count 1}} 14 | {:index 9 :name "Linus and Lucy" :duration 172 :stats {:play_count 5 :rating 5}} 15 | {:index 10 :name "Mostly Merle" :duration 131 :stats {:play_count 2}} 16 | {:index 11 :name "Mozard 101" :duration 235 :stats {:play_count 11 :rating 4}} 17 | {:index 12 :name "Wizard of Oz Medley" :duration 193 :stats {:play_count 4}} 18 | {:index 13 :name "Knights of the Round Table" :duration 92 :stats {:play_count 2}}] 19 | (map #(-> % 20 | (assoc :artist "Buster B. Jones") 21 | (assoc :album {:name "A Decade of Buster B. Jones" 22 | :year 2005}))))} 23 | :active-section (let [anchor (clojure.string/replace js/window.location.hash #"^\#" "")] 24 | (if (#{"usage" 25 | "basic" 26 | "css-options" 27 | "pagination" 28 | "sorting" 29 | "cell-rendering" 30 | "rows-selection" 31 | "additional-structure" 32 | "marking-individual-elements"} 33 | anchor) 34 | anchor 35 | "usage"))}) 36 | 37 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/formatters.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.formatters 2 | (:require [reagent.core :as reagent] 3 | [cljs.pprint :as pp] 4 | [clojure.walk :as walk])) 5 | 6 | 7 | (defn duration-formatter [seconds] 8 | [:span 9 | (let [m (quot seconds 60) 10 | s (mod seconds 60)] 11 | (if (zero? m) 12 | s 13 | (str m ":" (when (< s 10) 0) s)))]) 14 | 15 | 16 | (defn artist-formatter [artist-name] 17 | [:a {:href (str "https://google.com/search?q=" artist-name)} 18 | artist-name]) 19 | 20 | 21 | (defn album-formatter [{:keys [name year]}] 22 | [:span name 23 | [:small (str " (" year ")")]]) 24 | 25 | 26 | (defn rating-formatter [rating] 27 | (reagent/create-class 28 | {:component-function 29 | (fn [rating] 30 | [:div.ui.star.rating {:data-rating rating}]) 31 | 32 | :component-did-mount 33 | (fn [] 34 | (.ready (js/$ js/document) 35 | (fn [] 36 | (.rating (js/$ ".ui.rating") (js-obj "maxRating" 5)))))})) 37 | 38 | 39 | (defn song-digest-formatter [name song-info] 40 | [:span 41 | {:data-tooltip (str "From album \"" (get-in song-info [:album :name]) "\"")} 42 | (:index song-info) ". " 43 | name]) 44 | 45 | 46 | 47 | (defn formatted-code [data] 48 | [:pre 49 | [:code {:class "clojure"} 50 | (with-out-str 51 | (pp/pprint 52 | (walk/postwalk 53 | (fn [x] 54 | (cond 55 | (fn? x) 56 | (let [fname (last (re-find #"^function re_frame_datatable_docs\$(formatters|table_views)\$(.*?)\(" (str x)))] 57 | (symbol (clojure.string/replace fname #"_" "-"))) 58 | 59 | (and (keyword? x) 60 | (re-seq #"^:re-frame-datatable.core" (str x))) 61 | (-> x 62 | (str) 63 | (clojure.string/replace #"^:re-frame-datatable.core" ":dt") 64 | (keyword)) 65 | 66 | :else 67 | x)) 68 | data)))]]) 69 | 70 | 71 | 72 | (defn formatted-function-def [& sources] 73 | [:div 74 | (for [[i source] (map-indexed vector sources)] 75 | ^{:key i} 76 | [:pre 77 | [:code {:class "clojure"} 78 | source]]) 79 | [:br]]) 80 | 81 | 82 | 83 | (defn play-count-th [] 84 | [:div 85 | {:data-tooltip "Play count"} 86 | [:i.music.icon]]) 87 | 88 | 89 | 90 | (defn sort-play-count-comp [x y] 91 | (compare (:play_count x) (:play_count y))) 92 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/components.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.components 2 | (:require [re-frame.core :as re-frame] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable.core :as dt])) 5 | 6 | 7 | 8 | (defn icon-message [message-class icon-class text & [header]] 9 | [:div.ui.icon.message 10 | {:class message-class} 11 | [:i.icon {:class icon-class}] 12 | [:div.content 13 | (when header 14 | [:div.header header]) 15 | text]]) 16 | 17 | 18 | (def warning-message (partial icon-message "warning" "warning sign")) 19 | (def info-message (partial icon-message "info" "info circle")) 20 | 21 | 22 | 23 | (defn tabs-wrapper [dt-id data-sub columns-def options & [extra-tabs dt-container]] 24 | (let [data (re-frame/subscribe data-sub) 25 | example-dom-id (str (name dt-id) "-example") 26 | usage-dom-id (str (name dt-id) "-usage") 27 | data-dom-id (str (name dt-id) "-data") 28 | extra-tabs (map #(assoc % :data-tab (str (name dt-id) (:data-tab %))) 29 | extra-tabs)] 30 | 31 | (fn [] 32 | (let [dt-def [dt/datatable dt-id data-sub columns-def options]] 33 | [:div {:style {:margin-top "2em"}} 34 | [:div.ui.top.attached.tabular.menu 35 | [:a.active.item 36 | {:data-tab example-dom-id} "Example"] 37 | [:a.item 38 | {:data-tab usage-dom-id} "Usage"] 39 | [:a.item 40 | {:data-tab data-dom-id} "Data"] 41 | (doall 42 | (for [{:keys [data-tab label]} extra-tabs] 43 | ^{:key data-tab} 44 | [:a.item 45 | {:data-tab data-tab} label]))] 46 | 47 | [:div.ui.bottom.attached.active.tab.segment 48 | {:data-tab example-dom-id} 49 | (if dt-container 50 | [dt-container dt-def] 51 | dt-def)] 52 | 53 | [:div.ui.bottom.attached.tab.segment 54 | {:data-tab usage-dom-id} 55 | [formatters/formatted-code 56 | (vec (cons `dt/datatable 57 | (->> dt-def (rest) (filter (complement nil?)))))]] 58 | 59 | [:div.ui.bottom.attached.tab.segment 60 | {:data-tab data-dom-id} 61 | [formatters/formatted-code @data]] 62 | 63 | (doall 64 | (for [{:keys [data-tab component]} extra-tabs] 65 | ^{:key data-tab} 66 | [:div.ui.bottom.attached.tab.segment 67 | {:data-tab data-tab} 68 | [component]]))])))) 69 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/usage.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.usage 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | 6 | [re-frame-datatable-docs.subs :as subs] 7 | [re-frame-datatable.core :as dt] 8 | 9 | [cljs.repl :as r])) 10 | 11 | 12 | 13 | (defn definition [] 14 | [:div 15 | [:div "re-frame-datatable should be used as any other Reagent component. First, require it in the file that contains your re-frame application views:"] 16 | [:pre 17 | [:code {:class "clojure"} 18 | "(:require [re-frame-datatable.core :as dt])"]] 19 | 20 | [:p "Then define a Reagent component which uses a datatable:"] 21 | 22 | [:pre 23 | [:code {:class "clojure"} 24 | "(defn my-component [] 25 | [dt/datatable 26 | datatable-key ; a keyword, that will be used in re-frame's `app-db` to store datatable state 27 | subscription-vec ; a vector, which will be used by datatable to render data via `(re-frame/subscribe subscription-vec)` 28 | columns-def ; a vector of maps, that defines how each column of datatable should look like 29 | options] ; optional map of additional options)"]] 30 | 31 | [components/info-message 32 | [:p "A complete example is available at " 33 | [:a {:href "https://kishanov.github.io/re-frame-datatable-example/"} 34 | "https://kishanov.github.io/re-frame-datatable-example/"]]]]) 35 | 36 | 37 | 38 | (defn basic-usage [] 39 | [:div 40 | [:div 41 | [:p 42 | "There is only 1 mandatory parameter that should be provided for each map in " 43 | [:code.inline-code "columns-def"] " vector:"] 44 | [:ul.ui.list 45 | [:li [:code.inline-code "::column-key"] " - a vector that is used to access value of each item via " [:code "get-in"]]] 46 | 47 | [:p 48 | "To specify the label for the column (the one that will be rendered for a given column inside " [:code.inline-code ""] " element of header row)" 49 | ", use " [:code.inline-code "::column-label"] " parameter. It can be one of the following:"] 50 | 51 | [:ul.ui.list 52 | [:li "A string, that will be rendered as is"] 53 | [:li "A Reagent component"]]] 54 | 55 | [components/tabs-wrapper 56 | :basic-definition 57 | [::subs/basic-definition-data] 58 | [{::dt/column-key [:index]} 59 | {::dt/column-key [:name] 60 | ::dt/column-label "Name"} 61 | {::dt/column-key [:stats :play_count] 62 | ::dt/column-label formatters/play-count-th}] 63 | nil 64 | [{:data-tab "th-fourammter-source" 65 | :label "Play Count Formatter Source" 66 | :component (fn [] 67 | [formatters/formatted-function-def 68 | (with-out-str (r/source formatters/play-count-th))])}]]]) 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Change Log 2 | All notable changes to this project will be documented in this file. This change log follows the conventions of [keepachangelog.com](http://keepachangelog.com/). 3 | 4 | ## [0.6.0] - 2017-09-09 5 | ### Breaking 6 | - Moved default-pagination-controls view component to new namespace: re-frame-datatable.views 7 | - Reorganized documentation in a new way (including subsections) 8 | 9 | ### Changed 10 | - Update to ClojureScript 1.9.908, re-frame 0.10.1 and reagent 0.7.0 11 | 12 | ### Added 13 | - Ability to change pagination size via events (+ default page size selector control) 14 | - Support for custom sorting comparator function via ::comp-fn 15 | 16 | ### Fixed 17 | - Fixed bug with incorrect ::cure-page calculation when data-source has changed and this page is not exist 18 | - Fixed propagation of options via :component-did-update 19 | 20 | 21 | ## [0.5.2] - 2017-05-26 22 | ### Added 23 | - Sorting to work with nested data paths (Thanks to @ChrisHacker) 24 | - Sorting on values other than the column-key value (Thanks to @ChrisHacker) 25 | - Support ints in ::column-key (allows datatable to work with elements in collection that have nested vectors) 26 | 27 | ### Fixed 28 | - Update ClojureScript 1.9.542 and fix spec ns 29 | 30 | 31 | ## [0.5.1] - 2017-01-17 32 | ### Added 33 | - ::column-label became optional 34 | - ::column-label can now accept either string as before or an arbitrary Reagent component 35 | - New option: ::header-enabled? which can be used to prevent DataTable from rendering 36 | - "sortable" columns are marked with special class for styling via CSS 37 | 38 | ## [0.5.0] - 2016-12-30 39 | ### Breaking 40 | - Pagination controls are not rendered by default, extracted into separate component 41 | 42 | ### Added 43 | - Ability to create own pagination controls 44 | - Callback to unselect all rows in DataTable 45 | - Customizable rendering of empty DataTable 46 | 47 | ### Fixed 48 | - Empty DataTable doesn't select "selected all" checkbox 49 | - Default pagination control works correctly with empty DataTable 50 | 51 | ## [0.4.0] - 2016-12-27 52 | ### Added 53 | - Support for optional extra row inside `` via `::extra-header-row-component` 54 | - Support for optional `` via `::footer-component` 55 | - Ability to inject arbitrary classes into rendered `` and `` elements 56 | - Bumped [re-frame](https://github.com/Day8/re-frame) version to 0.9.1 57 | 58 | ## [0.3.0] - 2016-12-21 59 | ### Added 60 | - Selection of rows (including a subscription to selected elements) 61 | 62 | ### Changed 63 | - Changed all datatable-related keywords to namespace-qualified keywords 64 | 65 | ## [0.2.0] - 2016-12-19 66 | ### Added 67 | - Switched to [re-frame](https://github.com/Day8/re-frame) 0.9.0 68 | - `::render-fn` key that allows to specify custom rendering function 69 | - Styling of `` element after sorting 70 | 71 | ### Removed 72 | - `::th-classes` key from `columns-def` vector elements. Styling should happen outside of component 73 | 74 | ## 0.1.0 - 2016-12-15 75 | ### Added 76 | - Initial DataTable implmenetation 77 | - sorting 78 | - pagination 79 | - Styling of `` element 80 | -------------------------------------------------------------------------------- /src/re_frame_datatable/views.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable.views 2 | (:require [re-frame.core :as re-frame] 3 | [re-frame-datatable.core :as dt])) 4 | 5 | 6 | 7 | (defn default-pagination-controls [db-id data-sub] 8 | (let [pagination-state (re-frame/subscribe [::dt/pagination-state db-id data-sub])] 9 | (fn [] 10 | (let [{:keys [::dt/cur-page ::dt/pages]} @pagination-state 11 | total-pages (if (pos? (count pages)) (count pages) 1)] 12 | [:div.re-frame-datatable.page-selector 13 | {:style {:display "inline-block"}} 14 | (let [prev-enabled? (pos? cur-page)] 15 | [:span 16 | {:on-click 17 | #(when prev-enabled? 18 | (re-frame/dispatch 19 | [::dt/select-prev-page 20 | db-id @pagination-state])) 21 | :style {:cursor (when prev-enabled? "pointer") 22 | :color (when-not prev-enabled? "rgba(40,40,40,.3)")}} 23 | (str \u25C4 " PREVIOUS ")]) 24 | 25 | [:select 26 | {:value cur-page 27 | :on-change #(re-frame/dispatch 28 | [::dt/select-page 29 | db-id @pagination-state 30 | (js/parseInt (-> % .-target .-value))])} 31 | (doall 32 | (for [page-index (range total-pages)] 33 | ^{:key page-index} 34 | [:option 35 | {:value page-index} 36 | (str "Page " (inc page-index) " of " total-pages)]))] 37 | 38 | (let [next-enabled? (< cur-page (dec total-pages))] 39 | [:span 40 | {:style {:cursor (when next-enabled? "pointer") 41 | :color (when-not next-enabled? "rgba(40,40,40,.3)")} 42 | :on-click #(when next-enabled? 43 | (re-frame/dispatch 44 | [::dt/select-next-page 45 | db-id @pagination-state]))} 46 | (str " NEXT " \u25BA)])])))) 47 | 48 | 49 | 50 | (defn per-page-selector [db-id data-sub] 51 | (let [pagination-state (re-frame/subscribe [::dt/pagination-state db-id data-sub]) 52 | per-page-values [5 10 25 50 100]] 53 | 54 | (fn [] 55 | (let [{:keys [::dt/per-page]} @pagination-state] 56 | [:div.re-frame-datatable.per-page-selector 57 | {:style {:display "inline-block"}} 58 | [:span "Page Size: "] 59 | [:select 60 | {:value (or per-page dt/default-per-page) 61 | :on-change #(re-frame/dispatch 62 | [::dt/set-per-page-value 63 | db-id @pagination-state 64 | (js/parseInt (-> % .-target .-value))])} 65 | (doall 66 | (for [per-page-option (->> per-page-values 67 | (cons per-page) 68 | (filter (complement nil?)) 69 | (set) 70 | (sort))] 71 | ^{:key per-page-option} 72 | [:option 73 | {:value per-page-option} 74 | per-page-option]))]])))) 75 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/cell_rendering.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.cell-rendering 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | [cljs.repl :as r] 6 | 7 | [re-frame-datatable-docs.subs :as subs] 8 | [re-frame-datatable.core :as dt] 9 | [re-frame-datatable.views :as dt-views])) 10 | 11 | 12 | 13 | (defn cell-rendering [] 14 | [:div 15 | [:div 16 | "Each entry in " [:code.inline-code "columns-def"] " vector supports " [:code.inline-code "::render-fn"] 17 | " option that allows to specify function that defines Reagent component which should be used for rendering. This function should have the following signature: " 18 | [:pre 19 | [:code {:class "clojure"} 20 | "(defn custom-formatter [value & [item]])"]] 21 | [:ul 22 | [:li [:code.inline-code "value"] " - actual value of column property in this row"] 23 | [:li [:code.inline-code "item"] " - actual object in this row (can be used to pass arbitrary key-value pairs to cell rendering function)"]]] 24 | 25 | [:h5.ui.header "Basic custom rendering"] 26 | [components/tabs-wrapper 27 | :cell-rendering-basic 28 | [::subs/cell-rendering-data] 29 | [{::dt/column-key [:name] 30 | ::dt/column-label "Name"} 31 | {::dt/column-key [:artist] 32 | ::dt/column-label "Artist" 33 | ::dt/render-fn formatters/artist-formatter} 34 | {::dt/column-key [:duration] 35 | ::dt/column-label "Duration" 36 | ::dt/render-fn formatters/duration-formatter} 37 | {::dt/column-key [:album] 38 | ::dt/column-label "Album" 39 | ::dt/render-fn formatters/album-formatter} 40 | {::dt/column-key [:stats :rating] 41 | ::dt/column-label "Rating" 42 | ::dt/render-fn formatters/rating-formatter}] 43 | {::dt/table-classes ["ui" "very" "basic" "collapsing" "celled" "table"]} 44 | [{:data-tab "formatters-source" 45 | :label "Formatters Source" 46 | :component (fn [] 47 | [formatters/formatted-function-def 48 | (with-out-str (r/source formatters/artist-formatter)) 49 | (with-out-str (r/source formatters/duration-formatter)) 50 | (with-out-str (r/source formatters/album-formatter)) 51 | (with-out-str (r/source formatters/rating-formatter))])}]]]) 52 | 53 | 54 | 55 | (defn using-item-argument [] 56 | [:div 57 | [components/tabs-wrapper 58 | :cell-rendering-advanced 59 | [::subs/cell-rendering-data] 60 | [{::dt/column-key [:name] 61 | ::dt/column-label "Song" 62 | ::dt/render-fn formatters/song-digest-formatter} 63 | {::dt/column-key [:artist] 64 | ::dt/column-label "Artist" 65 | ::dt/render-fn formatters/artist-formatter}] 66 | {::dt/table-classes ["ui" "very" "basic" "collapsing" "celled" "table"]} 67 | [{:data-tab "formatters-source" 68 | :label "Formatters Source" 69 | :component (fn [] 70 | [formatters/formatted-function-def 71 | (with-out-str (r/source formatters/song-digest-formatter)) 72 | (with-out-str (r/source formatters/artist-formatter))])}]]]) 73 | -------------------------------------------------------------------------------- /re-frame-datatable.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # re-frame-datatable 2 | 3 | A UI component for [re-frame](https://github.com/Day8/re-frame). 4 | Uses existing subscription to data source in re-frame's `app-db` and declarative definition of how to render it as a table. 5 | Supports sorting, pagination and some basic CSS manipulations for generated table. 6 | 7 | ## Quick links 8 | 9 | * [Documentation Website](https://kishanov.github.io/re-frame-datatable/) - re-frame app that shows how to use differt DataTable options 10 | * [Complete example app](https://kishanov.github.io/re-frame-datatable-example/) - GMail-like interface which uses most of the features of DataTable component 11 | 12 | 13 | ## Usage 14 | 15 | Leiningen 16 | 17 | [![Leiningen version](https://clojars.org/re-frame-datatable/latest-version.svg)](http://clojars.org/re-frame-datatable) 18 | 19 | In your application add the following dependency to the file that defines views in your re-frame application: 20 | 21 | ```clojure 22 | (ns re-frame-datatable-docs.views 23 | (:require [re-frame-datatable.core :as dt] 24 | [your.app.subs :as subs] ; Namespace in which re-frame subscriptions are defined 25 | ;...)) 26 | ``` 27 | 28 | Here is a sample [Reagent](https://github.com/reagent-project/reagent) component which defines datatable (assuming that `subs` namespace constains `::songs-list` subscription which returns data in the following format: `[{:index 1 :name "Mister Sandman" :duration 136} ...]`) 29 | 30 | 31 | ```clojure 32 | (defn sneak-peek-for-readme [] 33 | [dt/datatable 34 | :songs 35 | [::subs/songs-list] 36 | [{::dt/column-key [:index] 37 | ::dt/sorting {::dt/enabled? true} 38 | ::dt/column-label "#"} 39 | {::dt/column-key [:name] 40 | ::dt/column-label "Name"} 41 | {::dt/column-key [:duration] 42 | ::dt/column-label "Duration" 43 | ::dt/sorting {::dt/enabled? true} 44 | ::dt/render-fn (fn [val] 45 | [:span 46 | (let [m (quot val 60) 47 | s (mod val 60)] 48 | (if (zero? m) 49 | s 50 | (str m ":" (when (< s 10) 0) s)))])}] 51 | {::dt/pagination {::dt/enabled? true 52 | ::dt/per-page 5} 53 | ::dt/table-classes ["ui" "table" "celled"]}]) 54 | ``` 55 | 56 | `dt/datatable` component accepts the following arguments: 57 | 58 | * `datatable-key` - a keyword, that will be used in re-frame's `app-db` to store datatable state 59 | * `subscription-vec` - a vector, which will be used by datatable to render data via `(re-frame/subscribe subscription-vec)` 60 | * `columns-def` - a vector of maps, that defines how each column of datatable should look like 61 | * `options` - optional map of additional options 62 | 63 | For the complete documenation and live examples visit [Documentation website](https://kishanov.github.io/re-frame-datatable/). 64 | 65 | ## How it works 66 | 67 | `re-frame-datatable` expects a re-frame subscription, that returns a collection of maps. It will render an HTML table (using `
` tag) based on the following rules 68 | 69 | * Table will have `n` columns (based on the amount of elements in `columns-def` vector 70 | * The columns will be rendered in the same order, as in `columns-def` vector 71 | * For each item in subscription, datatable will render a row, each cell of which will correspond to the value of a particular property in the item's map (datatable uses `get-in item column-key` based on `columns-def`) 72 | * For every action, that changes the state of datatable (sorting, pagination) it will re-calculate what should be rendered and apply it on top of provided data subscription 73 | 74 | ## License 75 | 76 | Copyright © 2016 Kirill Ishanov 77 | 78 | Distributed under the Eclipse Public License either version 1.0 or (at 79 | your option) any later version. 80 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/sorting.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.sorting 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | [re-frame-datatable-docs.subs :as subs] 6 | [re-frame-datatable.core :as dt] 7 | [cljs.repl :as r] 8 | [re-frame-datatable.views :as dt-views])) 9 | 10 | 11 | 12 | (defn enable-sorting [] 13 | [:div 14 | [:div 15 | "Sorting is enabled on per-column basis. To make column sortable just add " 16 | [:code.inline-code "::sorting"] " with the value " 17 | [:code.inline-code "{::enabled? true}"] " to particular column definition in " 18 | [:code.inline-code "columns-def"] " vector. " 19 | "In the example below, index and play_count columns are made sortable."] 20 | 21 | [components/info-message 22 | [:div " To sort table, click on a column header (for the column on which sorting was enabled)"]] 23 | 24 | [components/warning-message 25 | [:div 26 | " When the table was sorted by particular column, the
element of sorted column will have 2 HTML classes assigned: " 27 | [:code.inline-code "sorted-by"] " and either " [:code.inline-code "asc"] " or " [:code.inline-code "desc"] 28 | ". This allows to apply CSS styling to this column to emphasize that the table was sorted by it. DataTable doesn't render additional visual clues to show emphasize it."]] 29 | 30 | 31 | [components/tabs-wrapper 32 | :sorting 33 | [::subs/basic-definition-data] 34 | [{::dt/column-key [:index] 35 | ::dt/column-label "#" 36 | ::dt/sorting {::dt/enabled? true}} 37 | {::dt/column-key [:name] 38 | ::dt/column-label "Name"} 39 | {::dt/column-key [:stats :play_count] 40 | ::dt/column-label "Play count" 41 | ::dt/sorting {::dt/enabled? true}}] 42 | {::dt/table-classes ["ui" "table"]} 43 | [{:data-tab "css-example" 44 | :label "CSS" 45 | :component (fn [] 46 | [:pre 47 | [:code {:class "css"} 48 | " 49 | table.re-frame-datatable > thead th.sorted-by:after { 50 | display: inline-block; 51 | } 52 | 53 | table.re-frame-datatable > thead th.sorted-by.desc:after { 54 | content: '\f0d7'; 55 | } 56 | 57 | table.re-frame-datatable > thead th.asc:after { 58 | content: '\f0d8'; 59 | } 60 | 61 | table.re-frame-datatable > thead th:after { 62 | display: none; 63 | font-family: Icons; 64 | margin-left: .5em; 65 | }"]])}]]]) 66 | 67 | 68 | 69 | (defn custom-sorting-fn [] 70 | [:div 71 | [:div 72 | "To define custom comparator for sorting function, provide " 73 | [:code.inline-code "::comp-fn"] " with the function that accepts a function that will be used internally as a third argument to " 74 | [:a {:href "https://clojuredocs.org/clojure.core/sort-by"} "sort-by"] " (DataTable uses it internally in to sort table data). " 75 | "Note that DataTable requires a function, so things like keywords should be wrapped into function with proper signature."] 76 | 77 | [components/tabs-wrapper 78 | :sorting 79 | [::subs/basic-definition-data] 80 | [{::dt/column-key [:index] 81 | ::dt/column-label "#"} 82 | {::dt/column-key [:name] 83 | ::dt/column-label "Name"} 84 | {::dt/column-key [:stats] 85 | ::dt/column-label "Play count" 86 | ::dt/sorting {::dt/enabled? true 87 | ::dt/comp-fn formatters/sort-play-count-comp}}] 88 | {::dt/table-classes ["ui" "table"]} 89 | [{:data-tab "comparator-fn-source" 90 | :label "Comparator Fn Source" 91 | :component (fn [] 92 | [formatters/formatted-function-def 93 | (with-out-str (r/source formatters/sort-play-count-comp))])}]]]) 94 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/rows_selection.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.rows-selection 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | [cljs.repl :as r] 6 | 7 | [re-frame-datatable-docs.subs :as subs] 8 | [re-frame-datatable.core :as dt] 9 | [re-frame-datatable.views :as dt-views] 10 | [re-frame.core :as re-frame])) 11 | 12 | 13 | (defn enable-rows-selection [] 14 | [:div 15 | [:div 16 | [:p 17 | "To enable selection, pass " [:code.inline-code "::selection"] " option with value " [:code.inline-code "{::enabled? true}"] "."] 18 | 19 | [:div 20 | "To access selected items, DataTable provides subsciprtion " [:code.inline-code "::selected-items"] ", which accepts 2 arguments" 21 | [:ul 22 | [:li [:code.inline-code "datatable-id"] " - same keyword, that was used in DataTable definition"] 23 | [:li [:code.inline-code "data-sub"] " - same subscription vector, that was used in DataTable definition"]]]] 24 | 25 | [components/tabs-wrapper 26 | :rows-selection-basic 27 | [::subs/basic-definition-data] 28 | [{::dt/column-key [:name] 29 | ::dt/column-label "Name"} 30 | {::dt/column-key [:stats :play_count] 31 | ::dt/column-label "Play count"}] 32 | {::dt/table-classes ["ui" "very" "basic" "collapsing" "celled" "table"] 33 | ::dt/selection {::dt/enabled? true}} 34 | [{:data-tab "selected-items-preview" 35 | :label "Selected Items Source" 36 | :component (fn [] 37 | [:pre 38 | [:code {:class "clojure"} 39 | (with-out-str (r/source table-views/selected-rows-preview))]])}] 40 | (fn [dt-def] 41 | [:div.ui.two.column.divided.grid 42 | [:div.column 43 | [:h5.ui.header "Table"] 44 | dt-def] 45 | 46 | [:div.column 47 | [:h5.ui.header "Selected items"] 48 | [formatters/formatted-code 49 | @(re-frame/subscribe [::dt/selected-items :rows-selection-basic [::subs/basic-definition-data]])]]])]]) 50 | 51 | 52 | 53 | (defn selection-and-other-options [] 54 | [:div 55 | [:div 56 | [:p 57 | "Row selection also works with pagination and sorting. If pagination is enabled, \"select/unselect all\" will select/unselect all elements on all pages"] 58 | 59 | [components/warning-message 60 | [:div 61 | [:p 62 | "If you plan to modify the content of DataTable based on selection (for example, select n elements and delete them via separate handler), 63 | you also need to dispatch an vent that will unselect all items in DataTable. 64 | If you'll not do that, after deteletion some other elements will remain selected based on internal DataTable indexing mechanism."] 65 | [:p 66 | "To unselect all selected rows, dispatch the following event from the handler which modifies the content of DataTable."] 67 | [:pre 68 | [:code {:class "clojure"} 69 | "[::dt/unselect-all-rows datatable-id]"]]]]] 70 | 71 | [components/tabs-wrapper 72 | :rows-selection-pagination-sorting 73 | [::subs/pagination-data] 74 | [{::dt/column-key [:index] 75 | ::dt/column-label "#" 76 | ::dt/sorting {::dt/enabled? true}} 77 | {::dt/column-key [:name] 78 | ::dt/column-label "Name" 79 | ::dt/sorting {::dt/enabled? true}} 80 | {::dt/column-key [:stats :play_count] 81 | ::dt/column-label "Play count" 82 | ::dt/sorting {::dt/enabled? true}}] 83 | {::dt/table-classes ["ui" "very" "basic" "collapsing" "celled" "table"] 84 | ::dt/selection {::dt/enabled? true} 85 | ::dt/pagination {::dt/enabled? true 86 | ::dt/per-page 5}} 87 | nil 88 | (fn [dt-def] 89 | [:div.ui.two.column.divided.grid 90 | [:div.column 91 | [:h5.ui.header "Table"] 92 | [dt-views/default-pagination-controls :rows-selection-pagination-sorting [::subs/pagination-data]] 93 | dt-def] 94 | 95 | [:div.column 96 | [:h5.ui.header "Selected items"] 97 | [formatters/formatted-code 98 | @(re-frame/subscribe [::dt/selected-items :rows-selection-pagination-sorting [::subs/pagination-data]])]]])]]) 99 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/styling.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.styling 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | 6 | [re-frame-datatable-docs.subs :as subs] 7 | [re-frame-datatable.core :as dt] 8 | 9 | [cljs.repl :as r] 10 | [re-frame-datatable.views :as dt-views])) 11 | 12 | 13 | (defn css-options [] 14 | [:div 15 | [:div 16 | "HTML table that will be generated will have a standard structure:" 17 | [:pre 18 | [:code {:class "html"} 19 | " 20 | 21 | 22 | 23 | 24 | 25 | ... 26 | 27 |
...
...
28 | "]] 29 | 30 | "To avoid drilling too many \"holes\" in datatable most of the styling should be done in CSS via CSS selectors based on this structure. 31 | To enable that, datatable allows to provide a vector of CSS classes that should be applied to " 32 | [:code.inline-code ""] " HTML tag, and the rest can be done via CSS selectors."] 33 | [:div "To provide CSS classes that should be applied to
tag, use " 34 | [:code.inline-code "::table-classes"] " key in options as shown in example. Further styling can be provided via CSS selectors:" 35 | [:pre 36 | [:code {:class "css"} 37 | ".ui.table > thead > th { color: red; } "]]] 38 | 39 | [components/tabs-wrapper 40 | :css-options 41 | [::subs/basic-definition-data] 42 | [{::dt/column-key [:index]} 43 | {::dt/column-key [:name] 44 | ::dt/column-label "Name"} 45 | {::dt/column-key [:stats :play_count] 46 | ::dt/column-label formatters/play-count-th}] 47 | {::dt/table-classes ["ui" "celled" "stripped" "table"]} 48 | [{:data-tab "th-fourammter-source" 49 | :label "Play Count Formatter Source" 50 | :component (fn [] 51 | [formatters/formatted-function-def 52 | (with-out-str (r/source formatters/play-count-th))])}]]]) 53 | 54 | 55 | 56 | 57 | (defn styling-cells [] 58 | [:div 59 | [:div 60 | [:p 61 | "Some CSS frameworks can render individual cells differently if specific classes are assigned to the " [:code.inline-code " class as 'warning'" 45 | [song] 46 | [(when-not (pos? (get-in song [:stats :play_count])) 47 | "warning")]) 48 | 49 | 50 | (defn selected-rows-preview [] 51 | [:pre 52 | [:code 53 | @(re-frame/subscribe [::re-frame-datatable.core/selected-items 54 | :rows-basic-definition 55 | [::re-frame-datatable-docs.subs/basic-definition-data]])]]) 56 | 57 | 58 | (defn empty-tbody-formatter [] 59 | [:em 60 | "DataTable receieved an empty dataset via subscription"]) 61 | 62 | 63 | 64 | (defn basic-pagination [db-id data-sub] 65 | (let [pagination-state (re-frame/subscribe [::re-frame-datatable.core/pagination-state db-id data-sub])] 66 | (fn [] 67 | (let [{:keys [::re-frame-datatable.core/cur-page ::re-frame-datatable.core/pages]} @pagination-state 68 | total-pages (count pages) 69 | next-enabled? (< cur-page (dec total-pages)) 70 | prev-enabled? (pos? cur-page)] 71 | 72 | [:div.ui.pagination.menu 73 | [:a.item 74 | {:on-click #(when prev-enabled? 75 | (re-frame/dispatch [::re-frame-datatable.core/select-prev-page db-id @pagination-state])) 76 | :class (when-not prev-enabled? "disabled")} 77 | [:i.left.chevron.icon]] 78 | 79 | (for [i (range total-pages)] 80 | ^{:key i} 81 | [:a.item 82 | {:class (when (= i cur-page) "active") 83 | :on-click #(re-frame/dispatch [::re-frame-datatable.core/select-page db-id @pagination-state i])} 84 | (inc i)]) 85 | 86 | [:a.item 87 | {:on-click #(when next-enabled? 88 | (re-frame/dispatch [::re-frame-datatable.core/select-next-page db-id @pagination-state])) 89 | :class (when-not next-enabled? "disabled")} 90 | [:i.right.chevron.icon]]])))) 91 | 92 | 93 | 94 | (defn gmail-like-pagination [db-id data-sub] 95 | (let [pagination-state (re-frame/subscribe [::re-frame-datatable.core/pagination-state db-id data-sub])] 96 | (fn [] 97 | (let [{:keys [::re-frame-datatable.core/cur-page ::re-frame-datatable.core/pages]} @pagination-state 98 | total-pages (count pages) 99 | next-enabled? (< cur-page (dec total-pages)) 100 | prev-enabled? (pos? cur-page)] 101 | 102 | [:div 103 | [:div {:style {:display "inline-block" 104 | :margin-right ".5em"}} 105 | [:strong 106 | (str (inc (first (get pages cur-page))) "-" (inc (second (get pages cur-page))))] 107 | [:span " of "] 108 | [:strong (inc (second (last pages)))]] 109 | 110 | [:div.ui.pagination.mini.menu 111 | [:a.item 112 | {:on-click #(when prev-enabled? 113 | (re-frame/dispatch [::re-frame-datatable.core/select-prev-page db-id @pagination-state])) 114 | :class (when-not prev-enabled? "disabled")} 115 | [:i.left.chevron.icon]] 116 | 117 | [:a.item 118 | {:on-click #(when next-enabled? 119 | (re-frame/dispatch [::re-frame-datatable.core/select-next-page db-id @pagination-state])) 120 | :class (when-not next-enabled? "disabled")} 121 | [:i.right.chevron.icon]]]])))) 122 | 123 | 124 | 125 | (defn per-page-radio-selector [dt-id data-sub] 126 | (let [pagination-state (re-frame/subscribe [::re-frame-datatable.core/pagination-state dt-id data-sub]) 127 | per-page-values [5 10 15]] 128 | 129 | (fn [] 130 | (let [{:keys [::re-frame-datatable.core/per-page]} @pagination-state] 131 | [:div.ui.form 132 | [:div.inilne.fields 133 | (doall 134 | (for [per-page-option per-page-values] 135 | ^{:key per-page-option} 136 | [:div.field 137 | [:input 138 | {:type :radio 139 | :on-change #(re-frame/dispatch [::re-frame-datatable.core/set-per-page-value 140 | dt-id data-sub 141 | (js/parseInt (-> % .-target .-name))]) 142 | :name per-page-option 143 | :class "hidden" 144 | :checked (= per-page per-page-option)}] 145 | [:label per-page-option]]))]])))) 146 | 147 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/additional_structure.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.additional-structure 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | [cljs.repl :as r] 6 | 7 | [re-frame-datatable-docs.subs :as subs] 8 | [re-frame-datatable.core :as dt] 9 | [re-frame-datatable.views :as dt-views] 10 | 11 | [re-frame.core :as re-frame])) 12 | 13 | 14 | 15 | (defn disable-header [] 16 | [:div 17 | [:p 18 | "It is also possible to configure DataTable not to render header at all - " 19 | "just pass " [:code.inline-code "::header-enabled?"] " option set to false."] 20 | 21 | [components/warning-message 22 | [:div 23 | "Notice that " [:code.inline-code "::header-enabled?"] " has precedence over any of the specified " [:code.inline-code "::column-label"] 24 | "In the example below, header is not rendered even after " [:code.inline-code "::column-label"] " was specified for the second column."]] 25 | 26 | [components/tabs-wrapper 27 | :basic-definition-no-header 28 | [::subs/basic-definition-data] 29 | [{::dt/column-key [:index]} 30 | {::dt/column-key [:name] 31 | ::dt/column-label "Name"}] 32 | {::dt/header-enabled? false}]]) 33 | 34 | 35 | (defn empty-dataset [] 36 | [:div 37 | [:div 38 | [:p 39 | "In case if given subscription returns an empty datasert, DataTable will render single row with all " [:code.inline-code ""] 78 | " (it will be rendered on top of header row, rendered from " [:code.inline-code "columns-def"] " vector). " 79 | "To render it, use " [:code.inline-code "::extra-header-row-component"] " option key with the valid Reagent component passed as a value." 80 | 81 | [components/warning-message 82 | [:div 83 | " Notice that DataTable will not validate that passed component is in fact valid, it just checks if it's a function." 84 | "Internally, it will be rendered as " [:code.inline-code "[(::extra-header-row-component options)]"]]]] 85 | 86 | [components/tabs-wrapper 87 | :extra-header-row 88 | [::subs/cell-rendering-data] 89 | [{::dt/column-key [:name] 90 | ::dt/column-label "Name"} 91 | {::dt/column-key [:artist] 92 | ::dt/column-label "Artist"} 93 | {::dt/column-key [:album :name] 94 | ::dt/column-label "Album"} 95 | {::dt/column-key [:album :year] 96 | ::dt/column-label "Year"}] 97 | {::dt/table-classes ["ui" "celled" "table"] 98 | ::dt/extra-header-row-component table-views/aggregation-row} 99 | [{:data-tab "aggregation-row-source" 100 | :label "Aggregation Row Source" 101 | :component (fn [] 102 | [formatters/formatted-function-def 103 | (with-out-str (r/source table-views/aggregation-row))])}]]]) 104 | 105 | 106 | (defn footer [] 107 | [:div 108 | [:div 109 | "DataTable can also render an arbitrary Reagent comopnent inside " [:code.inline-code ""] " HTML tag." 110 | "To render it, use " [:code.inline-code "::footer-component"] " option key with the valid Reagent component passed as a value." 111 | 112 | [components/info-message 113 | [:div 114 | "Notice that in the example below " [:code.inline-code "total-play-count-footer"] " uses additional subscription to get total play count. " 115 | "DataTable doesn't maintain any intermediate state of given data, so in order to access it - use the same subscription as passed to DataTable (or the one which is build on top of it)"]]] 116 | 117 | [components/tabs-wrapper 118 | :footer-component 119 | [::subs/basic-definition-data] 120 | [{::dt/column-key [:index] 121 | ::dt/column-label "Index"} 122 | {::dt/column-key [:name] 123 | ::dt/column-label "Name"} 124 | {::dt/column-key [:stats :play_count] 125 | ::dt/column-label "Play count"}] 126 | {::dt/table-classes ["ui" "celled" "table"] 127 | ::dt/footer-component table-views/total-play-count-footer} 128 | [{:data-tab "footer-source" 129 | :label "Footer Source" 130 | :component (fn [] 131 | [formatters/formatted-function-def 132 | (with-out-str (r/source table-views/total-play-count-footer))])}]]]) 133 | -------------------------------------------------------------------------------- /docs/re-frame-datatable-docs.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/views.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.views 2 | (:require [re-frame.core :as re-frame] 3 | [reagent.core :as reagent] 4 | [cljs.repl :as r] 5 | 6 | [re-frame-datatable.core :as dt] 7 | [re-frame-datatable.views :as dt-views] 8 | 9 | [re-frame-datatable-docs.subs :as subs] 10 | [re-frame-datatable-docs.events :as events] 11 | [re-frame-datatable-docs.formatters :as formatters] 12 | [re-frame-datatable-docs.table-views :as table-views] 13 | [re-frame-datatable-docs.components :as components] 14 | 15 | [re-frame-datatable-docs.sections.usage :as usage-sections] 16 | [re-frame-datatable-docs.sections.styling :as styling-sections] 17 | [re-frame-datatable-docs.sections.additional-structure :as structure-sections] 18 | [re-frame-datatable-docs.sections.sorting :as sorting-sections] 19 | [re-frame-datatable-docs.sections.pagination :as pagination-sections] 20 | [re-frame-datatable-docs.sections.cell-rendering :as cell-rendering-sections] 21 | [re-frame-datatable-docs.sections.rows-selection :as rows-selection-sections])) 22 | 23 | 24 | 25 | (defn sneak-peek-for-readme [] 26 | [dt/datatable 27 | :songs 28 | [::subs/songs-list] 29 | [{::dt/column-key [:index] 30 | ::dt/sorting {::dt/enabled? true}} 31 | {::dt/column-key [:name] 32 | ::dt/column-label "Name"} 33 | {::dt/column-key [:duration] 34 | ::dt/column-label "Duration" 35 | ::dt/sorting {::dt/enabled? true} 36 | ::dt/render-fn (fn [val] 37 | [:span 38 | (let [m (quot val 60) 39 | s (mod val 60)] 40 | (if (zero? m) 41 | s 42 | (str m ":" (when (< s 10) 0) s)))])}] 43 | {::dt/pagination {::dt/enabled? true 44 | ::dt/per-page 5} 45 | ::dt/table-classes ["ui" "table" "celled"]}]) 46 | 47 | 48 | 49 | 50 | (defn main-header [] 51 | [:div.ui.grid 52 | [:div.twelve.wide.column 53 | [:h1.ui.header 54 | {:style {:margin-bottom "2em" 55 | :margin-top "1em"}} 56 | "re-frame-datatable" 57 | [:div.sub.header "DataTable component for re-frame 0.8.0+"]]] 58 | [:div.four.wide.column 59 | [:div 60 | [:div.ui.right.floated.menu 61 | {:style {:margin-top "2em"}} 62 | [:a.item 63 | {:href "https://github.com/kishanov/re-frame-datatable" 64 | :data-tooltip "View project on GitHub" 65 | :data-position "bottom center"} 66 | [:i.github.icon]]]]]]) 67 | 68 | 69 | 70 | 71 | (defn main-panel [] 72 | (let [active-section (re-frame/subscribe [::subs/active-section]) 73 | doc-structure (list 74 | {:group "Usage" 75 | :sections [["definition" "Definition" usage-sections/definition] 76 | ["basic-usage" "Basic Usage" usage-sections/basic-usage]]} 77 | 78 | {:group "Styling" 79 | :sections [["css-options" "CSS Options" styling-sections/css-options] 80 | ["css-styling-cells" "Styling Individual Cells" styling-sections/styling-cells] 81 | ["css-styling-rows" "Styling Rows" styling-sections/styling-rows]]} 82 | 83 | {:group "Structure" 84 | :sections [["no-header" "Remove Header" structure-sections/disable-header] 85 | ["extra-header-row" "Extra Header Row" structure-sections/extra-header] 86 | ["structure-footer" "Footer" structure-sections/footer] 87 | ["empty-dataset" "Empty Dataset" structure-sections/empty-dataset]]} 88 | 89 | {:group "Sorting" 90 | :sections [["sorting-basic" "Enabling Sorting" sorting-sections/enable-sorting] 91 | ["sorting-custom-comp-fn" "Custom Comparator" sorting-sections/custom-sorting-fn]]} 92 | 93 | {:group "Pagination" 94 | :sections [["enabling-pagination" "Enabling Pagination" pagination-sections/enabling-pagination] 95 | ["per-page-selector" "Setting Page Size" pagination-sections/per-page-selector] 96 | ["custom-pagination-controls" "Custom Pagination Controls" pagination-sections/custom-pagination-controls]]} 97 | 98 | {:group "Custom Cell Rendering" 99 | :sections [["custom-cell-rendering-formatter" "Custom Formatters" cell-rendering-sections/cell-rendering] 100 | ["custom-cell-rendering-extra-arg" "Row-aware Formatters" cell-rendering-sections/using-item-argument]]} 101 | 102 | {:group "Selection (rows)" 103 | :sections [["enabling-rows-selection" "Enabling Selection" rows-selection-sections/enable-rows-selection] 104 | ["rows-selection-and-other-options" "Selection and Other Options" rows-selection-sections/selection-and-other-options]]})] 105 | 106 | 107 | (reagent/create-class 108 | {:component-function 109 | (fn [] 110 | [:div.ui.main.text.container 111 | [:div.ui.vertical.segment 112 | [:div.ui.dividing.right.rail 113 | [:div.ui.sticky 114 | {:style {:min-height "260px"}} 115 | [:div.ui.vertical.text.menu 116 | {:style {:margin-top "1em" :margin-bottom "2em"}} 117 | 118 | (doall 119 | (for [[i {:keys [group sections]}] (map-indexed vector doc-structure)] 120 | ^{:key i} 121 | [:div 122 | {:style {:margin-bottom "1em"}} 123 | [:h4.ui.dividing.header group] 124 | 125 | (doall 126 | (for [[dom-id label] sections] 127 | ^{:key dom-id} 128 | [:a.item 129 | {:href (str \# dom-id) 130 | :class (when (= @active-section dom-id) "active") 131 | :on-click #(re-frame/dispatch [::events/set-active-section dom-id])} 132 | label]))]))]]] 133 | 134 | [:div#context 135 | [main-header] 136 | 137 | (doall 138 | (for [[i {:keys [group sections]}] (map-indexed vector doc-structure)] 139 | ^{:key i} 140 | [:div 141 | [:h2.ui.dividing.header group] 142 | 143 | (doall 144 | (for [[dom-id label component] sections] 145 | ^{:key dom-id} 146 | [:div.ui.section 147 | [:a {:id dom-id :class "anchor"}] 148 | [:h3.ui.dividing.header label] 149 | [component]]))]))]]]) 150 | 151 | 152 | :component-did-mount 153 | (fn [] 154 | (.ready (js/$ js/document) 155 | (fn [] 156 | (.tab (js/$ ".menu .item")) 157 | (.sticky (js/$ ".ui.sticky") (clj->js {:context "#context"})))))}))) 158 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/sections/pagination.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.sections.pagination 2 | (:require [re-frame-datatable-docs.components :as components] 3 | [re-frame-datatable-docs.formatters :as formatters] 4 | [re-frame-datatable-docs.table-views :as table-views] 5 | [cljs.repl :as r] 6 | 7 | [re-frame-datatable-docs.subs :as subs] 8 | [re-frame-datatable.core :as dt] 9 | [re-frame-datatable.views :as dt-views])) 10 | 11 | 12 | 13 | (defn enabling-pagination [] 14 | (let [dt-key :pagination-basic 15 | dt-sub [::subs/pagination-data]] 16 | 17 | [:div 18 | [:div 19 | "Pagination can be enabled via " [:code.inline-code "::pagination"] " key. There are 2 options:" 20 | [:ul.ui.list 21 | [:li [:code.inline-code "::enabled?"] " - boolean to define if pagination should be enabled"] 22 | [:li [:code.inline-code "::per-page"] " - integer to define how many elements should be shown per page (default is 10)"]]] 23 | 24 | [components/warning-message 25 | [:div 26 | [:p 27 | "DataTable's pagination controls are not rendered together with DataTable. There are 2 reasons for it:"] 28 | [:ul 29 | [:li "DataTable is not opinionated about where to put pagination controls"] 30 | [:li "DataTable is not opinionated how pagination controls will look like"]]] 31 | "Important"] 32 | 33 | [:p 34 | "DataTable ships with additional component " [:code.inline-code "default-pagination-controls"] 35 | ", that accepts 2 arguments: " [:code.inline-code "datatable-key"] " and " [:code.inline-code "data-sub"] 36 | " which should be the same as passed to " [:code.inline-code "datatable"] " component itself"] 37 | 38 | [components/tabs-wrapper 39 | dt-key 40 | dt-sub 41 | [{::dt/column-key [:index] 42 | ::dt/column-label "#"} 43 | {::dt/column-key [:name] 44 | ::dt/column-label "Name"} 45 | {::dt/column-key [:stats :play_count] 46 | ::dt/column-label "Play count"}] 47 | {::dt/pagination {::dt/enabled? true 48 | ::dt/per-page 5} 49 | ::dt/table-classes ["ui" "table"]} 50 | [{:data-tab "default-pagination-controls-usage" 51 | :label "Default Pagination Controls Usage" 52 | :component (fn [] 53 | [:pre 54 | [:code {:class "clojure"} 55 | (str 56 | "[dt-views/default-pagination-controls " 57 | dt-key 58 | \space 59 | dt-sub)]])}] 60 | (fn [dt-def] 61 | [:div.ui.grid 62 | [:div.row 63 | [:div.right.aligned.column 64 | [dt-views/default-pagination-controls dt-key dt-sub]]] 65 | [:div.row 66 | [:div.column 67 | dt-def]] 68 | [:div.row 69 | [:div.right.aligned.column 70 | [dt-views/default-pagination-controls dt-key dt-sub]]]])]])) 71 | 72 | 73 | 74 | (defn custom-pagination-controls [] 75 | (let [dt-key :pagination-custom-controls 76 | dt-sub [::subs/pagination-data]] 77 | 78 | [:div 79 | [:div 80 | [:p 81 | "It is possible to build your own custom pagiation controls. DataTable exposes single subscription - " [:code.inline-code "::pagination-state"] 82 | " - that accepts 2 arguments: " [:code.inline-code "datatable-key"] " and " [:code.inline-code "data-sub"] 83 | " That subscription returns a map with following keys:"] 84 | [:ul 85 | [:li [:code.inline-code "::cur-page"] " - an integer, which represents current page (0-based indexing)"] 86 | [:li [:code.inline-code "::per-page"] " - an integer, which represents maximum count of elements per each page"] 87 | [:li [:code.inline-code "::pages"] " - a vector of tuples of length " [:code.inline-code "::per-page"] 88 | ", each tuple has 2 numbers for each page: " [:code.inline-code "[first-element-index last-element-index]"] " (indexing is zero based)" 89 | "For example, " [:code.inline-code "[[0 4][5 9][10 12]"]]] 90 | [:p "See the example below to see how 2 custom pagination components are defined. It's easier to copy and modify their source, then to properly document how they work :)"]] 91 | 92 | [components/tabs-wrapper 93 | dt-key 94 | dt-sub 95 | [{::dt/column-key [:index] 96 | ::dt/column-label "#"} 97 | {::dt/column-key [:name] 98 | ::dt/column-label "Name"} 99 | {::dt/column-key [:stats :play_count] 100 | ::dt/column-label "Play count"}] 101 | {::dt/pagination {::dt/enabled? true 102 | ::dt/per-page 5} 103 | ::dt/table-classes ["ui" "table"]} 104 | [{:data-tab "pagination-controls-source" 105 | :label "Pagination Controls Source" 106 | :component (fn [] 107 | [formatters/formatted-function-def 108 | (with-out-str (r/source table-views/basic-pagination)) 109 | (with-out-str (r/source table-views/gmail-like-pagination))])}] 110 | (fn [dt-def] 111 | [:div.ui.two.column.grid 112 | [:div.column dt-def] 113 | [:div.column 114 | [:div {:style {:margin-bottom "1em"}} 115 | [:h5.ui.herader "Basic pagination controls"] 116 | [table-views/basic-pagination dt-key dt-sub]] 117 | 118 | [:div {:style {:margin-bottom "1em"}} 119 | [:h5.ui.herader "GMAIL-like pagination controls"] 120 | [table-views/gmail-like-pagination dt-key dt-sub]] 121 | 122 | [:div {:style {:margin-bottom "1em"}} 123 | [:h5.ui.herader "Radio Per Page Selector"] 124 | [table-views/per-page-radio-selector dt-key dt-sub]]]])]])) 125 | 126 | 127 | 128 | (defn per-page-selector [] 129 | [:div 130 | [:div 131 | "Per page count can be change via special event: " [:code.inline-code "::set-per-page-value"]] 132 | 133 | [components/info-message 134 | [:div 135 | [:p 136 | "Changing " [:code.inline-code "::per-page"] " value via event will also set the currnet page to 0"]]] 137 | 138 | (let [dt-key :per-page-selector-example 139 | dt-sub [::subs/pagination-data]] 140 | 141 | [components/tabs-wrapper 142 | dt-key 143 | dt-sub 144 | [{::dt/column-key [:index] 145 | ::dt/column-label "#"} 146 | {::dt/column-key [:name] 147 | ::dt/column-label "Name"} 148 | {::dt/column-key [:stats :play_count] 149 | ::dt/column-label "Play count"}] 150 | {::dt/pagination {::dt/enabled? true 151 | ::dt/per-page 5} 152 | ::dt/table-classes ["ui" "table"]} 153 | [{:data-tab "default-per-page-controls-usage" 154 | :label "Default Per Page Usage" 155 | :component (fn [] 156 | [:pre 157 | [:code {:class "clojure"} 158 | (str 159 | "[dt-views/per-page-selector " 160 | dt-key 161 | \space 162 | dt-sub)]])}] 163 | (fn [dt-def] 164 | [:div.ui.grid 165 | [:div.row 166 | [:div.right.aligned.column 167 | [dt-views/per-page-selector dt-key dt-sub] 168 | [:span {:style {:min-width "1em"}}] 169 | [dt-views/default-pagination-controls dt-key dt-sub]]] 170 | [:div.row 171 | [:div.column 172 | dt-def]]])])]) 173 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE PUBLIC 2 | LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF THE PROGRAM 3 | CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT. 4 | 5 | 1. DEFINITIONS 6 | 7 | "Contribution" means: 8 | 9 | a) in the case of the initial Contributor, the initial code and 10 | documentation distributed under this Agreement, and 11 | 12 | b) in the case of each subsequent Contributor: 13 | 14 | i) changes to the Program, and 15 | 16 | ii) additions to the Program; 17 | 18 | where such changes and/or additions to the Program originate from and are 19 | distributed by that particular Contributor. A Contribution 'originates' from 20 | a Contributor if it was added to the Program by such Contributor itself or 21 | anyone acting on such Contributor's behalf. Contributions do not include 22 | additions to the Program which: (i) are separate modules of software 23 | distributed in conjunction with the Program under their own license 24 | agreement, and (ii) are not derivative works of the Program. 25 | 26 | "Contributor" means any person or entity that distributes the Program. 27 | 28 | "Licensed Patents" mean patent claims licensable by a Contributor which are 29 | necessarily infringed by the use or sale of its Contribution alone or when 30 | combined with the Program. 31 | 32 | "Program" means the Contributions distributed in accordance with this 33 | Agreement. 34 | 35 | "Recipient" means anyone who receives the Program under this Agreement, 36 | including all Contributors. 37 | 38 | 2. GRANT OF RIGHTS 39 | 40 | a) Subject to the terms of this Agreement, each Contributor hereby grants 41 | Recipient a non-exclusive, worldwide, royalty-free copyright license to 42 | reproduce, prepare derivative works of, publicly display, publicly perform, 43 | distribute and sublicense the Contribution of such Contributor, if any, and 44 | such derivative works, in source code and object code form. 45 | 46 | b) Subject to the terms of this Agreement, each Contributor hereby grants 47 | Recipient a non-exclusive, worldwide, royalty-free patent license under 48 | Licensed Patents to make, use, sell, offer to sell, import and otherwise 49 | transfer the Contribution of such Contributor, if any, in source code and 50 | object code form. This patent license shall apply to the combination of the 51 | Contribution and the Program if, at the time the Contribution is added by the 52 | Contributor, such addition of the Contribution causes such combination to be 53 | covered by the Licensed Patents. The patent license shall not apply to any 54 | other combinations which include the Contribution. No hardware per se is 55 | licensed hereunder. 56 | 57 | c) Recipient understands that although each Contributor grants the licenses 58 | to its Contributions set forth herein, no assurances are provided by any 59 | Contributor that the Program does not infringe the patent or other 60 | intellectual property rights of any other entity. Each Contributor disclaims 61 | any liability to Recipient for claims brought by any other entity based on 62 | infringement of intellectual property rights or otherwise. As a condition to 63 | exercising the rights and licenses granted hereunder, each Recipient hereby 64 | assumes sole responsibility to secure any other intellectual property rights 65 | needed, if any. For example, if a third party patent license is required to 66 | allow Recipient to distribute the Program, it is Recipient's responsibility 67 | to acquire that license before distributing the Program. 68 | 69 | d) Each Contributor represents that to its knowledge it has sufficient 70 | copyright rights in its Contribution, if any, to grant the copyright license 71 | set forth in this Agreement. 72 | 73 | 3. REQUIREMENTS 74 | 75 | A Contributor may choose to distribute the Program in object code form under 76 | its own license agreement, provided that: 77 | 78 | a) it complies with the terms and conditions of this Agreement; and 79 | 80 | b) its license agreement: 81 | 82 | i) effectively disclaims on behalf of all Contributors all warranties and 83 | conditions, express and implied, including warranties or conditions of title 84 | and non-infringement, and implied warranties or conditions of merchantability 85 | and fitness for a particular purpose; 86 | 87 | ii) effectively excludes on behalf of all Contributors all liability for 88 | damages, including direct, indirect, special, incidental and consequential 89 | damages, such as lost profits; 90 | 91 | iii) states that any provisions which differ from this Agreement are offered 92 | by that Contributor alone and not by any other party; and 93 | 94 | iv) states that source code for the Program is available from such 95 | Contributor, and informs licensees how to obtain it in a reasonable manner on 96 | or through a medium customarily used for software exchange. 97 | 98 | When the Program is made available in source code form: 99 | 100 | a) it must be made available under this Agreement; and 101 | 102 | b) a copy of this Agreement must be included with each copy of the Program. 103 | 104 | Contributors may not remove or alter any copyright notices contained within 105 | the Program. 106 | 107 | Each Contributor must identify itself as the originator of its Contribution, 108 | if any, in a manner that reasonably allows subsequent Recipients to identify 109 | the originator of the Contribution. 110 | 111 | 4. COMMERCIAL DISTRIBUTION 112 | 113 | Commercial distributors of software may accept certain responsibilities with 114 | respect to end users, business partners and the like. While this license is 115 | intended to facilitate the commercial use of the Program, the Contributor who 116 | includes the Program in a commercial product offering should do so in a 117 | manner which does not create potential liability for other Contributors. 118 | Therefore, if a Contributor includes the Program in a commercial product 119 | offering, such Contributor ("Commercial Contributor") hereby agrees to defend 120 | and indemnify every other Contributor ("Indemnified Contributor") against any 121 | losses, damages and costs (collectively "Losses") arising from claims, 122 | lawsuits and other legal actions brought by a third party against the 123 | Indemnified Contributor to the extent caused by the acts or omissions of such 124 | Commercial Contributor in connection with its distribution of the Program in 125 | a commercial product offering. The obligations in this section do not apply 126 | to any claims or Losses relating to any actual or alleged intellectual 127 | property infringement. In order to qualify, an Indemnified Contributor must: 128 | a) promptly notify the Commercial Contributor in writing of such claim, and 129 | b) allow the Commercial Contributor tocontrol, and cooperate with the 130 | Commercial Contributor in, the defense and any related settlement 131 | negotiations. The Indemnified Contributor may participate in any such claim 132 | at its own expense. 133 | 134 | For example, a Contributor might include the Program in a commercial product 135 | offering, Product X. That Contributor is then a Commercial Contributor. If 136 | that Commercial Contributor then makes performance claims, or offers 137 | warranties related to Product X, those performance claims and warranties are 138 | such Commercial Contributor's responsibility alone. Under this section, the 139 | Commercial Contributor would have to defend claims against the other 140 | Contributors related to those performance claims and warranties, and if a 141 | court requires any other Contributor to pay any damages as a result, the 142 | Commercial Contributor must pay those damages. 143 | 144 | 5. NO WARRANTY 145 | 146 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS PROVIDED ON 147 | AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 148 | EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES OR 149 | CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY OR FITNESS FOR A 150 | PARTICULAR PURPOSE. Each Recipient is solely responsible for determining the 151 | appropriateness of using and distributing the Program and assumes all risks 152 | associated with its exercise of rights under this Agreement , including but 153 | not limited to the risks and costs of program errors, compliance with 154 | applicable laws, damage to or loss of data, programs or equipment, and 155 | unavailability or interruption of operations. 156 | 157 | 6. DISCLAIMER OF LIABILITY 158 | 159 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR ANY 160 | CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT, INCIDENTAL, 161 | SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING WITHOUT LIMITATION 162 | LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN 163 | CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 164 | ARISING IN ANY WAY OUT OF THE USE OR DISTRIBUTION OF THE PROGRAM OR THE 165 | EXERCISE OF ANY RIGHTS GRANTED HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY 166 | OF SUCH DAMAGES. 167 | 168 | 7. GENERAL 169 | 170 | If any provision of this Agreement is invalid or unenforceable under 171 | applicable law, it shall not affect the validity or enforceability of the 172 | remainder of the terms of this Agreement, and without further action by the 173 | parties hereto, such provision shall be reformed to the minimum extent 174 | necessary to make such provision valid and enforceable. 175 | 176 | If Recipient institutes patent litigation against any entity (including a 177 | cross-claim or counterclaim in a lawsuit) alleging that the Program itself 178 | (excluding combinations of the Program with other software or hardware) 179 | infringes such Recipient's patent(s), then such Recipient's rights granted 180 | under Section 2(b) shall terminate as of the date such litigation is filed. 181 | 182 | All Recipient's rights under this Agreement shall terminate if it fails to 183 | comply with any of the material terms or conditions of this Agreement and 184 | does not cure such failure in a reasonable period of time after becoming 185 | aware of such noncompliance. If all Recipient's rights under this Agreement 186 | terminate, Recipient agrees to cease use and distribution of the Program as 187 | soon as reasonably practicable. However, Recipient's obligations under this 188 | Agreement and any licenses granted by Recipient relating to the Program shall 189 | continue and survive. 190 | 191 | Everyone is permitted to copy and distribute copies of this Agreement, but in 192 | order to avoid inconsistency the Agreement is copyrighted and may only be 193 | modified in the following manner. The Agreement Steward reserves the right to 194 | publish new versions (including revisions) of this Agreement from time to 195 | time. No one other than the Agreement Steward has the right to modify this 196 | Agreement. The Eclipse Foundation is the initial Agreement Steward. The 197 | Eclipse Foundation may assign the responsibility to serve as the Agreement 198 | Steward to a suitable separate entity. Each new version of the Agreement will 199 | be given a distinguishing version number. The Program (including 200 | Contributions) may always be distributed subject to the version of the 201 | Agreement under which it was received. In addition, after a new version of 202 | the Agreement is published, Contributor may elect to distribute the Program 203 | (including its Contributions) under the new version. Except as expressly 204 | stated in Sections 2(a) and 2(b) above, Recipient receives no rights or 205 | licenses to the intellectual property of any Contributor under this 206 | Agreement, whether expressly, by implication, estoppel or otherwise. All 207 | rights in the Program not expressly granted under this Agreement are 208 | reserved. 209 | 210 | This Agreement is governed by the laws of the State of New York and the 211 | intellectual property laws of the United States of America. No party to this 212 | Agreement will bring a legal action under this Agreement more than one year 213 | after the cause of action arose. Each party waives its rights to a jury trial 214 | in any resulting litigation. 215 | -------------------------------------------------------------------------------- /src/re_frame_datatable/core.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable.core 2 | (:require-macros [reagent.ratom :refer [reaction]]) 3 | (:require [reagent.core :as reagent] 4 | [re-frame.core :as re-frame :refer [trim-v]] 5 | [cljs.spec.alpha :as s])) 6 | 7 | 8 | ; --- Model (spec) --- 9 | 10 | (s/def ::db-id keyword?) 11 | (s/def ::enabled? boolean?) 12 | (s/def ::css-classes (s/coll-of string?)) 13 | 14 | 15 | ; columns-def 16 | 17 | (s/def ::column-key (s/coll-of #(or (keyword? %) (int? %)) :kind vector)) 18 | (s/def ::column-label (s/or :string string? 19 | :component fn?)) 20 | (s/def ::comp-fn fn?) 21 | (s/def ::sorting (s/keys :req [::enabled?] 22 | :opt [::comp-fn])) 23 | (s/def ::td-class-fn fn?) 24 | 25 | 26 | (s/def ::column-def 27 | (s/keys :req [::column-key] 28 | :opt [::sorting ::render-fn ::td-class-fn ::column-label])) 29 | 30 | (s/def ::columns-def (s/coll-of ::column-def :min-count 1)) 31 | 32 | 33 | ; options 34 | 35 | (s/def ::table-classes ::css-classes) 36 | 37 | (s/def ::per-page (s/and integer? pos?)) 38 | (s/def ::cur-page (s/and integer? (complement neg?))) 39 | (s/def ::total-pages (s/and integer? pos?)) 40 | (s/def ::pagination 41 | (s/keys :req [::enabled?] 42 | :opt [::per-page ::cur-page ::total-pages])) 43 | 44 | (s/def ::selected-indexes (s/coll-of nat-int? :kind set)) 45 | (s/def ::selection 46 | (s/keys :req [::enabled?] 47 | :opt [::selected-indexes])) 48 | 49 | (s/def ::extra-header-row-component fn?) 50 | (s/def ::footer-component fn?) 51 | (s/def ::header-enabled? ::enabled?) 52 | 53 | (s/def ::options 54 | (s/nilable 55 | (s/keys :opt [::pagination 56 | ::header-enabled? 57 | ::table-classes 58 | ::selection 59 | ::extra-header-row-component 60 | ::footer-component]))) 61 | 62 | 63 | ; --- Re-frame database paths --- 64 | 65 | (def root-db-path [::re-frame-datatable]) 66 | (defn db-path-for [db-path db-id] 67 | (vec (concat (conj root-db-path db-id) 68 | db-path))) 69 | 70 | (def columns-def-db-path (partial db-path-for [::columns-def])) 71 | (def options-db-path (partial db-path-for [::options])) 72 | (def state-db-path (partial db-path-for [::state])) 73 | (def sort-key-db-path (partial db-path-for [::state ::sort ::sort-key])) 74 | (def sort-comp-order-db-path (partial db-path-for [::state ::sort ::sort-comp])) 75 | (def sort-comp-fn-db-path (partial db-path-for [::state ::sort ::sort-fn])) 76 | 77 | 78 | ; --- Defaults --- 79 | 80 | (def default-per-page 25) 81 | 82 | 83 | ; --- Utils --- 84 | (defn css-class-str [classes] 85 | {:class (->> classes 86 | (filter (complement nil?)) 87 | (clojure.string/join \space))}) 88 | 89 | 90 | 91 | ; --- Events --- 92 | ; ---------------------------------------------------------------------------------------------------------------------- 93 | 94 | (re-frame/reg-event-db 95 | ::on-will-mount 96 | [trim-v] 97 | (fn [db [db-id data-sub columns-def options]] 98 | (-> db 99 | (assoc-in (columns-def-db-path db-id) 100 | columns-def) 101 | (assoc-in (options-db-path db-id) 102 | options) 103 | (assoc-in (state-db-path db-id) 104 | {::pagination (merge {::per-page default-per-page 105 | ::cur-page 0} 106 | (select-keys (::pagination options) [::per-page ::enabled?])) 107 | ::total-items (count @(re-frame/subscribe data-sub)) 108 | ::selection (merge {::selected-indexes (if (get-in options [::selection ::enabled?]) 109 | (or (get-in options [::selection ::selected-indexes]) #{}) 110 | #{})} 111 | (select-keys (::selection options) [::enabled?]))})))) 112 | 113 | 114 | (re-frame/reg-event-db 115 | ::on-did-update 116 | [trim-v] 117 | (fn [db [db-id data-sub columns-def options]] 118 | (-> db 119 | (assoc-in (columns-def-db-path db-id) 120 | columns-def) 121 | (assoc-in (options-db-path db-id) 122 | options) 123 | 124 | (assoc-in (conj (state-db-path db-id) ::total-items) (count @(re-frame/subscribe data-sub)))))) 125 | 126 | 127 | (re-frame/reg-event-db 128 | ::on-will-unmount 129 | [trim-v] 130 | (fn [db [db-id]] 131 | (update-in db root-db-path dissoc db-id))) 132 | 133 | 134 | 135 | (re-frame/reg-event-db 136 | ::change-state-value 137 | [trim-v] 138 | (fn [db [db-id state-path new-val]] 139 | (assoc-in db (vec (concat (state-db-path db-id) state-path)) new-val))) 140 | 141 | 142 | 143 | ; --- Subs --- 144 | ; ---------------------------------------------------------------------------------------------------------------------- 145 | 146 | (re-frame/reg-sub 147 | ::state 148 | (fn [db [_ db-id]] 149 | (get-in db (state-db-path db-id)))) 150 | 151 | 152 | (re-frame/reg-sub 153 | ::data 154 | (fn [[_ db-id data-sub]] 155 | [(re-frame/subscribe data-sub) 156 | (re-frame/subscribe [::state db-id])]) 157 | 158 | (fn [[items state]] 159 | (let [sort-data (fn [coll] 160 | (let [{:keys [::sort-key ::sort-comp ::sort-fn]} (::sort state)] 161 | (if sort-key 162 | (cond->> coll 163 | true (sort-by #(get-in (second %) sort-key) sort-fn) 164 | (= ::sort-desc sort-comp) (reverse)) 165 | coll))) 166 | 167 | paginate-data (fn [coll] 168 | (let [{:keys [::cur-page ::per-page ::enabled?]} (::pagination state)] 169 | (if enabled? 170 | (->> coll 171 | (drop (* (or cur-page 0) (or per-page 0))) 172 | (take (or per-page 0))) 173 | coll)))] 174 | 175 | {::visible-items (->> items 176 | (map-indexed vector) 177 | (sort-data) 178 | (paginate-data)) 179 | ::state state}))) 180 | 181 | 182 | ; --- Sorting --- 183 | ; ---------------------------------------------------------------------------------------------------------------------- 184 | 185 | (re-frame/reg-event-db 186 | ::set-sort-key 187 | [trim-v] 188 | (fn [db [db-id sort-key comp-fn]] 189 | (let [comp-fn (or comp-fn <) 190 | cur-sort-key (get-in db (sort-key-db-path db-id)) 191 | cur-sort-comp (get-in db (sort-comp-order-db-path db-id) ::sort-asc)] 192 | (if (= cur-sort-key sort-key) 193 | (assoc-in db (sort-comp-order-db-path db-id) 194 | (get {::sort-asc ::sort-desc 195 | ::sort-desc ::sort-asc} cur-sort-comp)) 196 | (-> db 197 | (assoc-in (sort-key-db-path db-id) sort-key) 198 | (assoc-in (sort-comp-fn-db-path db-id) comp-fn) 199 | (assoc-in (sort-comp-order-db-path db-id) cur-sort-comp)))))) 200 | 201 | 202 | 203 | ; --- Pagination --- 204 | ; ---------------------------------------------------------------------------------------------------------------------- 205 | 206 | (re-frame/reg-sub 207 | ::pagination-state 208 | (fn [[_ db-id data-sub]] 209 | [(re-frame/subscribe [::state db-id]) 210 | (re-frame/subscribe data-sub)]) 211 | (fn [[state items]] 212 | (let [{:keys [::pagination]} state] 213 | (merge 214 | (select-keys pagination [::per-page]) 215 | {::cur-page (or (::cur-page pagination) 0) 216 | ::pages (->> items 217 | (map-indexed vector) 218 | (map first) 219 | (partition-all (or (::per-page pagination) 1)) 220 | (mapv (fn [i] [(first i) (last i)])))})))) 221 | 222 | 223 | (re-frame/reg-event-fx 224 | ::select-next-page 225 | [trim-v] 226 | (fn [{:keys [db]} [db-id pagination-state]] 227 | (let [{:keys [::cur-page ::pages]} pagination-state] 228 | {:db db 229 | :dispatch [::change-state-value db-id [::pagination ::cur-page] (min (inc cur-page) (dec (count pages)))]}))) 230 | 231 | 232 | (re-frame/reg-event-fx 233 | ::select-prev-page 234 | [trim-v] 235 | (fn [{:keys [db]} [db-id pagination-state]] 236 | (let [{:keys [::cur-page]} pagination-state] 237 | {:db db 238 | :dispatch [::change-state-value db-id [::pagination ::cur-page] (max (dec cur-page) 0)]}))) 239 | 240 | 241 | (re-frame/reg-event-fx 242 | ::select-page 243 | [trim-v] 244 | (fn [{:keys [db]} [db-id pagination-state page-idx]] 245 | {:db db 246 | :dispatch [::change-state-value db-id [::pagination ::cur-page] page-idx]})) 247 | 248 | 249 | (re-frame/reg-event-fx 250 | ::set-per-page-value 251 | [trim-v] 252 | (fn [{:keys [db]} [db-id pagination-state per-page]] 253 | {:db db 254 | :dispatch-n [[::select-page db-id pagination-state 0] 255 | [::change-state-value db-id [::pagination ::per-page] per-page]]})) 256 | 257 | 258 | 259 | ; --- Rows Selection --- 260 | ; ---------------------------------------------------------------------------------------------------------------------- 261 | 262 | (re-frame/reg-sub 263 | ::selected-items 264 | (fn [[_ db-id data-sub]] 265 | [(re-frame/subscribe data-sub) 266 | (re-frame/subscribe [::state db-id])]) 267 | 268 | (fn [[items state]] 269 | (->> items 270 | (map-indexed vector) 271 | (filter (fn [[idx _]] (contains? (get-in state [::selection ::selected-indexes]) idx))) 272 | (map second)))) 273 | 274 | 275 | (re-frame/reg-event-db 276 | ::change-row-selection 277 | [trim-v] 278 | (fn [db [db-id row-index selected?]] 279 | (update-in db (vec (concat (state-db-path db-id) [::selection ::selected-indexes])) 280 | (if selected? conj disj) row-index))) 281 | 282 | 283 | (re-frame/reg-event-db 284 | ::change-table-selection 285 | [trim-v] 286 | (fn [db [db-id indexes selected?]] 287 | (let [selection-indexes-path (vec (concat (state-db-path db-id) [::selection ::selected-indexes])) 288 | selection (get-in db selection-indexes-path)] 289 | (assoc-in db selection-indexes-path 290 | (if selected? 291 | (clojure.set/union (set indexes) selection) 292 | (clojure.set/difference selection (set indexes))))))) 293 | 294 | 295 | (re-frame/reg-event-db 296 | ::unselect-all-rows 297 | [trim-v] 298 | (fn [db [db-id]] 299 | (assoc-in db (vec (concat (state-db-path db-id) [::selection ::selected-indexes])) #{}))) 300 | 301 | 302 | 303 | ; --- Views --- 304 | ; ---------------------------------------------------------------------------------------------------------------------- 305 | 306 | (defn datatable [db-id data-sub columns-def & [options]] 307 | {:pre [(or (s/valid? ::db-id db-id) 308 | (js/console.error (s/explain-str ::db-id db-id))) 309 | 310 | (or (s/valid? ::columns-def columns-def) 311 | (js/console.error (s/explain-str ::columns-def columns-def))) 312 | 313 | (or (s/valid? ::options options) 314 | (js/console.error (s/explain-str ::options options)))]} 315 | 316 | (let [view-data (re-frame/subscribe [::data db-id data-sub])] 317 | 318 | (reagent/create-class 319 | {:component-will-mount 320 | #(re-frame/dispatch [::on-will-mount db-id data-sub columns-def options]) 321 | 322 | 323 | :component-did-update 324 | (fn [this] 325 | (let [[_ db-id data-sub columns-def options] (reagent/argv this)] 326 | (re-frame/dispatch [::on-did-update db-id data-sub columns-def options]) 327 | (when (not= (get-in @view-data [::state ::total-items]) (count @(re-frame/subscribe data-sub))) 328 | (re-frame/dispatch [::select-page db-id @(re-frame/subscribe [::pagination-state db-id data-sub]) 0])))) 329 | 330 | 331 | :component-will-unmount 332 | #(re-frame/dispatch [::on-will-unmount db-id]) 333 | 334 | 335 | :component-function 336 | (fn [db-id data-sub columns-def & [options]] 337 | (let [{:keys [::visible-items ::state]} @view-data 338 | {:keys [::selection]} state 339 | {:keys [::table-classes 340 | ::tr-class-fn 341 | ::header-enabled? 342 | ::extra-header-row-component 343 | ::footer-component 344 | ::empty-tbody-component]} options] 345 | 346 | [:table.re-frame-datatable 347 | (when table-classes 348 | (css-class-str table-classes)) 349 | 350 | (when-not (= header-enabled? false) 351 | [:thead 352 | (when extra-header-row-component 353 | [extra-header-row-component]) 354 | 355 | [:tr 356 | (when (::enabled? selection) 357 | [:th {:style {:max-width "16em"}} 358 | [:input {:type "checkbox" 359 | :checked (clojure.set/subset? 360 | (->> visible-items (map first) (set)) 361 | (::selected-indexes selection)) 362 | :on-change #(when-not (zero? (count visible-items)) 363 | (re-frame/dispatch [::change-table-selection 364 | db-id 365 | (->> visible-items (map first) (set)) 366 | (-> % .-target .-checked)]))}] 367 | [:br] 368 | [:small (str (count (::selected-indexes selection)) " selected")]]) 369 | 370 | (doall 371 | (for [{:keys [::column-key ::column-label ::sorting]} columns-def] 372 | ^{:key (str column-key)} 373 | [:th 374 | (merge 375 | (when (::enabled? sorting) 376 | {:style {:cursor "pointer"} 377 | :on-click #(re-frame/dispatch [::set-sort-key db-id column-key (::comp-fn sorting)]) 378 | :class "sorted-by"}) 379 | (when (= column-key (get-in state [::sort ::sort-key])) 380 | (css-class-str ["sorted-by" 381 | (condp = (get-in state [::sort ::sort-comp]) 382 | ::sort-asc "asc" 383 | ::sort-desc "desc" 384 | "")]))) 385 | (cond 386 | (string? column-label) column-label 387 | (fn? column-label) [column-label] 388 | :else "")]))]]) 389 | 390 | 391 | [:tbody 392 | (if (empty? visible-items) 393 | [:tr 394 | [:td {:col-span (+ (count columns-def) 395 | (if (::enabled? selection) 1 0)) 396 | :style {:text-align "center"}} 397 | (if empty-tbody-component 398 | [empty-tbody-component] 399 | "no items")]] 400 | 401 | (doall 402 | (for [[i data-entry] visible-items] 403 | ^{:key i} 404 | [:tr 405 | (merge 406 | {} 407 | (when tr-class-fn 408 | (css-class-str (tr-class-fn data-entry)))) 409 | 410 | (when (::enabled? selection) 411 | [:td 412 | [:input {:type "checkbox" 413 | :checked (contains? (::selected-indexes selection) i) 414 | :on-change #(re-frame/dispatch [::change-row-selection db-id i (-> % .-target .-checked)])}]]) 415 | 416 | (doall 417 | (for [{:keys [::column-key ::render-fn ::td-class-fn]} columns-def] 418 | ^{:key (str i \- column-key)} 419 | [:td 420 | (merge 421 | {} 422 | (when td-class-fn 423 | (css-class-str (td-class-fn (get-in data-entry column-key) data-entry)))) 424 | 425 | (if render-fn 426 | [render-fn (get-in data-entry column-key) data-entry] 427 | (get-in data-entry column-key))]))])))] 428 | 429 | (when footer-component 430 | [:tfoot 431 | [footer-component]])]))}))) --------------------------------------------------------------------------------
"] " element. " 62 | "DataTable can set these classes via column definition by providing " [:code.inline-code "::td-class-fn"] " option that accepts a function with following signature:"] 63 | [:pre 64 | [:code {:class "clojure"} 65 | "(defn td-class-fn [cell-value row-value] 66 | {:post [(seq? %) 67 | (every? (fn [t] (or (string? t) (nil? t))) %)]} 68 | ;... 69 | )"]] 70 | 71 | [:p 72 | "i.e. function should return a sequence of CSS classes (represented as strings) that should be applied to " [:code.inline-code ""] " element."] 73 | 74 | [components/info-message 75 | [:div "In the example below " [:code.inline-code "row-value"] " argument is not provided to neither of functions, because the value of class can be determined by single property only."]]] 76 | 77 | [components/tabs-wrapper 78 | :marking-cells 79 | [::subs/marking-elements-data] 80 | [{::dt/column-key [:index] 81 | ::dt/column-label "#"} 82 | {::dt/column-key [:name] 83 | ::dt/column-label "Name"} 84 | {::dt/column-key [:stats :play_count] 85 | ::dt/column-label "Play Count" 86 | ::dt/td-class-fn table-views/play-count-td-classes} 87 | {::dt/column-key [:stats :rating] 88 | ::dt/column-label "Rating" 89 | ::dt/td-class-fn table-views/rating-td-classes}] 90 | {::dt/table-classes ["ui" "celled" "table"]} 91 | [{:data-tab "css-classes-source" 92 | :label "CSS Classes Source" 93 | :component (fn [] 94 | [formatters/formatted-function-def 95 | (with-out-str (r/source table-views/play-count-td-classes)) 96 | (with-out-str (r/source table-views/rating-td-classes))])}]]]) 97 | 98 | 99 | 100 | (defn styling-rows [] 101 | [:div 102 | [:div 103 | [:p 104 | "Similar capabilities are available for the complete row, with the difference that the option is defined via " [:code.inline-code "options"] " argument." 105 | [:code.inline-code "::tr-class-fn"] " option that accepts a function with following signature:"] 106 | [:pre 107 | [:code {:class "clojure"} 108 | "(defn td-class-fn [row-value] 109 | {:post [(seq? %) 110 | (every? (fn [t] (or (string? t) (nil? t))))]} 111 | ;... 112 | )"]]] 113 | 114 | [components/tabs-wrapper 115 | :marking-rows 116 | [::subs/marking-elements-data] 117 | [{::dt/column-key [:index] 118 | ::dt/column-label "#"} 119 | {::dt/column-key [:name] 120 | ::dt/column-label "Name"} 121 | {::dt/column-key [:stats :play_count] 122 | ::dt/column-label "Play Count"} 123 | {::dt/column-key [:stats :rating] 124 | ::dt/column-label "Rating"}] 125 | {::dt/table-classes ["ui" "celled" "table"] 126 | ::dt/tr-class-fn table-views/play-count-tr-classes} 127 | [{:data-tab "css-classes-source" 128 | :label "CSS Classes Source" 129 | :component (fn [] 130 | [formatters/formatted-function-def 131 | (with-out-str (r/source table-views/play-count-tr-classes))])}]]]) 132 | -------------------------------------------------------------------------------- /docs/src/cljs/re_frame_datatable_docs/table_views.cljs: -------------------------------------------------------------------------------- 1 | (ns re-frame-datatable-docs.table-views 2 | (:require [re-frame.core :as re-frame] 3 | [re-frame-datatable.core] 4 | [re-frame-datatable-docs.subs])) 5 | 6 | 7 | 8 | (defn aggregation-row [] 9 | [:tr 10 | [:th {:col-span 1} ""] 11 | [:th {:col-span 3} "Album info"]]) 12 | 13 | 14 | 15 | (defn total-play-count-footer [] 16 | ; ::total-play-count subscription returns sum of all :play_count values 17 | ; for ::basic-definition-data subscription 18 | (let [total-count (re-frame/subscribe [::re-frame-datatable-docs.subs/total-play-count])] 19 | [:tr 20 | [:th {:col-span 2}] 21 | [:th 22 | [:strong (str @total-count " total")]]])) 23 | 24 | 25 | 26 | (defn play-count-td-classes [play-count] 27 | "Set 'disabled' class for the song if it hasn't been played yet" 28 | [(when (zero? play-count) 29 | "disabled")]) 30 | 31 | 32 | 33 | (defn rating-td-classes [rating] 34 | "Color-code cell by rating" 35 | [(cond 36 | (= 5 rating) "positive" 37 | (#{3 4} rating) "warning" 38 | (nil? rating) nil ; DataTable will ignore all nil values in classes vector 39 | :else "negative")]) 40 | 41 | 42 | 43 | (defn play-count-tr-classes 44 | "For all songs that hasn't been played yet, define
"] 40 | " elements collapsed into single one via " [:code.inline-code ":col-span"] " attribute."] 41 | [:p 42 | "If you want to provide a custom Reagent component to be rendered inside this empty row, use " 43 | [:code.inline-code "::empty-tbody-component"] " option."]] 44 | 45 | [components/tabs-wrapper 46 | :empty-tbody 47 | [::subs/empty-dataset] 48 | [{::dt/column-key [:index] 49 | ::dt/column-label "Index"} 50 | {::dt/column-key [:name] 51 | ::dt/column-label "Name"} 52 | {::dt/column-key [:stats :play_count] 53 | ::dt/column-label "Play count"}] 54 | {::dt/table-classes ["ui" "celled" "table"] 55 | ::dt/empty-tbody-component table-views/empty-tbody-formatter 56 | ::dt/pagination {::dt/enabled? true}} 57 | [{:data-tab "empty-tbody-source" 58 | :label "Empty Row Source" 59 | :component (fn [] 60 | [formatters/formatted-function-def 61 | (with-out-str (r/source table-views/empty-tbody-formatter))])}] 62 | 63 | (fn [dt-def] 64 | [:div.ui.grid 65 | [:div.row 66 | [:div.right.aligned.column 67 | [dt-views/default-pagination-controls :empty-tbody [::subs/empty-dataset]]]] 68 | [:div.row 69 | [:div.column 70 | dt-def]]])]]) 71 | 72 | 73 | 74 | (defn extra-header [] 75 | [:div 76 | [:div 77 | "DataTable supports rendering of an additional row inside " [:code.inline-code "