Last active
May 9, 2019 15:09
-
-
Save JulienRouse/32b3119f305491f70c70b30df0b64859 to your computer and use it in GitHub Desktop.
Little table for employee with filtering. Use Reagent, Re-Frame and spec (and there is a klipse version for trying it out in the browser)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<head> | |
<meta charset="utf-8"> | |
<link rel="stylesheet" type="text/css" href="http://app.klipse.tech/css/codemirror.css"> | |
<script> | |
window.klipse_settings = { | |
eval_idle_msec: 200, // idle time in msec before the snippet is evaluated | |
selector: '.clojure',// css selector for the html elements you want to klipsify | |
selector_reagent: '.reagent', // selector for reagent snippets | |
codemirror_options_in: { | |
indentUnit: 2, | |
lineWrapping: true, | |
lineNumbers: true, | |
autoCloseBrackets: true | |
}, | |
codemirror_options_out: { | |
lineWrapping: true, | |
lineNumbers: true | |
} | |
}; | |
</script> | |
<style> | |
table { | |
font-family: "Trebuchet MS", Arial, Helvetica, sans-serif; | |
border-collapse: collapse; | |
width: 100%; | |
} | |
td, th { | |
border: 1px solid #ddd; | |
padding: 8px; | |
} | |
tr:nth-child(even){background-color: #f2f2f2;} | |
tr:hover {background-color: #ddd;} | |
th { | |
padding-top: 12px; | |
padding-bottom: 12px; | |
text-align: left; | |
background-color: #4CAF50; | |
color: white; | |
} | |
</style> | |
</head> | |
<body> | |
<div class="clojure"> | |
(ns my.reagent-examples | |
(:require [reagent.core :as reagent] | |
[cljs.spec.alpha :as s] | |
[re-frame.db :as db] | |
[re-frame.core :as rf])) | |
;; -- Specs -- | |
;; ----------- | |
(s/def ::name string?) ; Name is a string | |
(s/def ::date-fin-mandat inst?) ; date is of type #inst (a timestamp) | |
(s/def ::role keyword?) ;role is a keyword | |
; One people is a map with required keys name, date and role and no optional keys | |
(s/def ::people (s/keys :req [::name ::date-fin-mandat ::role] ;; ?Should I use :req or :un-req ? | |
:opt [])) | |
; Peoples is a vector of 0 to many people | |
(s/def ::peoples (s/coll-of ::people :kind vector? :min-count 0)) | |
; A filter is a 2 elements vector of keyword like [:role :dev] | |
(s/def ::filter (s/tuple keyword? (s/or :s string? :k keyword?))) | |
; Filters is a vector of 0 to many filter | |
(s/def ::filters (s/coll-of ::filter :kind vector? :min-count 0)) | |
; Spec for the whole db | |
(s/def ::db (s/keys :req-un [::peoples ::filters])) | |
;; -- Data -- | |
;; -------- | |
(def peoples [ | |
{::name "julien" | |
::date-fin-mandat (js/Date.) | |
::role :dev} | |
{::name "juscellino" | |
::date-fin-mandat (js/Date. 2019 7 21) | |
::role :dev} | |
{::name "danny" | |
::date-fin-mandat (js/Date. 2019 4 15) | |
::role :dev} | |
{::name "nathalie" | |
::date-fin-mandat (js/Date. 2031 9 22) | |
::role :rh} | |
{::name "malik" | |
::date-fin-mandat (js/Date. 2019 1 22) | |
::role :analyste} | |
{::name "daniel" | |
::date-fin-mandat (js/Date. 2019 8 15) | |
::role :dev}]) | |
;; -- Helpers -- | |
;; ------------- | |
(defn filter-people | |
[peoples filters] | |
(into | |
[] | |
(set | |
(flatten | |
(mapv | |
(fn [[k v]] | |
(filter | |
#(= (k %1) v) | |
peoples)) | |
filters))))) | |
(def end-soon-style {:background-color "red"}) | |
(def end-not-so-soon-style {:background-color "orange"}) | |
(def end-very-not-soon-style {:background-color "green"}) | |
(defn date+30 | |
[date] | |
(js/Date. (.getFullYear date) (.getMonth date) (+ (.getDate date) 30))) | |
(defn date+90 | |
[date] | |
(js/Date. (.getFullYear date) (.getMonth date) (+ (.getDate date) 90))) | |
(defn style-by-date | |
[date end-soon end-not-so-soon end-very-not-soon] | |
(let [today (js/Date.)] | |
(cond | |
(>= today date) end-soon | |
(< date (date+30 today)) end-not-so-soon | |
:else end-very-not-soon))) | |
;; -- 1 - Dispatch event -- | |
;; ------------------------ | |
; Do i need this? | |
(defn dispatch-add-filter-event | |
[new-filter] | |
(rf/dispatch [:add-filter [:role :dev]])) | |
;; -- 2 - Event Handler -- | |
;; ----------------------- | |
;; -- Interceptor -- | |
;; ----------------- | |
; Interceptor for validating spec after every add into db | |
(defn check-and-throw | |
"Throws an exception if `db` doesn't match the Spec `a-spec`." | |
[a-spec db] | |
(when-not (s/valid? a-spec db) | |
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {})))) | |
;; now we create an interceptor using `after` | |
(def check-spec-interceptor (rf/after (partial check-and-throw ::db))) | |
;; -- Define Event Handlers -- | |
;; --------------------------- | |
; Initialize the app state | |
(rf/reg-event-db | |
:initialize | |
[check-spec-interceptor] | |
(fn [_ _] | |
{:peoples peoples | |
:filters [[::role :dev] [::name "julien"]]})) | |
; Add a new filter in app state | |
(rf/reg-event-db | |
:add-filter | |
[check-spec-interceptor] | |
(fn [db [_ new-filter]] | |
(assoc db :filters (conj (:filters db) new-filter)))) | |
; Add all filters for a particular key | |
(rf/reg-event-db | |
:add-all-filters-for-key | |
[check-spec-interceptor] | |
(fn [db [_ key1 values]] | |
(assoc db | |
:filters | |
(into | |
(:filters db) | |
(map | |
#(vec [key1 %1]) | |
values))))) | |
; Remove a filter from app state | |
(rf/reg-event-db | |
:remove-filter | |
[check-spec-interceptor] | |
(fn [db [_ old-filter]] | |
(assoc db | |
:filters | |
(filterv | |
#(not (= %1 old-filter)) | |
(:filters db))))) | |
; Remove all filters from app state for a particular key | |
(rf/reg-event-db | |
:remove-all-filters-for-key | |
[check-spec-interceptor] | |
(fn [db [_ key1]] | |
(assoc db | |
:filters | |
(filterv | |
#(not (= (first %1) key1)) | |
(:filters db))))) | |
;; -- 3 - Effect Handler -- | |
;; ------------------------ | |
;; ?? Probably nothing here?? | |
;; -- 4 - Subscription Handler -- | |
;; ------------------------------ | |
; Return the vector of filters | |
(rf/reg-sub | |
:filters | |
(fn [db _] | |
(:filters db))) | |
; Return the peoples, unfiltered | |
(rf/reg-sub | |
:peoples | |
(fn [db _] | |
(:peoples db))) | |
; Return a list of filtered peoples | |
(rf/reg-sub | |
:peoples-filtered | |
(fn [db _] | |
(filter-people (:peoples db) (:filters db)))) | |
; Given a key k, return a vector of values representing all the values present in peoples | |
(rf/reg-sub | |
:values-for-key | |
(fn [db [_ k]] | |
(map #(k %1) (:peoples db)))) | |
; Does filter contains the value k in it? | |
(rf/reg-sub | |
:filter-contains? | |
(fn [db [_ k]] | |
(contains? (set (:filters db)) k))) | |
;; -- 5 - View Function -- | |
;; ----------------------- | |
(defn list-people | |
[] | |
[:p (pr-str @(rf/subscribe [:peoples]))]) | |
(defn list-people-filtered | |
[] | |
[:p (pr-str @(rf/subscribe [:peoples-filtered]))]) | |
(defn list-filter | |
[] | |
[:p (pr-str @(rf/subscribe [:filters]))]) | |
(defn button-add-role | |
[role] | |
[:button | |
{:on-click (fn [e] (rf/dispatch [:add-filter [::role role]]))} | |
(pr-str "Add filter" role)]) | |
(defn button-remove-role | |
[role] | |
[:button | |
{:on-click (fn [e] (rf/dispatch [:remove-filter [::role role]]))} | |
(pr-str "Remove filter" role)]) | |
(defn ressource | |
[data] | |
[:tr | |
[:td (data ::name)] | |
[:td {:style (style-by-date | |
(data ::date-fin-mandat) | |
end-soon-style | |
end-not-so-soon-style | |
end-very-not-soon-style)}(str (data ::date-fin-mandat))] | |
[:td (data ::role)] | |
]) | |
(defn checklist-filter | |
[key1] | |
(let [list-values (set @(rf/subscribe [:values-for-key key1]))] | |
(into | |
[:form | |
[:input | |
{:type "checkbox" | |
:id (str "all-" key1) | |
:name (str "all-" key1) | |
:on-change (fn [e] (if (.. e -target -checked) | |
(rf/dispatch [:add-all-filters-for-key key1 list-values]) | |
(rf/dispatch [:remove-all-filters-for-key key1])))}] | |
[:label {:for (str "all-" key1)} "All"] | |
] | |
(for [value list-values] | |
[:div | |
[:input | |
{:type "checkbox" | |
:id value | |
:name value | |
:on-change (fn [e] (if (.. e -target -checked) | |
(rf/dispatch [:add-filter [key1 value]]) | |
(rf/dispatch [:remove-filter [key1 value]]))) | |
:checked @(rf/subscribe [:filter-contains? [key1 value]]) | |
}] | |
[:label {:for value} value]] | |
)))) | |
(defn peoples-ui-filtered | |
[] | |
(let [pf @(rf/subscribe [:peoples-filtered])] | |
(into | |
[:table | |
[:tr | |
[:th "NAME"] | |
[:th "DATE FIN MANDAT"] | |
[:th "ROLE"]] | |
[:tr | |
[:td [checklist-filter ::name]] | |
[:td ] | |
[:td [checklist-filter ::role]]] | |
] | |
(doall | |
(for [p pf] | |
[ressource p]))))) | |
(defn ui | |
[] | |
[:div | |
[peoples-ui-filtered] | |
]) | |
;; -- Kickstart the application -- | |
;; ------------------------------- | |
(defn ^:export run | |
[] | |
(rf/dispatch-sync [:initialize]) ;; puts a value into application state | |
(reagent/render [ui] ;; mount the application's ui into '' | |
(js/document.getElementById "app"))) | |
(run) | |
</div> | |
<div id="app"></div> | |
<script src="https://storage.googleapis.com/app.klipse.tech/plugin/js/klipse_plugin.js"></script> | |
</body> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(ns my.reagent-examples | |
(:require [reagent.core :as reagent] | |
[cljs.spec.alpha :as s] | |
[re-frame.db :as db] | |
[re-frame.core :as rf])) | |
;; -- Specs -- | |
;; ----------- | |
(s/def ::name string?) ; Name is a string | |
(s/def ::date-fin-mandat inst?) ; date is of type #inst (a timestamp) | |
(s/def ::role keyword?) ;role is a keyword | |
; One people is a map with required keys name, date and role and no optional keys | |
(s/def ::people (s/keys :req [::name ::date-fin-mandat ::role] ;; ?Should I use :req or :un-req ? | |
:opt [])) | |
; Peoples is a vector of 0 to many people | |
(s/def ::peoples (s/coll-of ::people :kind vector? :min-count 0)) | |
; A filter is a 2 elements vector of keyword like [:role :dev] | |
(s/def ::filter (s/tuple keyword? (s/or :s string? :k keyword?))) | |
; Filters is a vector of 0 to many filter | |
(s/def ::filters (s/coll-of ::filter :kind vector? :min-count 0)) | |
; Spec for the whole db | |
(s/def ::db (s/keys :req-un [::peoples ::filters])) | |
;; -- Data -- | |
;; -------- | |
(def peoples [ | |
{::name "julien" | |
::date-fin-mandat (js/Date.) | |
::role :dev} | |
{::name "juscellino" | |
::date-fin-mandat (js/Date. 2019 7 21) | |
::role :dev} | |
{::name "danny" | |
::date-fin-mandat (js/Date. 2019 4 15) | |
::role :dev} | |
{::name "nathalie" | |
::date-fin-mandat (js/Date. 2031 9 22) | |
::role :rh} | |
{::name "malik" | |
::date-fin-mandat (js/Date. 2019 1 22) | |
::role :analyste} | |
{::name "daniel" | |
::date-fin-mandat (js/Date. 2019 8 15) | |
::role :dev}]) | |
;; -- Helpers -- | |
;; ------------- | |
(defn filter-people | |
[peoples filters] | |
(into | |
[] | |
(set | |
(flatten | |
(mapv | |
(fn [[k v]] | |
(filter | |
#(= (k %1) v) | |
peoples)) | |
filters))))) | |
(def end-soon-style {:background-color "red"}) | |
(def end-not-so-soon-style {:background-color "orange"}) | |
(def end-very-not-soon-style {:background-color "green"}) | |
(defn date+30 | |
[date] | |
(js/Date. (.getFullYear date) (.getMonth date) (+ (.getDate date) 30))) | |
(defn date+90 | |
[date] | |
(js/Date. (.getFullYear date) (.getMonth date) (+ (.getDate date) 90))) | |
(defn style-by-date | |
[date end-soon end-not-so-soon end-very-not-soon] | |
(let [today (js/Date.)] | |
(cond | |
(>= today date) end-soon | |
(< date (date+30 today)) end-not-so-soon | |
:else end-very-not-soon))) | |
;; -- 1 - Dispatch event -- | |
;; ------------------------ | |
; Do i need this? | |
(defn dispatch-add-filter-event | |
[new-filter] | |
(rf/dispatch [:add-filter [:role :dev]])) | |
;; -- 2 - Event Handler -- | |
;; ----------------------- | |
;; -- Interceptor -- | |
;; ----------------- | |
; Interceptor for validating spec after every add into db | |
(defn check-and-throw | |
"Throws an exception if `db` doesn't match the Spec `a-spec`." | |
[a-spec db] | |
(when-not (s/valid? a-spec db) | |
(throw (ex-info (str "spec check failed: " (s/explain-str a-spec db)) {})))) | |
;; now we create an interceptor using `after` | |
(def check-spec-interceptor (rf/after (partial check-and-throw ::db))) | |
;; -- Define Event Handlers -- | |
;; --------------------------- | |
; Initialize the app state | |
(rf/reg-event-db | |
:initialize | |
[check-spec-interceptor] | |
(fn [_ _] | |
{:peoples peoples | |
:filters [[::role :dev] [::name "julien"]]})) | |
; Add a new filter in app state | |
(rf/reg-event-db | |
:add-filter | |
[check-spec-interceptor] | |
(fn [db [_ new-filter]] | |
(assoc db :filters (conj (:filters db) new-filter)))) | |
; Add all filters for a particular key | |
(rf/reg-event-db | |
:add-all-filters-for-key | |
[check-spec-interceptor] | |
(fn [db [_ key1 values]] | |
(assoc db | |
:filters | |
(into | |
(:filters db) | |
(map | |
#(vec [key1 %1]) | |
values))))) | |
; Remove a filter from app state | |
(rf/reg-event-db | |
:remove-filter | |
[check-spec-interceptor] | |
(fn [db [_ old-filter]] | |
(assoc db | |
:filters | |
(filterv | |
#(not (= %1 old-filter)) | |
(:filters db))))) | |
; Remove all filters from app state for a particular key | |
(rf/reg-event-db | |
:remove-all-filters-for-key | |
[check-spec-interceptor] | |
(fn [db [_ key1]] | |
(assoc db | |
:filters | |
(filterv | |
#(not (= (first %1) key1)) | |
(:filters db))))) | |
;; -- 3 - Effect Handler -- | |
;; ------------------------ | |
;; ?? Probably nothing here?? | |
;; -- 4 - Subscription Handler -- | |
;; ------------------------------ | |
; Return the vector of filters | |
(rf/reg-sub | |
:filters | |
(fn [db _] | |
(:filters db))) | |
; Return the peoples, unfiltered | |
(rf/reg-sub | |
:peoples | |
(fn [db _] | |
(:peoples db))) | |
; Return a list of filtered peoples | |
(rf/reg-sub | |
:peoples-filtered | |
(fn [db _] | |
(filter-people (:peoples db) (:filters db)))) | |
; Given a key k, return a vector of values representing all the values present in peoples | |
(rf/reg-sub | |
:values-for-key | |
(fn [db [_ k]] | |
(map #(k %1) (:peoples db)))) | |
; Does filter contains the value k in it? | |
(rf/reg-sub | |
:filter-contains? | |
(fn [db [_ k]] | |
(contains? (set (:filters db)) k))) | |
;; -- 5 - View Function -- | |
;; ----------------------- | |
(defn list-people | |
[] | |
[:p (pr-str @(rf/subscribe [:peoples]))]) | |
(defn list-people-filtered | |
[] | |
[:p (pr-str @(rf/subscribe [:peoples-filtered]))]) | |
(defn list-filter | |
[] | |
[:p (pr-str @(rf/subscribe [:filters]))]) | |
(defn button-add-role | |
[role] | |
[:button | |
{:on-click (fn [e] (rf/dispatch [:add-filter [::role role]]))} | |
(pr-str "Add filter" role)]) | |
(defn button-remove-role | |
[role] | |
[:button | |
{:on-click (fn [e] (rf/dispatch [:remove-filter [::role role]]))} | |
(pr-str "Remove filter" role)]) | |
(defn ressource | |
[data] | |
[:tr | |
[:td (data ::name)] | |
[:td {:style (style-by-date | |
(data ::date-fin-mandat) | |
end-soon-style | |
end-not-so-soon-style | |
end-very-not-soon-style)}(str (data ::date-fin-mandat))] | |
[:td (data ::role)] | |
]) | |
(defn checklist-filter | |
[key1] | |
(let [list-values (set @(rf/subscribe [:values-for-key key1]))] | |
(into | |
[:form | |
[:input | |
{:type "checkbox" | |
:id (str "all-" key1) | |
:name (str "all-" key1) | |
:on-change (fn [e] (if (.. e -target -checked) | |
(rf/dispatch [:add-all-filters-for-key key1 list-values]) | |
(rf/dispatch [:remove-all-filters-for-key key1])))}] | |
[:label {:for (str "all-" key1)} "All"] | |
] | |
(for [value list-values] | |
[:div | |
[:input | |
{:type "checkbox" | |
:id value | |
:name value | |
:on-change (fn [e] (if (.. e -target -checked) | |
(rf/dispatch [:add-filter [key1 value]]) | |
(rf/dispatch [:remove-filter [key1 value]]))) | |
:checked @(rf/subscribe [:filter-contains? [key1 value]]) | |
}] | |
[:label {:for value} value]] | |
)))) | |
(defn peoples-ui-filtered | |
[] | |
(let [pf @(rf/subscribe [:peoples-filtered])] | |
(into | |
[:table | |
[:tr | |
[:th "NAME"] | |
[:th "DATE FIN MANDAT"] | |
[:th "ROLE"]] | |
[:tr | |
[:td [checklist-filter ::name]] | |
[:td ] | |
[:td [checklist-filter ::role]]] | |
] | |
(doall | |
(for [p pf] | |
[ressource p]))))) | |
(defn ui | |
[] | |
[:div | |
[peoples-ui-filtered] | |
]) | |
;; -- Kickstart the application -- | |
;; ------------------------------- | |
(defn ^:export run | |
[] | |
(rf/dispatch-sync [:initialize]) ;; puts a value into application state | |
(reagent/render [ui] ;; mount the application's ui into '' | |
(js/document.getElementById "app"))) | |
(run) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
table.cljs
is a little project with the aim to have visual clue on when colleague will end their missions. The table allow filtering either by 'role' or by 'name'.klipse.html
is the same project, you can paste this code into a file and then open it with a browser to play with live, thanks to Klipse.