Skip to content

Instantly share code, notes, and snippets.

@puppybits
Last active November 17, 2017 23:19
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save puppybits/833833f245051f5cd032a5c0e0bbef3f to your computer and use it in GitHub Desktop.
Save puppybits/833833f245051f5cd032a5c0e0bbef3f to your computer and use it in GitHub Desktop.
Om Next in JS

Intro

There's a big learning curves getting into Om Next. It's even hard for those who don't know ClojureScript. Most developers see a Lisp and are instantly turned off or annoyed at the syntax before even stopping to ask why or figure out the trade-offs with a lisp..

In order to focus on the patterns over the langauge, this is a rough translation about the workflow and patterns in Om Next. We should first focus on the core of what Om Next does, then why it uses certian patterns and how they work. Lastly is what does it gain by being in written ClojureScript.

Installation

Create the project through a template in the cli.

javascript:

yo devcards my_project
cd my_project
open package.json
# add "devcards-om-next":"0.3.0" in "dependancies"
npm install
npm run start

clojurescript:

lein new devcards my_project
cd my_project
open project.clj
# add [devcards-om-next "0.3.0"] in :dependancies
lein deps
lein fighweel

Figwheel boots up a HMR webserver. Devcards is an interactive environment similar to React Storyboards. Once fighweel is loaded open http://localhost:3449. When you save a file the code will be hot loaded in the browser.

If all you want is basic components with no Redux then you can stop here. Om Next passes props just like React.

import {h1} from "om/dom";
import {defui} as om from "om/next";

// Om Next components are just vanilla React components
class Headline extends defui {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

And in ClojureScript:

; cljs component looks very similar. 
; The trailing parentheses are maintained by an editor plugin. 
(defui Headline
  Object
  (render [this]
    (h1 nil (str "Hello, " (:name (om/props this))))))
     ;^ h1 is the element
     ;    ^ nil means we won't pass any props. Props would look like {:style {:color "red"}}
     ;                               ^ use om/props to get the React props and then get the value for the "name" key

Om Next uses figwheel for HMR and DevCards for an interactive environment like React Storyboards.

import {omNextRoot} from "devcardsOmNext/core";
// The function below is called `defcard` because Clojure  
// defines functions and variables with "def" & "defn".
import {startDevCardUi, defCard} as devcards from "devcards/core";  

import Headline from "index";

// run the bootstrap for dev cards
startDevCardUi();

// Create a card to render a series of components
class BasicCard extends defCard {
  constructor(){
    // This is just like React.render()
    // The second argument is the data for om next's state.
    omNextRoot(Headline, {name: "ClojureScript!"});
  }
}

ClojureScript:

(defcard basic-card
  (om-next-root 
    Headline
    {:name "ClojureScript!"}))

This is the equivalent of a simple Redux store in Om Next.

import om from "om/next";
import {omNextRoot} from "devcardsOmNext/core";
import {defCard} from "devcards/core"; 
import Headline from "index";

// data to initialize om next
const myData = {name: "ClojureScript!"};

// All read functions return a object with a key of "value" for the data to be returned.
const myReader = function(){ 
  return {value: myData}; 
};

// Create an instance of the Om Next parser
const myParser = om.parser({read: myReader});


class OmNextReconciler extends defCard {
  constructor(){
    /* Instead of just data we'll create a reconciler to manage.
       At it's more basic the parser is just a single function to
       read from the data. In a larger app each read is just a single
       function that's namespaced.
    */
    omNextRoot(
      Headline, 
      om.reconciler({
          state: data, 
          parser: myParser
        }
      )
    );
  }
}

ClojureScript:

(def my-data 
  {:name "ClojureScript!"})

(def my-parser 
  (om/parser 
    {:read (fn [] {:value my-data})}))

(defcard om-next-reconciler
  (om-next-root 
    Headline
    (om/reconciler
      {:state  data
       :parser my-parser})))

Read and Writes in Om Next. Again similar to Redux.

import om from "om/next";
import {omNextRoot} from "devcardsOmNext/core";
import {defCard} from "devcards/core"; 
import Headline from "index";

const myData = {name: "ClojureScript!"};

const myReader = function({state}, key, params){ 
  return {value: state[key]}; 
};

// mutations are how the state gets changed in Om Next.
const myMutations = function({state}, key, params){
  /* Both `key` and `params` are from the callee.
     The first param is `env`. Basically we just want a reference to the state for now. 
     In our case `key` will be "name" because we only have
     one key in our state. `params` is whatever you want to send in. I usaully use `{value: myStuff}`.
  */
  
  /* `value` should return which keys should be rerendered after the mutation is applied
     `action` returns a thunk to apply the state change.
     Om Next is synchronous to remove issues with async. Async is isolated to remote code. 
     Action thunks will all be executed together then the reads are called.
  */
  return {
    value:  {keys: ["name"]},
    action: () => ( state["name"] = params.value ) // params is an object from the callee. I used `value`.
  };
};
const myParser = om.parser({
  read: myReader,
  mutate: myMutations
});


class OmNextReconciler extends defCard {
  constructor(){
    omNextRoot(Headline, om.reconciler({state: data, parser: myParser}));
  }
}

JavaScript without comments:

import om from "om/next";
import {omNextRoot} from "devcardsOmNext/core";
import {defCard} from "devcards/core"; 
import Headline from "index";

const myData = {name: "ClojureScript!"};

const myReader = ({state}, key, params) => ({value: state[key]});

const myMutations = ({state}, key, params) => ({
  value:  {keys: ["name"]},
  action: () => { state["name"] = params.value }
});

const myParser = om.parser({
  read: myReader,
  mutate: myMutations
});


class OmNextReconciler extends defCard {
  constructor(){
    omNextRoot(Headline, om.reconciler({state: data, parser: myParser}));
  }
}

ClojureScript:

(def my-data 
  ; we switch to an atom because it's the only mutatable data in clojure
  (atom {:name "ClojureScript!"})) 

(defn my-reader
  [{:keys [state]} key params] 
  {:value (get state key)})

(defn my-mutations
  [{:keys [state]} key params]
  {:value  {:keys [:name]}  ; reread the name key in the state
   :action (fn [] (swap! state update :name (get params :value)))}) ; swap! is a function to chage an atom. 

(def my-parser 
  (om/parser 
    {:read   my-reader
     :mutate my-mutations}))

(defcard om-next-reconciler
  (om-next-root 
    Headline
    (om/reconciler
      {:state  data
       :parser my-parser})))

TODO - write transact!. Transacts are like message in Redux.

TODO - write a read query example. Instead of Redux connectors, Om Next lets you write a simple object about what data it wants. If you don't want/need the you can just use the read functions to get data.

TODO - idents are not really needed. They help to take a basic object with arrays and make it more like a database so that getting entities from a unique key is easier and doesn't have duplicate or out of sync data.

TODO - write remote for non-in-memory (network, or local db) data. Remotes isolate all the side-effects into a isolated place in your code base.

TODO - How to do offline and optomistic commits. This handles what Relay would in the JS world. If you don't need Relay-like functionality then you just don't use tempids.

TODO - how to use all the reads and mutate calls in the server. This gives us really good code reuse. It's not needed but it's helpful that Om Next is very versitile to handle these different needs without having a lot of custom framworks and stacks.

TODO - how you can send transact messages to a CQRS-like backend. This reuses all the read/mutates but changes the remote to persist in a database. Messages from OmNext can get sent directly (via transit) so there's no manual (de)serialization but all data types (including dates and custom types) are preservered.

TODO - wrap up with just a basic example using 2 componets w/ queries and reconciler w/ read/writes, a transact that sends to a remote. Queries are super useful at getting data. Won't use idents, database normalization, or anything more than a basic REST backend. Explain "code is data" in Clojure and why it's important at framing problems. Explain immutable by default and the benefits of explict (and enforced) data flow with immutatable.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment