I added the ability to create JS class
and more recently support for JS template strings (optionally tagged) to shadow-cljs
. I think they’d be useful additions to CLJS core at some point, but they’ll definitely need more discussion and testing first. I hope to start this discussion here first before taking it to the proper official places. Maybe these features are best kept in a library instead too. The code I have now does not actually depend on shadow-cljs
in any way, it just was easier for me to distribute them with shadow-cljs
vs making a separate library.
class via defclass
class
has become very widespread in its use in JS and I expect that to keep growing. With that comes uses of constructors required to call super()
with class ... extends AThingFromALibrary
. Not always can this be emulated via ES5 code. Once such case where class
becomes more or less required is writing web components. The answer if you wanted to do this in CLJS has always been very unsatisfactory to me so I changed it.
<script>
class HelloWorld extends HTMLElement {
constructor() {
// custom elements MUST call super as per spec
super();
}
// called by the JS engine
connectedCallback() {
console.log("hey I was added to the DOM", this);
}
}
customElements.define("hello-world", HelloWorld);
</script>
<hello-world></hello-world>
The example is kinda pointless but I dare you to implement it in CLJS.
To support this I added the defclass
“special form” that enables you to write class
definitions in pure CLJS that generates an actual class
without trying to emulate it in any way.
(ns foo.bar
(:require [shadow.cljs.modern :refer (defclass)]))
(defclass HelloWorld
(extends js/HTMLElement)
(constructor [this]
(super))
Object
(connectedCallback [this]
(js/console.log "hey from CLJS" this)))
(js/window.customElements.define "hello-world" HelloWorld)
I do think that pretty much everyone should stick with default CLJS deftype
and defrecord
in most places but sometimes you just can’t. This is in no way meant to replace them.
More complex examples might look like this
(defprotocol AProtocol
(example-fn [this a b]))
(defclass Example
(extends a-library/Thing)
;; fields don't need to match constructor args
;; and can have default values, initialized automatically
;; in the constructor, without default they need to be
;; set! or they'll default to undefined
(field foo)
(field bar "default value")
(constructor [this some arg]
(let [x (+ some arg)]
(super x "something")
(set! foo (calculate-something this x))))
;; for uses from within CLJS
;; just like extend-protocol/extend-type
AProtocol
(example-fn [this a b]
(str foo bar a b))
;; for when a library will call thing.jsLibraryMethod(x)
;; and you are supposed to give it a thing implementing that
Object
(jsLibraryMethod [this x]
foo))
Of course I wrote this out of my own needs in case you are looking for an actual example in the wild.
Template Strings via js-template
The use of template strings in JS libraries is also spreading rapidly and they are rather annoying to replicate from CLJS. Template literals in JS serve as a regular string on steroids but also have a special “tag” mechanism in which case they totally do not act like strings at all. You might have seen them used in some JS code like
import {html, render} from 'lit-html';
let sayHello = (name) => html`<h1>Hello ${name}</h1>`;
render(sayHello('World'), document.body);
const Button = styled.button`
color: grey;
`;
import { gql, useQuery } from '@apollo/client';
const GET_DOGS = gql`
query GetDogs {
dogs {
id
breed
}
}
`;
Note all the weird backticks. Basically none of this code is “accessible” from CLJS since the semantics of tagged template literals are rather specific and annoying to emulate.
The new js-template
form is meant to address this and enables you to directly generate those template literals with optional tags.
(require '[shadow.cljs.modern :refer (js-template)])
(require '["lit-html" :as lit])
(defn say-hello [name]
(js-template lit/html "<h1>Hello " name "</h1>"))
At first sight this looks just like str
and that is basically what it is API wise. The above would however give you the tagged template literal which is quite different from the regular str
.
cljs.user.say_hello = function(name) {
lit.html`<h1>Hello ${name}</h1>`;
}
Basically if the first argument to js-template
is a string that will end up emitting just the plain literal in which case you might just as well use str
. If the first argument however is not a string that will be treated as the tag and called as such. I don’t expect to see much use of this in regular CLJS code but some JS library code may expect to be passed one of these.
ES6 and beyond
Note that both of these features were readily available as of ES6 (or ES2016) and work in all modern platforms.
I don’t plan to add all modern JS features to via the shadow.cljs.modern
namespace/lib but maybe there are other that might end up being useful in some way. Before you ask async/await
cannot be added in this way and would require some actual compiler work.
Please don’t make these part of any CLJS API/library you write, they are strictly meant to ease some interop issues to make some JS code accessible that otherwise isn’t currently.