GithubHelp home page GithubHelp logo

js-interop's Introduction

js-interop latest release tests passing?

A JavaScript-interop library for ClojureScript.

Features

  1. Operations that mirror behaviour of core Clojure functions like get, get-in, assoc!, etc.
  2. Keys are parsed at compile-time, and support both static keys (via keywords) and compiler-renamable forms (via dot-properties, eg .-someAttribute)

Quick Example

(ns my.app
  (:require [applied-science.js-interop :as j]))

(def o #js{ …some javascript object… })

;; Read
(j/get o :x)
(j/get o .-x "fallback-value")
(j/get-in o [:x :y])
(j/select-keys o [:a :b :c])

(let [{:keys [x]} (j/lookup o)] ;; lookup wrapper
  ...)

;; Destructure
(j/let [^:js {:keys [a b c]} o] ...)
(j/fn [^:js [n1 n2]] ...)
(j/defn my-fn [^:js {:syms [a b c]}])

;; Write
(j/assoc! o :a 1)
(j/assoc-in! o [:x :y] 100)
(j/assoc-in! o [.-x .-y] 100)

(j/update! o :a inc)
(j/update-in! o [:x :y] + 10)

;; Call functions
(j/call o :someFn 42)
(j/apply o :someFn #js[42])

(j/call-in o [:x :someFn] 42)
(j/apply-in o [:x :someFn] #js[42])

;; Create
(j/obj :a 1 .-b 2)
(j/lit {:a 1 .-b [2 3 4]})

Installation

;; lein or boot
[applied-science/js-interop "..."]
;; deps.edn
applied-science/js-interop {:mvn/version "..."}

Motivation

ClojureScript does not have built-in functions for cleanly working with JavaScript objects without running into complexities related to the Closure Compiler. The built-in host interop syntax (eg. (.-theField obj)) leaves keys subject to renaming, which is a common case of breakage when working with external libraries. The externs handling of ClojureScript itself is constantly improving, as is externs handling of the build tool shadow-cljs, but this is still a source of bugs and does not cover all cases.

The recommended approach for JS interop when static keys are desired is to use functions in the goog.object namespace such as goog.object/get, goog.object/getValueByKeys, and goog.object/set. These functions are performant and useful but they do not offer a Clojure-centric api. Keys need to be passed in as strings, and return values from mutations are not amenable to threading. The goog.object namespace has published breaking changes as recently as 2017.

One third-party library commonly recommended for JavaScript interop is cljs-oops. This solves the renaming problem and is highly performant, but the string-oriented api diverges from Clojure norms.

Neither library lets you choose to allow a given key to be renamed. For that, you must fall back to host-interop (dot) syntax, which has a different API, so the structure of your code may need to change based on unrelated compiler issues.

The functions in this library work just like their Clojure equivalents, but adapted to a JavaScript context. Static keys are expressed as keywords, renamable keys are expressed via host-interop syntax (eg. .-someKey), nested paths are expressed as vectors of keys. Mutation functions are nil-friendly and return the original object, suitable for threading. Usage should be familiar to anyone with Clojure experience.

Reading

Reading functions include get, get-in, select-keys and follow Clojure lookup syntax (fallback to default values only when keys are not present)

(j/get obj :x)

(j/get obj :x default-value) ;; `default-value` is returned if key `:x` is not present

(j/get-in obj [:x :y])

(j/get-in obj [:x :y] default-value)

(j/select-keys obj [:x :z])

get and get-in return "getter" functions when called with one argument:

(j/get :x) ;; returns a function that will read key `x`

This can be useful for various kinds of functional composition (eg. juxt):

(map (j/get :x) some-seq) ;; returns item.x for each item

(map (juxt (j/get :x) (j/get :y)) some-seq) ;; returns [item.x, item.y] for each item

To cohere with Clojure semantics, j/get and j/get-in return nil if reading from a nil object instead of throwing an error. Unchecked variants (slightly faster) are provided as j/!get and j/!get-in. These will throw when attempting to read a key from an undefined/null object.

The lookup function wraps an object with an ILookup implementation, suitable for destructuring:

(let [{:keys [x]} (j/lookup obj)] ;; `x` will be looked up as (j/get obj :x)
  ...)

Destructuring

With j/let, j/defn and j/fn, opt-in to js-interop lookups by adding ^js in front of a binding form:

(j/let [^js {:keys [x y z]} obj  ;; static keys using keywords
        ^js {:syms [x y z]} obj] ;; renamable keys using symbols
  ...)

(j/fn [^js [n1 n2 n3 & nrest]] ;; array access using aget, and .slice for &rest parameters
  ...)

(j/defn my-fn [^js {:keys [a b c]}]
  ...)

The opt-in ^js syntax was selected so that bindings behave like regular Clojure wherever ^js is not explicitly invoked, and js-lookups are immediately recognizable even in a long let binding. (Note: the keyword metadata ^:js is also accepted.)

^js is recursive. At any depth, you may use ^clj to opt-out.

Mutation

Mutation functions include assoc!, assoc-in!, update!, and update-in!. These functions mutate the provided object at the given key/path, and then return it.

(j/assoc! obj :x 10) ;; mutates obj["x"], returns obj

(j/assoc-in! obj [:x :y] 10) ;; intermediate objects are created when not present

(j/update! obj :x inc)

(j/update-in! obj [:x :y] + 10)

Host-interop (renamable) keys

Keys of the form .-someName may be renamed by the Closure compiler just like other dot-based host interop forms.

(j/get obj .-x) ;; like (.-x obj)

(j/get obj .-x default) ;; like (.-x obj), but `default` is returned when `x` is not present

(j/get-in obj [.-x .-y])

(j/assoc! obj .-a 1) ;; like (set! (.-a obj) 1), but returns `obj`

(j/assoc-in! obj [.-x .-y] 10)

(j/update! obj .-a inc)

Wrappers

These utilities provide more convenient access to built-in JavaScript operations.

Array operations

Wrapped versions of push! and unshift! operate on arrays, and return the mutated array.

(j/push! a 10)

(j/unshift! a 10)

Function operations

j/call and j/apply look up a function on an object, and invoke it with this bound to the object. These types of calls are particularly hard to get right when externs aren't available because there are no goog.object/* utils for this.

;; before
(.someFunction o 10)

;; after
(j/call o :someFunction 10)
(j/call o .-someFunction 10)

;; before
(let [f (.-someFunction o)]
  (.apply f o #js[1 2 3]))

;; after
(j/apply o :someFunction #js[1 2 3])
(j/apply o .-someFunction #js[1 2 3])

j/call-in and j/apply-in evaluate nested functions, with this bound to the function's parent object.

(j/call-in o [:x :someFunction] 42)
(j/call-in o [.-x .-someFunction] 1 2 3)

(j/apply-in o [:x :someFunction] #js[42])
(j/apply-in o [.-x .-someFunction] #js[1 2 3])

Object/array creation

j/obj returns a literal js object for provided keys/values:

(j/obj :a 1 .-b 2) ;; can use renamable keys

j/lit returns literal js objects/arrays for an arbitrarily nested structure of maps/vectors:

(j/lit {:a 1 .-b [2 3]})

j/lit supports unquote-splicing (similar to es6 spread):

(j/lit [1 2 ~@some-sequential-value])

~@ is compiled to a loop of .push invocations, using .forEach when we infer the value to be an array, otherwise doseq.

Threading

Because all of these functions return their primary argument (unlike the functions in goog.object), they are suitable for threading.

(-> #js {}
    (j/assoc-in! [:x :y] 9)
    (j/update-in! [:x :y] inc)
    (j/get-in [:x :y]))

#=> 10

Core operations

arguments examples
j/get [key]
[obj key]
[obj key not-found]
(j/get :x) ;; returns a getter function
(j/get o :x)
(j/get o :x :default-value)
(j/get o .-x)
j/get-in [path]
[obj path]
[obj path not-found]
(j/get-in [:x :y]) ;; returns a getter function
(j/get-in o [:x :y])
(j/get-in o [:x :y] :default-value)
j/select-keys [obj keys] (j/select-keys o [:a :b :c])
j/assoc! [obj key value]
[obj key value & kvs]
(j/assoc! o :a 1)
(j/assoc! o :a 1 :b 2)
j/assoc-in! [obj path value] (j/assoc-in! o [:x :y] 100)
j/update! [obj key f & args] (j/update! o :a inc)
(j/update! o :a + 10)
j/update-in! [obj path f & args] (j/update-in! o [:x :y] inc)
(j/update-in! o [:x :y] + 10)

Destructuring forms

example
j/let (j/let [^:js {:keys [a]} obj] ...)
j/fn (j/fn [^:js [a b c]] ...)
j/defn (j/defn [^:js {:syms [a]}] ...)

Tests

To run the tests:

yarn test;

Patronage

Special thanks to NextJournal for supporting the maintenance of this library.

js-interop's People

Contributors

benswift avatar dependabot[bot] avatar mhuebert avatar mk avatar mtruyens avatar oconn avatar prestancedesign avatar rgkirch avatar royalaid avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

js-interop's Issues

js-delete feature request

It would be great if there was something like j/delete for doing this:

(fn [x] (js-delete x "some-key") x)

So then you can do updates like this:

(j/update-in! some-object [:coordinate] j/delete! :x)

Destructuring macros

I've added some new destructuring macros in the destruct branch.

There are new variants of let, fn and defn which let one opt-in to object/array access by adding a ^js hint on destructuring forms.

Explanation/example: https://dev.maria.cloud/gist/1852d9fe0f67c339517b684f06020101?eval=true

You can try it via a git dep:

{appliedscience/js-interop 
  {:git/url "https://github.com/appliedsciencestudio/js-interop"
   :sha "9927230e238cd3dd044f8a9f00059ab9e92f19a7"}}

Feedback is welcome.

Error getting length of a string

Works on arrays:

(.-length #js [])
=> 0
(j/get #js [] :length)
=> 0

Fails on strings:

(.-length "1")
; => 1

(j/get "1" :length)
; => #object[TypeError TypeError: Cannot use 'in' operator to search for 'length' in 1]

Support for CLJC

Hey Matt 👋️

I often use js-interop in .cljc files. These files are mostly front-end UI component files in CLJC to support SSR. The UI components have lifecycle methods defined that only run in js-land. However, because we're dealing with CLJC I find myself having to wrap requires and every call with #?(:cljs ...) reader conditionals. This gets ugly quickly.

Any ideas how to get around this?

I like Roman's approach in UIX, where he implemented either no-op or analog function in clj
https://github.com/roman01la/uix/blob/master/core/src/uix/core/alpha.cljc#L271-L280

Call a function in a nested key?

I have a javacript object name myObj that looks like this:

{
  foo: 
      { bar: x => x }
}

And I want to call myObj.foo.bar(42).

How can I do that with js-interop?

Destructuring nested array returns nil

e.g.

(def cljs-foo {:a [1 2 3 4]})
(def js-foo #js {:a [1 2 3 4]})
(j/let [^:js {a :a} js-foo] a) => [1 2 3 4]

(let [{[first second] :a} cljs-foo] first) => 1
(j/let [^:js {[first second] :a} js-foo] first) => nil

Great library, btw. I am curious why the ^:js metadata is needed to activate destructuring, as it is a bit cumbersome. Is there a reason to use j/let otherwise?

a shorter namespace for convenience?

Currently it's quite difficult to remember the whole name of the package and namespace since it's quite long...

[appliedscience/js-interop "0.1.20"]
[applied-science.js-interop :as j]

Do you think it's okay to change to a shorter name, as a common used library for cljs community? For example:

[js-interop "0.1.20"]
[js-interop.core :as j]

How can I translate this js code into cljs code?

I want to translate the following js code into the cljs code using your js-interop library.

function myRenderer(instance, td, row, col, prop, value, cellProperties) {
    Handsontable.renderers.TextRenderer.apply(this, arguments);
    td.className = 'name'
};

Could you tell me how I can do this using your library?

j/get-in macro v.s j/get-in function

Hey, thanks for making this awesome library. I'm trying to understand how it works under the hood.

Take j/get-in as an example. There is a macro version

and a cljs function version:

The thing that puzzles me is: to me the cljs function version looks not necessary, because when I call (j/get-in foo [:bar :baz]) in my application code, it would macro-expand to a series of calls to cljs.core/unchecked-get at compile time.

Am I missing something?

compile error with `j/lit` and fn value

The following code produces a compile error Parameter declaration cljs.core/array should be a vector as it's trying to convert the argument vector into a js array:

(require '[applied-science.js-interop :as j])
(j/lit {:key (fn [])})

Examples that show the Javascript equivalent

Great library! Really appreciate its existence.

As someone who continues to struggle with the mental model of Javascript vs Clojurescript, it would be very helpful if the js-interop examples in the readme and the cljdoc would also show the javascript equivalent.

Thanks

Bug in 2-arity `get-in`: Don't know how to create ISeq from: clojure.lang.Symbol when the keys are coming from a var

Steps to reproduce:

Type to CLJS REPL:

cljs.user> (def o #js {"foo" 1})
;; => #'cljs.user/o
cljs.user> (def ks [:foo])
;; => #'cljs.user/ks
cljs.user> (j/get-in o ks)
Compile Exception: Don't know how to create ISeq from: clojure.lang.Symbol  

Expected: (j/get-in o ks) to return 1

Actual: Exception is thrown.

Note that the following works just fine:

cljs.user> (def o #js {"foo" 1})
;; => #'cljs.user/o
cljs.user> (j/get-in o [:foo])
;; => 1
cljs.user> (def o #js {"foo" 1})
;; => #'cljs.user/o
cljs.user> (def ks [:foo])
;; => #'cljs.user/ks
cljs.user> (j/get-in o ks "not found, but the 3-arity version works just fine")
;; => 1

ClojureScript as provided dependency

Hello,

is it possible to declare ClojureScript as a provided dependency in this project? If I have shadow-cljs and js-interop as project dependencies for example, I generally want the ClojureScript version from shadow-cljs, and not the one from a library.

In Leiningen this is done by adding :scope "provided" to a dependency. I read [1] deps.edn does not support this, and the recommended way is to use aliases I think.

WDYT?

Thanks, r0man.

[1] https://ask.clojure.org/index.php/9110/scope-in-deps-edn-should-be-added-and-not-deleted-from-pom-xml

escaped " does not survive lit

Hi folks,

I'm using lit to translate the following cljs map
At clojure side the string keys that have escaped " are fine, but when they are translated to clojurescript "&[data-orientation="horizontal"]" becomes "&data-orientation="horizontal"]" as if horizontal were an identifier.
I don't know if its feasible for you to handle this at j/lit (which is awesome btw) level, so I'm registering the issue.

Thanks for j/lit

(j/lit
{
  "border" "none"
  "margin" 0
  "flexShrink" 0
  "backgroundColor" "$slate6"
  "cursor" "default"

  "variants" {
    "size" {
      "1" {
        "&[data-orientation=\"horizontal\"]" {
          "height" "1px"
          "width" "$3"
        }

         "&[data-orientation=\"vertical\"]" {
           "width" "1px"
           "height" "$3"
         }
      }
      "2" {
         "&[data-orientation=\"horizontal\"]" {
           "height" "1px"
           "width" "$7"
         }

         "&[data-orientation=\"vertical\"]" {
           "width" "1px"
           "height" "$7"
         }
      }
    }
  }
  "defaultVariants" {
    "size" "1"
  }
}
            )

consider using only project name in namespace

is there a reason you chose to use organization-name/project-name in namespace, rather than just project name itself? i.e. keep full name for the deps reuire, but only project name for the namespace require

this library is imported a lot, so it'd be great to be able to write [js-interop.core :as j] rather than [applied-science.js-interop :as j].

edit: i do not mean single segment namespace, just namespacing based on project name not organization name

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.