React.js based plugin for LightTable

LightTable made quite a big fuzz some time ago. Now original authors are working fulltime on Eve, while LightTable is now more driven  by community members. And they're doing some pretty interesting work like switching to Atom Shell instead of node-webkit.

I like the idea that text editor is a browser. It gives you ability to do literally anything you want. So I decided to integrate React.js into LightTable plugins ecosystem. One of the most interesting things in latest release is User plugin. It is a place to configure your editor not just by tweaking settings and changing keybinding. You can add new features right there without need to create separate plugin. It is much like .vimrc or .emacs/init.el files. This turns process of configuring your editor into development task.

First let's add User plugin to workspace. Start LightTable and run command Settings: Add User plugin to workspace, open workspace panel (Workspace: toggle workspace tree command). Now you can see User dir with a some files added already.

Let's open user.behaviour file. It is a place to actually change settings and add or remove hooks on different events LightTable and plugins provide. We need to add custom javascript file with React.js and make sure User plugin is loaded too.

[
  [:app :lt.objs.plugins/load-js "/Users/brabadu/react/react.min.js"]
  [:app :lt.objs.plugins/load-js "user_compiled.js"]
  ...
]

Let's open User/src/lt/plugins/user.cljs. It already has an example that shows how to make objects and open new tabs. We'll tweak this example to use React.js.

First we need to define a namespace and dependencies
(ns lt.plugins.user
  (:require [clojure.string :as string]
            [lt.object :as object]
            [lt.objs.tabs :as tabs]
            [lt.objs.command :as cmd])
  (:require-macros [lt.macros :refer [defui behavior]]))

All LightTable plugins are in lt.plugins.* namespace. As you can see, we'll be working with LightTable object, commands and tabs. Next, we'll define couple of helper functions

(defn format-time [d]
  (first (string/split (.toTimeString d) #" ")))

(def el React/createElement)

format-time is for pretty-printing time from javascript Date object. And el is simply an alias to React.createElement.

(def label
  (React/createClass
     #js {:displayName "TimerLabel"
          :render (fn [] 
             (this-as this
                      (el "h1" nil (str "Timer: "
                                        (-> this .-props .-time)))))}))

Our component, that is going to show whole React.js-LightTable integration. I guess nothing stops us from using om/reagent/quiescent/etc. Add it's dependency in project.clj and you're good to go. Our example is simple enough not to bring anything more than bare React.

Interesting thing is how javascript this works. Calling macro this-as with symbol to which javascript this bounds is idiomatic way to access it. Most of ClojureScript libs are hiding this pattern in their innards, so you'll need it mostly when doing interop with javascript.

(defui react-panel [this]
  [:h1 {:id "app-root"} "React + LightTable!"])

(object/object* ::user.react-timer
                :tags [:user.hello]
                :behaviors [::on-close-timer-destroy]
                :init (fn [this]
                        (react-panel this)))

(def react-timer (object/create ::user.react-timer))

Here we define LightTable (not React.js) ui component react-panel and object prototype ::user.react-timer. LightTable has it's own notion of object, that it uses to implement BOT principle. When the object react-timer is created function that is passed with :init is executed.

(cmd/command {
  :command :user.show-time
  :desc "User: Show time"
  :exec (fn []
    (tabs/add-or-focus! react-timer)
    (let [app-root (.getElementById js/document "app-root")
          label-inst (React/render 
                        (el label #js {:time (format-time (js/Date.))} [])
                        app-root)
          refresh-timer (fn []
                   (prn (format-time (js/Date.)))
                   (.setProps label-inst #js {:time (format-time (js/Date.))}))
          interval (js/window.setInterval refresh-timer 1000)]
       (object/merge! react-timer {:interval interval})))})

This code adds new command to LightTable. It creates new tab, then instantiates React.js component and mounts it on rendered by react-panel node.

After that defines refresh-timer function, that updates props in React component. It is called every second as a callback of js/window.setInterval. We could save timer ID in React component's state, but I decided to put it in object, so our component could be simple and it's only purpose would be view layer, while all the business-logic lies separately.

(behavior ::on-close-timer-destroy
          :triggers #{:close}
          :reaction (fn [this]
                      (js/window.clearTimeout (:interval @this))
                      (object/raise this :destroy)))

Now we're taking care cleaning on our tab close event. This is why I added prn to refresh-timer function - to see if it stopped being called by setInterval.



That's it. Code is a bit messy, but hope everything is clear. All code in one place is here

No comments:

Post a Comment

Thanks for your comment!
Come back and check response later.