{"id":13760496,"url":"https://github.com/binaryage/cljs-oops","last_synced_at":"2025-10-07T20:37:16.021Z","repository":{"id":62431319,"uuid":"67241272","full_name":"binaryage/cljs-oops","owner":"binaryage","description":"ClojureScript macros for convenient native Javascript object access.","archived":false,"fork":false,"pushed_at":"2022-03-05T20:00:18.000Z","size":743,"stargazers_count":352,"open_issues_count":5,"forks_count":13,"subscribers_count":6,"default_branch":"master","last_synced_at":"2025-09-14T21:54:06.533Z","etag":null,"topics":["clojurescript","externs","interop"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"other","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/binaryage.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":"license.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2016-09-02T17:15:38.000Z","updated_at":"2025-08-12T15:25:49.000Z","dependencies_parsed_at":"2022-11-01T21:00:51.011Z","dependency_job_id":null,"html_url":"https://github.com/binaryage/cljs-oops","commit_stats":null,"previous_names":[],"tags_count":21,"template":false,"template_full_name":null,"purl":"pkg:github/binaryage/cljs-oops","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-oops","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-oops/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-oops/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-oops/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/binaryage","download_url":"https://codeload.github.com/binaryage/cljs-oops/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/binaryage%2Fcljs-oops/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":278842290,"owners_count":26055527,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-10-07T02:00:06.786Z","response_time":59,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["clojurescript","externs","interop"],"created_at":"2024-08-03T13:01:11.498Z","updated_at":"2025-10-07T20:37:15.987Z","avatar_url":"https://github.com/binaryage.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"# cljs-oops\n\n[![GitHub license](https://img.shields.io/badge/license-MIT-lightgrey.svg)](license.txt) \n[![Clojars Project](https://img.shields.io/clojars/v/binaryage/oops.svg)](https://clojars.org/binaryage/oops) \n[![Travis](https://img.shields.io/travis/binaryage/cljs-oops.svg)](https://travis-ci.org/binaryage/cljs-oops) \n[![Sample Project](https://img.shields.io/badge/project-example-ff69b4.svg)](https://github.com/binaryage/cljs-oops-sample)\n\nThis 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.\n\n**TOC**\n| **[Object operations](#object-operations)** \n| **[Installation](#installation)**\n| **[Motivation](#motivation)**\n| **[Benefits](#benefits)**\n| **[FAQ](#faq)**\n\n```text\nBoss: \"Ship it!\"\nYou:  \"Let me compile it with :advanced optimizations...\"\nBoss: \"Sounds good!\"\n...one coffee later\nYou:  \"Oops! It just broke! And I don't know why.\" \nBoss: \"Don't tell me that a random person on the Internet was wrong again.\"\nYou:  (sad face) \"Yep, they provided slightly outdated externs!\"\n```\n\n### Object operations\n\nAdd these new power-macros to your tool belt:\n\n 1. `oget` is a flexible, safe and guilt-free replacement for [`aget`][16]\n 2. `oset!` is [`aset`][17] on steroids\n 3. `ocall` is a replacement for `(.call ...)` built on top of `oget`\n 4. `oapply` is a replacement for `(.apply ...)` built on top of `oget`\n\nLet's see some code examples first and then discuss the concepts:\n\n\u003ca href=\"https://box.binaryage.com/cljs-oops-intro-oget.png\"\u003e\u003cimg src=\"https://box.binaryage.com/cljs-oops-intro-oget.png\"\u003e\u003c/a\u003e\n\n\u003ca href=\"https://box.binaryage.com/cljs-oops-intro-oset.png\"\u003e\u003cimg src=\"https://box.binaryage.com/cljs-oops-intro-oset.png\"\u003e\u003c/a\u003e\n\n### Installation\n\n#### Integrate with your project\n\nAdd oops dependency into your Leiningen's `project.clj` or boot file. \n\n[![Clojars Project](https://img.shields.io/clojars/v/binaryage/oops.svg)](https://clojars.org/binaryage/oops)\n\nRequire macros in your namespaces via `oops.core` ClojureScript namespace:\n\n```clojure\n(ns your.project.namespace\n  (:require [oops.core :refer [oget oset! ocall oapply ocall! oapply!\n                               oget+ oset!+ ocall+ oapply+ ocall!+ oapply!+]]))\n\n(oset! (js-obj) :mood \"a happy camper\")\n```\n\nPlease note that we are not using `:refer-macros` here. We rely on [automatic macro refer inference][18] in latest ClojureScript. \n\nAlso please be aware that oops uses [clojure.spec][14] which is available since Clojure 1.9.\nIf you cannot upgrade to Clojure 1.9, you may stick with Clojure 1.8 and add this [backported version of clojure.spec][15].\n\nOtherwise pretty standard stuff. If in doubts, look at the [sample project][13].\n\n### Motivation\n\n\u003e I don't always do Javascript interops, but when I do, I call them by names.\n\u003e\n\u003e -- \u003ccite\u003eDarwin (with sunglasses on)\u003c/cite\u003e\n\nClojureScript developers should quickly learn how to inter-operate with native Javascript objects via [the dot operator][1].\nThis was modelled to closely follow [Clojure's Java interop][4] story.\n\nFor example, the ClojureScript form `(.-nativeProp obj)` will compile to `obj.nativeProp` in Javascript.\n\nIt works pretty well [during development][3] but there is a catch! When you naively write code like that, it might\nnot survive [advanced optimizations][2]. Closure Compiler needs some information about which property names are safe to rename\nand which cannot be renamed because they might be referenced externally or dynamically via strings.\n\nSomeone at Google had a quick and bad idea. We could provide a separate file which would describe this information.\nLet's call it an \"externs file\"!\n\n#### Externs from hell\n\nI'm pretty opinionated about using externs. I hate it with passion. Here is the list of my reasons:\n\n1. Development behaviour is disconnected from production behaviour - discovering breakages only after switching to :advanced mode.\nI know, I should continuously run tests against :advanced mode. But :advanced builds are pretty slow and it is no fun to\nfish for \"Cannot read property 'j349s' of null\"-kind of errors in minified raw Javascript files which could balloon to multi-MB sizes.\nHave to wait for quantum computers to provide our IDEs with enough computational power to parse and syntax-highlight\nmulti-megabyte one-line Javascript files ;)\n\n2. Say, authors of a useful (native) library don't provide externs file (usually simply because they don't use Closure Compiler).\n So there must come [someone else][7] who is willing to maintain an externs file for their library by following changes in the library.\n 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.\n Also that someone will probably sooner or later lose interest in maintaining the externs file and you have no way of telling\n if it is outdated/incomplete without doing a full code-review. And the worst part is that \"someone\" is very often you.\n\n3. Incomplete (or outdated) externs files provide no feedback. Except that you suddenly discover that a new build is broken again and\n you are back to \"[pseudo-names][8] fishing\".\n\n4. Externs have to be configured. Paths must be specified. Externs are not co-located with the code they are describing.\nIt is not always clear where individual externs are coming from. Some \"default\" externs for standard browser/DOM APIs are baked-in\nClosure Compiler by default which might give you false sense of security or confuse assumptions about how this whole thing works.\n\n#### Side-stepping the whole externs mess\n\nWhat if I told you to ditch your externs because there is a simpler way?\n\nSimply [use string names][5] to access object properties in Javascript (in cases where you would rely on externs).\nInstead of `(.-nativeProp obj)` write `(aget obj \"nativeProp\")` which compiles to `obj[\"nativeProp\"]`. String names are not\nsubject of renaming in advanced mode. And practically the same code runs in development and advanced mode.\n\nI hear you. This looks dirty. We are abusing `aget` which was [explicitly documented][6] to be for native array only.\nAlternatively we could use `goog.object/get` or the multi-arity `goog.object/getValueByKeys` which looks a bit better,\nbut kinda verbose.\n\nInstead of investing your energy into maintaining externs you could as well incrementally write a lightweight\nClojure-style wrapper functions to access native APIs by string names directly. For example:\n\n```clojure\n(defn get-element-by-id [id]\n  (.call (aget js/document \"getElementById\") js/document id))\n```\n\nIt is much more flexible than externs. You have full control and power of ClojureScript code here.\nAnd who knows, maybe later you will extract the code and publish it as a nice ClojureScript wrapper library.\n\nSounds good? With oops library the situation can be even better.\nWhat if we had something like `aget` but safer and more flexible?\nI'm pleased to introduce you to `oget`...\n\n### Benefits\n\n#### Be more expressive with selectors\n\nThe signature for `oget` is `(oget obj \u0026 selector)`.\n\nSelector is a data structure describing exact path for traversing into a native object `obj`.\nSelectors can be plain strings, keywords or for convenience [arbitrarily nested collections of those][9].\n\nSelectors are pretty flexible. The following selectors describe the same path:\n\n```clojure\n(oget o \"k3.?k31.k311\")\n(oget o \"k3\" \"?k31\" :k311)\n(oget o [\"k3\" \"?k31\" \"k311\"])\n(oget o [[\"k3\"] \"?k31\"] \"k311\")\n```\n\n##### Access modifiers\n\nPlease note the \".?\" is a modifier for \"soft\" access (inspired by [CoffeeScript's existential operator][19]).\nWe expect that the key 'k31' might not be present and want `oget` to stop and silently return nil in that case.\n\nIn case of `oset!` you can use so-called \"punching\" for creation of missing keys on path. For example:\n\n```clojure\n(oset! (js-obj) \"!k1.!k2.!k3\" \"val\")\n```\n\nThat will create `k1` and `k2` on the path to setting final `k3` key to `val`. If you didn't specify the exclamation modifiers\n `oset!` would complain about missing keys. This makes sense because if you know the path exists for sure\n 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).\n\n#### Static vs. dynamic selectors\n\nDynamic selector is a selector which is not fully known at compile-time. For example result of a function call\n is a dynamic selector:\n\n```clojure\n(oget o (identity \"key\"))\n```\n\nAt runtime the form result is the same but generated code is less effective. Dynamic selectors should be very rare.\nBy default, oops assumes that you want to prefer static selectors and dynamic selectors are unintentional.\nCompiler will issue a compile-time warning about \"Unexpected dynamic selector usage\".\nTo silence this warnings use \"plus\" version of `oget` like this:\n\n```clojure\n(oget+ o (identity \"key\"))\n```\n\nThis way you express explicit consent with dynamic selector code-path.\n\n#### Play it safe during development\n\nBy default, oops generates diagnostics code and does pretty intensive safe-checking in non-advanced builds.\nAs you can see on the screenshots above you might get compile-time or run-time warnings and errors when unexpected things happen,\nlike accessing missing keys or traversing non-objects.\n\n#### Produce efficient barebone code in :advanced builds\n\nBy default, all diagnostics code is elided in :advanced builds and oops produces code similar to hand-written `aget` usage\n(without any safety-checks).\n\nYou can inspect our test [compilation transcripts][10] to see what code is generated in different compiler modes.\n\n#### Tailor oops behaviour\n\nI believe oops has sensible defaults and there should be no need to tweak it under normal circumstances.\nAnyways, look at possible configuration options in [defaults.clj][11].\n\nAs you can see, you can provide your own config overrides in the ClojureScript compiler options map\nvia `:external-config \u003e :oops/config`.\nSee example in [cljs-oops-sample][12] project.\n\n### Recommended links\n\n  * [Navigating ClojureScript's Fire Swamps](https://www.youtube.com/watch?v=tpXfASdPteI) by [Peter Schuck](https://github.com/spinningtopsofdoom)\n\n### FAQ\n\n\u003e Isn't accessing properties by string names slower?\n\nWell, only if the strings are computed dynamically at runtime. In case of string literals Javascript parser can see them\nand there should be no reason to treat them differently than dot properties. But you don't have to worry about this.\nGoogle Closure compiler rewrites string literals to dot property access whenever possible.\n\n\u003e Should I use cljs-oops with Closure Library (e.g. goog.something namespace)?\n\nNo! Use oops only for interop with external code which is not part of your :advanced build.\nThat means for all code where you would normally need to write externs.\n\nClosure Library is compatible with advanced compilation and identifiers get properly minified during compilation.\nYou don't have to write any externs for it, so you don't have to use oops with it.\n\nSecond area where you want to use string names is when you work with JSON data objects (e.g. data received over a network).\nString names explicitly prevent minification of key names which must stay intact.\n\nFor better understanding please [read this detailed article][20] by Luke VanderHart.\n\n\u003e How this approach compares to [ClojureScript externs inference][21]?\n\nExterns inference is very recent feature and looks promising. It was introduced after I put all the effort into developing\nthis library so my opinion is biased :-). I personally still prefer investing time into building light-weight ClojureScript\nwrapper libraries using string-names than dealing with externs (even if they are auto-inferred).\n\nI would recommend watching [Navigating ClojureScript's Fire Swamps](https://www.youtube.com/watch?v=tpXfASdPteI) by [Peter Schuck](https://github.com/spinningtopsofdoom)\nwhere he compares both methods.\n\n[1]: http://cljs.github.io/api/syntax/dot\n[2]: https://github.com/clojure/clojurescript/wiki/Advanced-Compilation\n[3]: https://github.com/clojure/clojurescript/wiki/Compiler-Options#optimizations\n[4]: http://clojure.org/reference/java_interop\n[5]: https://github.com/clojure/clojurescript/wiki/Dependencies#using-string-names\n[6]: http://cljs.github.io/api/cljs.core/aget\n[7]: http://cljsjs.github.io\n[8]: https://github.com/clojure/clojurescript/wiki/Compiler-Options#pseudo-names\n[9]: https://github.com/binaryage/cljs-oops/blob/master/src/lib/oops/sdefs.clj\n[10]: https://github.com/binaryage/cljs-oops/tree/master/test/transcripts/expected\n[11]: https://github.com/binaryage/cljs-oops/blob/master/src/lib/oops/defaults.clj\n[12]: https://github.com/binaryage/cljs-oops-sample/blob/932fc322ff5ab1cb26e48c136b63d24c8c5c1615/project.clj#L43\n[13]: https://github.com/binaryage/cljs-oops-sample\n[14]: http://clojure.org/guides/spec\n[15]: https://github.com/tonsky/clojure-future-spec\n[16]: http://cljs.github.io/api/cljs.core/aget\n[17]: http://cljs.github.io/api/cljs.core/aset\n[18]: http://dev.clojure.org/jira/browse/CLJS-1507\n[19]: http://valve.github.io/blog/2013/07/13/existential-operator-in-coffeescript/\n[20]: http://lukevanderhart.com/2011/09/30/using-javascript-and-clojurescript.html\n[21]: https://clojurescript.org/guides/externs\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryage%2Fcljs-oops","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fbinaryage%2Fcljs-oops","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fbinaryage%2Fcljs-oops/lists"}