• Stars
    star
    351
  • Rank 120,906 (Top 3 %)
  • Language
    Clojure
  • License
    Other
  • Created about 8 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

ClojureScript macros for convenient native Javascript object access.

cljs-oops

GitHub license Clojars Project Travis Sample Project

This is a ClojureScript library providing a few essential macros for operating with native Javascript objects ("oops" stands for "Object OPerationS"). Cljs-oops provides optimizer-safe property and method accessors, compact but efficient nested property accessors, and development-build-only safety checks that catch many common errors.

TOC | Object operations | Installation | Motivation | Benefits | FAQ

Boss: "Ship it!"
You:  "Let me compile it with :advanced optimizations..."
Boss: "Sounds good!"
...one coffee later
You:  "Oops! It just broke! And I don't know why." 
Boss: "Don't tell me that a random person on the Internet was wrong again."
You:  (sad face) "Yep, they provided slightly outdated externs!"

Object operations

Add these new power-macros to your tool belt:

  1. oget is a flexible, safe and guilt-free replacement for aget
  2. oset! is aset on steroids
  3. ocall is a replacement for (.call ...) built on top of oget
  4. oapply is a replacement for (.apply ...) built on top of oget

Let's see some code examples first and then discuss the concepts:

Installation

Integrate with your project

Add oops dependency into your Leiningen's project.clj or boot file.

Clojars Project

Require macros in your namespaces via oops.core ClojureScript namespace:

(ns your.project.namespace
  (:require [oops.core :refer [oget oset! ocall oapply ocall! oapply!
                               oget+ oset!+ ocall+ oapply+ ocall!+ oapply!+]]))

(oset! (js-obj) :mood "a happy camper")

Please note that we are not using :refer-macros here. We rely on automatic macro refer inference in latest ClojureScript.

Also please be aware that oops uses clojure.spec which is available since Clojure 1.9. If you cannot upgrade to Clojure 1.9, you may stick with Clojure 1.8 and add this backported version of clojure.spec.

Otherwise pretty standard stuff. If in doubts, look at the sample project.

Motivation

I don't always do Javascript interops, but when I do, I call them by names.

-- Darwin (with sunglasses on)

ClojureScript developers should quickly learn how to inter-operate with native Javascript objects via the dot operator. This was modelled to closely follow Clojure's Java interop story.

For example, the ClojureScript form (.-nativeProp obj) will compile to obj.nativeProp in Javascript.

It works pretty well during development but there is a catch! When you naively write code like that, it might not survive advanced optimizations. Closure Compiler needs some information about which property names are safe to rename and which cannot be renamed because they might be referenced externally or dynamically via strings.

Someone at Google had a quick and bad idea. We could provide a separate file which would describe this information. Let's call it an "externs file"!

Externs from hell

I'm pretty opinionated about using externs. I hate it with passion. Here is the list of my reasons:

  1. Development behaviour is disconnected from production behaviour - discovering breakages only after switching to :advanced mode. I know, I should continuously run tests against :advanced mode. But :advanced builds are pretty slow and it is no fun to fish for "Cannot read property 'j349s' of null"-kind of errors in minified raw Javascript files which could balloon to multi-MB sizes. Have to wait for quantum computers to provide our IDEs with enough computational power to parse and syntax-highlight multi-megabyte one-line Javascript files ;)

  2. Say, authors of a useful (native) library don't provide externs file (usually simply because they don't use Closure Compiler). So there must come someone else who is willing to maintain an externs file for their library by following changes in the library. You want to use the library so now you made yourself dependent on two sources of truth and they don't usually move in a lock-step. Also that someone will probably sooner or later lose interest in maintaining the externs file and you have no way of telling if it is outdated/incomplete without doing a full code-review. And the worst part is that "someone" is very often you.

  3. Incomplete (or outdated) externs files provide no feedback. Except that you suddenly discover that a new build is broken again and you are back to "pseudo-names fishing".

  4. Externs have to be configured. Paths must be specified. Externs are not co-located with the code they are describing. It is not always clear where individual externs are coming from. Some "default" externs for standard browser/DOM APIs are baked-in Closure Compiler by default which might give you false sense of security or confuse assumptions about how this whole thing works.

Side-stepping the whole externs mess

What if I told you to ditch your externs because there is a simpler way?

Simply use string names to access object properties in Javascript (in cases where you would rely on externs). Instead of (.-nativeProp obj) write (aget obj "nativeProp") which compiles to obj["nativeProp"]. String names are not subject of renaming in advanced mode. And practically the same code runs in development and advanced mode.

I hear you. This looks dirty. We are abusing aget which was explicitly documented to be for native array only. Alternatively we could use goog.object/get or the multi-arity goog.object/getValueByKeys which looks a bit better, but kinda verbose.

Instead of investing your energy into maintaining externs you could as well incrementally write a lightweight Clojure-style wrapper functions to access native APIs by string names directly. For example:

(defn get-element-by-id [id]
  (.call (aget js/document "getElementById") js/document id))

It is much more flexible than externs. You have full control and power of ClojureScript code here. And who knows, maybe later you will extract the code and publish it as a nice ClojureScript wrapper library.

Sounds good? With oops library the situation can be even better. What if we had something like aget but safer and more flexible? I'm pleased to introduce you to oget...

Benefits

Be more expressive with selectors

The signature for oget is (oget obj & selector).

Selector is a data structure describing exact path for traversing into a native object obj. Selectors can be plain strings, keywords or for convenience arbitrarily nested collections of those.

Selectors are pretty flexible. The following selectors describe the same path:

(oget o "k3.?k31.k311")
(oget o "k3" "?k31" :k311)
(oget o ["k3" "?k31" "k311"])
(oget o [["k3"] "?k31"] "k311")
Access modifiers

Please note the ".?" is a modifier for "soft" access (inspired by CoffeeScript's existential operator). We expect that the key 'k31' might not be present and want oget to stop and silently return nil in that case.

In case of oset! you can use so-called "punching" for creation of missing keys on path. For example:

(oset! (js-obj) "!k1.!k2.!k3" "val")

That will create k1 and k2 on the path to setting final k3 key to val. If you didn't specify the exclamation modifiers oset! would complain about missing keys. This makes sense because if you know the path exists for sure you don't want to use punching and that will ultimately lead to simpler code generated in :advanced mode (without any checks for missing keys).

Static vs. dynamic selectors

Dynamic selector is a selector which is not fully known at compile-time. For example result of a function call is a dynamic selector:

(oget o (identity "key"))

At runtime the form result is the same but generated code is less effective. Dynamic selectors should be very rare. By default, oops assumes that you want to prefer static selectors and dynamic selectors are unintentional. Compiler will issue a compile-time warning about "Unexpected dynamic selector usage". To silence this warnings use "plus" version of oget like this:

(oget+ o (identity "key"))

This way you express explicit consent with dynamic selector code-path.

Play it safe during development

By default, oops generates diagnostics code and does pretty intensive safe-checking in non-advanced builds. As you can see on the screenshots above you might get compile-time or run-time warnings and errors when unexpected things happen, like accessing missing keys or traversing non-objects.

Produce efficient barebone code in :advanced builds

By default, all diagnostics code is elided in :advanced builds and oops produces code similar to hand-written aget usage (without any safety-checks).

You can inspect our test compilation transcripts to see what code is generated in different compiler modes.

Tailor oops behaviour

I believe oops has sensible defaults and there should be no need to tweak it under normal circumstances. Anyways, look at possible configuration options in defaults.clj.

As you can see, you can provide your own config overrides in the ClojureScript compiler options map via :external-config > :oops/config. See example in cljs-oops-sample project.

Recommended links

FAQ

Isn't accessing properties by string names slower?

Well, only if the strings are computed dynamically at runtime. In case of string literals Javascript parser can see them and there should be no reason to treat them differently than dot properties. But you don't have to worry about this. Google Closure compiler rewrites string literals to dot property access whenever possible.

Should I use cljs-oops with Closure Library (e.g. goog.something namespace)?

No! Use oops only for interop with external code which is not part of your :advanced build. That means for all code where you would normally need to write externs.

Closure Library is compatible with advanced compilation and identifiers get properly minified during compilation. You don't have to write any externs for it, so you don't have to use oops with it.

Second area where you want to use string names is when you work with JSON data objects (e.g. data received over a network). String names explicitly prevent minification of key names which must stay intact.

For better understanding please read this detailed article by Luke VanderHart.

How this approach compares to ClojureScript externs inference?

Externs inference is very recent feature and looks promising. It was introduced after I put all the effort into developing this library so my opinion is biased :-). I personally still prefer investing time into building light-weight ClojureScript wrapper libraries using string-names than dealing with externs (even if they are auto-inferred).

I would recommend watching Navigating ClojureScript's Fire Swamps by Peter Schuck where he compares both methods.

More Repositories

1

cljs-devtools

A collection of Chrome DevTools enhancements for ClojureScript developers
Clojure
1,107
star
2

dirac

A Chrome DevTools fork for ClojureScript developers
Clojure
769
star
3

chromex

Write Chrome Extensions in ClojureScript
Clojure
411
star
4

totalterminal

Terminal.app plugin for quick access to terminal window (Quake-style)
Objective-C
390
star
5

asepsis

a solution for .DS_Store pollution
C
350
star
6

drydrop

Deploy static sites to App Engine by pushing to GitHub
Python
224
star
7

firelogger.py

Python library for FireLogger - a logger console integrated into Firebug
Python
219
star
8

totalfinder-i18n

Localization for TotalFinder
HTML
184
star
9

firequery

Firebug extension for jQuery development
JavaScript
170
star
10

cljs-devtools-sample

An example integration of cljs-devtools
Clojure
75
star
11

firelogger.php

PHP library for FireLogger - a logger console integrated into Firebug
PHP
68
star
12

xrefresh

Browser refresh automation for web developers
C#
64
star
13

leechgate

Google Analytics for your S3 bucket
PHP
64
star
14

firelogger

Firebug logging support for server side languages/frameworks (Python, PHP)
JavaScript
61
star
15

firerainbow

Javascript syntax highlighting for Firebug
JavaScript
58
star
16

totalfinder-osax

Scripting additions used by TotalFinder (SIMBL replacement)
Objective-C++
55
star
17

chromex-sample

An example Chrome extension using Chromex library (ClojureScript)
Clojure
53
star
18

cljs-zones

A magical binding macro which survives async calls
Clojure
43
star
19

totalfinder-kext

The kernel extension used by TotalFinder for .DS_Store redirection [obsolete]
C
40
star
20

totalspaces2-api

API bindings to enable you to get information from and to control TotalSpaces2
HTML
32
star
21

env-config

A Clojure(Script) library for config map overrides via environmental variables
Clojure
29
star
22

restatic

Google Docs content parser
CoffeeScript
24
star
23

terminal-profiles

Terminal.app profiles contributed by the community
21
star
24

crashwatcher

Agent process which monitors crash logs and offers sending a crash report
Objective-C
21
star
25

totalfinder-web

Web site for TotalFinder - the best Finder plugin around
HTML
18
star
26

dirac-sample

An example integration of Dirac DevTools
Clojure
17
star
27

cljs-react-three-fiber

ClojureScript port of react-three-fiber/examples.
JavaScript
17
star
28

totalterminal-osax

SIMBL replacement for TotalTerminal (Visor)
Objective-C++
16
star
29

totalspaces-api

API bindings to enable you to get information from and to control TotalSpaces
HTML
16
star
30

totalspaces-osax

Scripting additions used by TotalSpaces (SIMBL replacement)
Objective-C
12
star
31

fscript-osax

Simple way how to inject F-Script into any app from the command-line (replacement for F-Script Anywhere and F-Script SIMBL)
Objective-C
12
star
32

electrum-server-docker

Run your own electrum-server+bitcoind easily, isolated by docker
Shell
12
star
33

totalfinder-installer

A support project for TotalFinder - installer scripts
Shell
11
star
34

bitcoind-docker

A docker container for running bitcoind node
Shell
10
star
35

totalspaces2-display-manager

Small app to manage which display spaces are on
Objective-C
9
star
36

blog

Ideas from BinaryAge
HTML
7
star
37

uninstaller

Minimalist Cocoa app for wrapping TotalFinder uninstall script
Objective-C++
6
star
38

totalspaces2-i18n

TotalSpaces2 Internationalization
HTML
6
star
39

holycrash-osax

Crash any Mac application using an AppleScript event (for debugging purposes)
Objective-C
6
star
40

discourse-hipchat-plugin

A plugin for reporting discourse activity into a HipChat room.
Ruby
5
star
41

layouts

Jekyll layouts for main site
5
star
42

clearcut

Unified logging overlay on top of console.log and clojure.tools.logging (WIP)
Clojure
5
star
43

totalspaces-i18n

Localization for TotalSpaces
Ruby
4
star
44

totalfinder-tests

automated scripts to test TotalFinder (powered by Sikuli)
Python
4
star
45

totalterminal-i18n

TotalTerminal internationalization
Ruby
4
star
46

totalspaces-web

the web site for TotalSpaces - a set of handy tweaks for OS X Spaces
HTML
4
star
47

asepsis-web

the website for the Asepsis project
HTML
4
star
48

instaedit

JavaScript
4
star
49

totalterminal-installer

Installer-related files for TotalTerminal
Shell
4
star
50

php-ga

NO LONGER IN USE
PHP
4
star
51

site

An umbrella project for our web sites at binaryage.com
Ruby
4
star
52

totalterminal-web

Website for TotalTerminal
HTML
3
star
53

shared

Shared static content for binaryage sites
Stylus
3
star
54

hints

Your feed of Mac-related hints and tips from BinaryAge
HTML
3
star
55

www

BinaryAge home page
HTML
3
star
56

totalspaces2-desktopname

TotalSpaces2 plugin to display the name of the current space in the menu bar
Objective-C
3
star
57

restatic-web

Regenerator of static websites with fresh data from Google spreadsheets
CSS
3
star
58

cljs-oops-sample

An example integration of cljs-oops
Clojure
2
star
59

badev

A command-line tool to aid developers in BinaryAge
Ruby
1
star
60

support

BinaryAge support page
HTML
1
star
61

crash-prism

Symbolizing crash reports with ease (a custom tool for BinaryAge apps)
Ruby
1
star