Skip to content

Instantly share code, notes, and snippets.

@edbond
Created June 30, 2014 14:13
Show Gist options
  • Save edbond/a57c635b86be56c831da to your computer and use it in GitHub Desktop.
Save edbond/a57c635b86be56c831da to your computer and use it in GitHub Desktop.
marionette contacts editor in om
;; src/contacts/core.cljs
(ns contacts.core
(:require [secretary.core :as secretary
:include-macros true :refer [defroute]]
[goog.events :as events]
[om.core :as om :include-macros true]
[sablono.core :as html :refer-macros [html]]
[alandipert.storage-atom :refer [local-storage]]
[domina :refer [set-styles! by-id value]]
[domina.events :refer (listen!)]
[cljs-uuid-utils])
(:import goog.History
goog.history.EventType))
(enable-console-print!)
(secretary/set-config! :prefix "#")
(defn uuid []
(cljs-uuid-utils/make-random-uuid))
;; Quick and dirty history configuration.
(let [h (History.)]
(goog.events/listen h EventType/NAVIGATE #(secretary/dispatch! (.-token %)))
(doto h (.setEnabled true)))
(def contacts (local-storage (atom []) :contacts))
;; top menu
(def active-menu (atom "Contacts"))
(defroute contacts-path "contacts" []
(reset! active-menu "Contacts"))
(defroute about-path "about" []
(reset! active-menu "About"))
(defn top-menu [state]
(om/component
(html [:div.navbar-collapse.collapse
[:ul.nav.navbar-nav
[:li {:class (if (= state "Contacts") "active")}
[:a {:href "#contacts"} "Contacts"]]
[:li {:class (if (= state "About") "active")}
[:a {:href "#about"} "About"]]]])))
(defn filter-contacts [query]
(prn "FILTER" query)
(swap! contacts
(fn [cs]
(mapv #(assoc % :visible
(or (= query "")
(> (.indexOf (.toLowerCase (:first-name %))
(.toLowerCase query)) -1)))
cs))))
(defn hide-dialog []
(set-styles! (by-id "shade") {:display "none"})
(set-styles! (by-id "dialog") {:display "none"}))
(defn add-contact [contact]
(swap! contacts conj (assoc contact :uuid (str (uuid))))
(hide-dialog)
(filter-contacts ""))
(defn replace-contact-by-uuid [uuid updated-contact old-contact]
(if (= (:uuid old-contact) uuid)
(-> updated-contact
(assoc :first-name (:new-first-name updated-contact))
(dissoc :new-first-name))
old-contact))
(defn display-dialog [widget state]
(prn "display dialog")
(set-styles! (by-id "shade") {:display "block"})
(let [d (set-styles! (by-id "dialog") {:display "block"})]
(om/root widget state {:target d})))
(defn update-contact [updated-contact]
(swap! contacts
(fn [cs]
(map (partial replace-contact-by-uuid
(:uuid updated-contact) updated-contact)
cs)))
(hide-dialog)
(filter-contacts ""))
(defn edit-contact [state]
(om/component
(html [:div.panel.panel-default
[:div.panel-body
[:h2 "New Contact"]
[:hr]
[:div.form-group
(html/label "first-name" "First Name:")
[:input {:name "first-name"
:class "form-control"
:on-change #(om/update! state [:new-first-name]
(value (.-target %)))
:on-key-down #(if (= 13 (.-keyCode %))
(update-contact @state))
:value (:first-name state)}]]
[:div.form-group
[:button {:class "btn"
:on-click #(hide-dialog)} "Cancel"]
[:button {:class (str "btn btn-primary pull-right"
(if (= "" (:first-name state)) " disabled"))
:on-click #(when (not= (:new-first-name @state)
(:first-name @state))
(update-contact @state))} "Update"]]]])))
(defn new-contact [state]
(om/component
(html [:div.panel.panel-default
[:div.panel-body
[:h2 "New Contact"]
[:hr]
[:div.form-group
(html/label "first-name" "First Name:")
[:input {:name "first-name"
:class "form-control"
:on-change #(om/update! state [:first-name]
(value (.-target %)))
:on-key-down #(if (= 13 (.-keyCode %))
(add-contact @state))
:value (:first-name state)}]]
[:div.form-group
[:button {:class "btn"
:on-click #(hide-dialog)} "Cancel"]
[:button {:class "btn btn-primary pull-right"
:on-click #(when-not (= "" (:first-name @state))
(add-contact @state))} "Create"]]]])))
(def filter-state (local-storage (atom nil) :filter))
(defn new-and-filter [state]
(om/component
(html [:div.container
[:div.row
[:div.col-md-4
[:button {:class "btn btn-primary"
:on-click #(display-dialog new-contact {})} "New Contact"]]
[:div.col-md-4]
[:div.col-md-4
[:input {:name "filter"
:class "form-control"
:placeholder "Filter"
:on-change (fn [e]
(filter-contacts (value (.-target e))))
:value @filter-state}]]]])))
(defn remove-contact [contact state]
(let [uuid (:uuid @contact)]
(filterv #(not= uuid (:uuid %)) state)))
(defn contacts-table [state]
(om/component
(html
[:div.container
[:h2 "Contacts"]
[:table.table
[:thead
[:tr
[:th "First Name"]
[:th "Edit"]
[:th "Delete"]]]
[:tbody
(for [contact state
:when (:visible contact)]
[:tr
[:td (:first-name contact)]
[:td
[:button {:class "btn btn-default"
:on-click #(display-dialog edit-contact contact)} "Edit"]]
[:td
[:button {:class "btn btn-danger"
:on-click #(om/transact! state (partial remove-contact contact))}
"Delete"]]])]]])))
(om/root top-menu active-menu
{:target (by-id "nav")})
(om/root new-and-filter filter-state
{:target (by-id "new-and-filter")})
(om/root contacts-table contacts
{:target (by-id "contacts")})
(hide-dialog)
(filter-contacts "")
(listen! js/document :keydown
(fn [e]
(let [kc (-> e .-evt .-keyCode)]
(if (= kc 27)
(hide-dialog)
false))))
<html>
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Contacts</title>
<link rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap.min.css">
<link rel="stylesheet"
href="http://netdna.bootstrapcdn.com/bootstrap/3.1.1/css/bootstrap-theme.min.css">
<style type="text/css">
#shade {
width: 100%;
height: 100%;
position: fixed;
left: 0; right: 0; top: 0; bottom: 0;
background-color: #eee;
opacity: 0.5;
}
#dialog {
z-index: 10;
position: absolute;
left:0; right:0;
margin-left: auto; margin-right: auto;
max-width: 300px;
top: 120px;
}
</style>
</head>
<body>
<nav class="navbar navbar-default" role="navigation">
<div class="container">
<!-- Brand and toggle get grouped for better mobile display -->
<div class="navbar-header">
<a class="navbar-brand" href="#contacts">Contacts</a>
</div>
<!-- Collect the nav links, forms, and other content for
toggling -->
<div id="nav">
</div><!-- /.navbar-collapse -->
</div><!-- /.container-fluid -->
</nav>
<div id="new-and-filter">
</div>
<div id="contacts">
</div>
<div id="shade">
</div>
<div id="dialog">
</div>
<!-- <script src="http://fb.me/react-0.9.0.js"></script> -->
<!-- <script src="out/goog/base.js" type="text/javascript"></script> -->
<script src="contacts.js" type="text/javascript"></script>
<!-- <script type="text/javascript">goog.require("contacts.core");</script> -->
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment