Comprehensive comparison document covering all jQuery 4.0.0 examples with equivalent code in Google Closure Library and ClojureScript. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
53 KiB
jQuery vs Google Closure Library vs ClojureScript Comparison
A comprehensive comparison of jQuery 4.0.0 with Google Closure Library and ClojureScript (with Google Closure), providing equivalent code for every example in the jQuery documentation.
Table of Contents
- Overview
- Installation
- Core Selection
- Utility Functions
- DOM Manipulation
- Event Handling
- Traversal
- Data Storage
- AJAX
- Deferred/Promises
- Callbacks
- Animation
- Security Features
Overview
| Aspect | jQuery 4.0.0 | Google Closure Library | ClojureScript + Closure |
|---|---|---|---|
| Status | Active (Jan 2026) | Sunset (Aug 2024) | Active |
| Size | ~30kb gzipped | Modular (tree-shaken) | Compiled & optimized |
| Philosophy | Imperative, chaining | Object-oriented, modular | Functional, immutable |
| Browser compat | Last 3 versions | Cross-browser normalized | Via Closure Compiler |
| Build system | Optional | Closure Compiler | ClojureScript compiler |
Key Differences
jQuery: Designed for simplicity and DOM manipulation with a fluent chaining API. Excellent for quick prototyping and small projects.
Google Closure Library: A comprehensive, modular library built for large-scale applications. Designed to work with the Closure Compiler for aggressive optimizations. Note: The library was sunset in August 2024.
ClojureScript: A Clojure dialect that compiles to JavaScript. Automatically includes Google Closure Library and uses the Closure Compiler. Offers functional programming paradigms with immutable data structures.
Installation
jQuery 4.0.0
<!-- CDN -->
<script src="https://code.jquery.com/jquery-4.0.0.min.js"></script>
// ES Module
import $ from 'jquery';
// CommonJS
const $ = require('jquery');
Google Closure Library
<!-- Include base.js -->
<script src="closure-library/closure/goog/base.js"></script>
<script>
goog.require('goog.dom');
goog.require('goog.events');
</script>
// Modern ES modules (if using Closure Compiler)
goog.module('myapp.main');
const dom = goog.require('goog.dom');
const events = goog.require('goog.events');
ClojureScript
;; deps.edn
{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}}}
;; In your namespace
(ns myapp.core
(:require [goog.dom :as gdom]
[goog.events :as gevents])
(:import [goog.events EventType]))
Core Selection
jQuery: Select by ID
// jQuery
const $header = $('#header');
Google Closure
// Google Closure
const header = goog.dom.getElement('header');
// or shorthand
const header = goog.dom.$('header');
ClojureScript
;; ClojureScript with goog.dom
(require '[goog.dom :as gdom])
(def header (gdom/getElement "header"))
;; Or using native JS interop
(def header (.getElementById js/document "header"))
jQuery: Select by Class
// jQuery
const $buttons = $('.btn');
Google Closure
// Google Closure - single element
const button = goog.dom.getElementByClass('btn');
// Multiple elements
const buttons = goog.dom.getElementsByClass('btn');
ClojureScript
;; ClojureScript
(def button (gdom/getElementByClass "btn"))
(def buttons (gdom/getElementsByClass "btn"))
;; With dommy library
(require '[dommy.core :as dommy :refer-macros [sel sel1]])
(def button (sel1 :.btn))
(def buttons (sel :.btn))
jQuery: Select by Tag
// jQuery
const $paragraphs = $('p');
Google Closure
// Google Closure
const paragraphs = goog.dom.getElementsByTagName('p');
// With class filter
const items = goog.dom.getElementsByTagNameAndClass('div', 'item');
ClojureScript
;; ClojureScript
(def paragraphs (gdom/getElementsByTagName "p"))
;; With dommy
(def paragraphs (sel :p))
jQuery: Select with Context
// jQuery
const $items = $('.item', '#container');
Google Closure
// Google Closure
const container = goog.dom.getElement('container');
const items = goog.dom.getElementsByClass('item', container);
// Using query selector
const items = goog.dom.query('.item', container);
ClojureScript
;; ClojureScript
(def container (gdom/getElement "container"))
(def items (gdom/getElementsByClass "item" container))
;; With dommy
(def items (sel container :.item))
jQuery: Create Elements
// jQuery
const $newDiv = $('<div class="new">Content</div>');
Google Closure
// Google Closure
const newDiv = goog.dom.createDom('div', {'class': 'new'}, 'Content');
// Shorthand
const newDiv = goog.dom.$dom('div', {'class': 'new'}, 'Content');
// Complex nesting
const complex = goog.dom.createDom('div', {'class': 'wrapper'},
goog.dom.createDom('h1', null, 'Title'),
goog.dom.createDom('p', null, 'Paragraph')
);
ClojureScript
;; ClojureScript with goog.dom
(def new-div (gdom/createDom "div" #js {:class "new"} "Content"))
;; With hiccup-style libraries (hipo, hiccups)
(require '[hipo.core :as hipo])
(def new-div (hipo/create [:div.new "Content"]))
;; Complex with hiccup
(def complex (hipo/create
[:div.wrapper
[:h1 "Title"]
[:p "Paragraph"]]))
Utility Functions
jQuery: $.extend() - Object Merging
// jQuery - Shallow merge
const defaults = { color: 'blue', size: 'medium' };
const options = { color: 'red' };
const settings = $.extend({}, defaults, options);
// Result: { color: 'red', size: 'medium' }
// jQuery - Deep merge
const obj1 = { a: { b: 1, c: 2 } };
const obj2 = { a: { b: 3 } };
const result = $.extend(true, {}, obj1, obj2);
// Result: { a: { b: 3, c: 2 } }
Google Closure
// Google Closure - goog.object
goog.require('goog.object');
// Shallow merge (modifies target)
const settings = {};
goog.object.extend(settings, defaults, options);
// Clone then merge
const settings = goog.object.clone(defaults);
goog.object.extend(settings, options);
// Note: No built-in deep merge - use goog.object.unsafeClone for deep copy
ClojureScript
;; ClojureScript - native merge (immutable, shallow)
(def defaults {:color "blue" :size "medium"})
(def options {:color "red"})
(def settings (merge defaults options))
;; => {:color "red" :size "medium"}
;; Deep merge
(def obj1 {:a {:b 1 :c 2}})
(def obj2 {:a {:b 3}})
(def result (merge-with merge obj1 obj2))
;; => {:a {:b 3 :c 2}}
;; Or use a deep-merge library
(require '[clojure.walk :refer [postwalk]])
jQuery: $.each() - Iteration
// jQuery - Array iteration
$.each(['a', 'b', 'c'], function(index, value) {
console.log(`${index}: ${value}`);
});
// jQuery - Object iteration
$.each({ name: 'John', age: 30 }, function(key, value) {
console.log(`${key}: ${value}`);
});
// jQuery - Return false to break
$.each([1, 2, 3, 4, 5], function(i, num) {
if (num > 3) return false;
console.log(num);
});
// Output: 1, 2, 3
Google Closure
// Google Closure - goog.array and goog.object
goog.require('goog.array');
goog.require('goog.object');
// Array iteration
goog.array.forEach(['a', 'b', 'c'], function(value, index) {
console.log(index + ': ' + value);
});
// Object iteration
goog.object.forEach({ name: 'John', age: 30 }, function(value, key) {
console.log(key + ': ' + value);
});
// Break with goog.array.some (return true to stop)
goog.array.some([1, 2, 3, 4, 5], function(num) {
if (num > 3) return true;
console.log(num);
return false;
});
ClojureScript
;; ClojureScript - Array iteration
(doseq [[index value] (map-indexed vector ["a" "b" "c"])]
(println (str index ": " value)))
;; Simpler
(run! println ["a" "b" "c"])
;; Object/map iteration
(doseq [[k v] {:name "John" :age 30}]
(println (str (name k) ": " v)))
;; Early termination with reduced
(reduce (fn [_ num]
(if (> num 3)
(reduced nil)
(println num)))
nil [1 2 3 4 5])
;; Output: 1, 2, 3
jQuery: $.map() - Transform Array
// jQuery
const numbers = [1, 2, 3, 4];
const doubled = $.map(numbers, n => n * 2);
// Result: [2, 4, 6, 8]
// Return multiple values
const expanded = $.map([1, 2], n => [n, n * 2]);
// Result: [1, 2, 2, 4]
// Return null to remove
const filtered = $.map([1, 2, 3, 4], n => n > 2 ? n : null);
// Result: [3, 4]
Google Closure
// Google Closure
goog.require('goog.array');
// Map
const doubled = goog.array.map([1, 2, 3, 4], function(n) {
return n * 2;
});
// Flat map (expand)
const expanded = goog.array.flatten(
goog.array.map([1, 2], function(n) {
return [n, n * 2];
})
);
// Filter then map (no null removal in map)
const filtered = goog.array.filter([1, 2, 3, 4], function(n) {
return n > 2;
});
ClojureScript
;; ClojureScript - map
(def numbers [1 2 3 4])
(def doubled (map #(* % 2) numbers))
;; => (2 4 6 8)
;; mapcat for expansion (flat map)
(def expanded (mapcat (fn [n] [n (* n 2)]) [1 2]))
;; => (1 2 2 4)
;; filter + map or keep
(def filtered (filter #(> % 2) [1 2 3 4]))
;; => (3 4)
;; Or use keep (removes nils)
(def filtered (keep #(when (> % 2) %) [1 2 3 4]))
jQuery: $.grep() - Filter Array
// jQuery
const numbers = [1, 2, 3, 4, 5, 6];
// Keep elements where callback returns true
const evens = $.grep(numbers, n => n % 2 === 0);
// Result: [2, 4, 6]
// With invert=true, keep elements where callback returns false
const odds = $.grep(numbers, n => n % 2 === 0, true);
// Result: [1, 3, 5]
Google Closure
// Google Closure
goog.require('goog.array');
const numbers = [1, 2, 3, 4, 5, 6];
// Filter
const evens = goog.array.filter(numbers, function(n) {
return n % 2 === 0;
});
// Inverted (negate the condition)
const odds = goog.array.filter(numbers, function(n) {
return n % 2 !== 0;
});
ClojureScript
;; ClojureScript
(def numbers [1 2 3 4 5 6])
;; Filter
(def evens (filter even? numbers))
;; => (2 4 6)
;; Inverted with remove or complement
(def odds (remove even? numbers))
;; => (1 3 5)
;; Or
(def odds (filter odd? numbers))
(def odds (filter (complement even?) numbers))
jQuery: $.merge() - Merge Arrays
// jQuery
const arr1 = [1, 2];
const arr2 = [3, 4];
$.merge(arr1, arr2);
// arr1 is now [1, 2, 3, 4]
Google Closure
// Google Closure
goog.require('goog.array');
const arr1 = [1, 2];
const arr2 = [3, 4];
// Extend modifies first array
goog.array.extend(arr1, arr2);
// arr1 is now [1, 2, 3, 4]
// Or create new array
const merged = goog.array.concat(arr1, arr2);
ClojureScript
;; ClojureScript - concat (immutable, creates new sequence)
(def arr1 [1 2])
(def arr2 [3 4])
(def merged (concat arr1 arr2))
;; => (1 2 3 4)
;; As vector
(def merged (into arr1 arr2))
;; => [1 2 3 4]
jQuery: $.inArray() - Find Index
// jQuery
const arr = ['a', 'b', 'c', 'd'];
$.inArray('b', arr); // Returns: 1
$.inArray('x', arr); // Returns: -1
$.inArray('b', arr, 2); // Returns: -1 (starts search at index 2)
Google Closure
// Google Closure
goog.require('goog.array');
const arr = ['a', 'b', 'c', 'd'];
goog.array.indexOf(arr, 'b'); // Returns: 1
goog.array.indexOf(arr, 'x'); // Returns: -1
goog.array.indexOf(arr, 'b', 2); // Returns: -1
ClojureScript
;; ClojureScript
(def arr ["a" "b" "c" "d"])
;; Using .indexOf on vector
(.indexOf arr "b") ;; => 1
(.indexOf arr "x") ;; => -1
;; Or idiomatic ClojureScript
(defn index-of [coll item]
(first (keep-indexed #(when (= %2 item) %1) coll)))
(index-of arr "b") ;; => 1
(index-of arr "x") ;; => nil
jQuery: .isPlainObject() / .isEmptyObject()
// jQuery
$.isPlainObject({}); // true
$.isPlainObject({ a: 1 }); // true
$.isPlainObject([]); // false
$.isPlainObject(new Date()); // false
$.isEmptyObject({}); // true
$.isEmptyObject({ a: 1 }); // false
Google Closure
// Google Closure
goog.require('goog.object');
goog.isObject({}); // true (but also true for arrays!)
// Check plain object more carefully
function isPlainObject(obj) {
return goog.isObject(obj) &&
!goog.isArray(obj) &&
Object.getPrototypeOf(obj) === Object.prototype;
}
// Empty object check
goog.object.isEmpty({}); // true
goog.object.isEmpty({ a: 1 }); // false
ClojureScript
;; ClojureScript
(map? {}) ;; true
(map? {:a 1}) ;; true
(map? []) ;; false
;; Empty check
(empty? {}) ;; true
(empty? {:a 1}) ;; false
;; For JS objects specifically
(object? #js {}) ;; true
(goog.object/isEmpty #js {}) ;; true
Removed jQuery Functions (Use Native)
// jQuery 3.x (deprecated) -> Native JavaScript
// $.isArray() -> Array.isArray()
if (Array.isArray(myVar)) { /* ... */ }
// $.isFunction() -> typeof
if (typeof callback === 'function') { /* ... */ }
// $.trim() -> String.prototype.trim()
var trimmed = userInput.trim();
// $.parseJSON() -> JSON.parse()
var data = JSON.parse(jsonString);
// $.now() -> Date.now()
var timestamp = Date.now();
Google Closure Equivalents
// Google Closure
goog.isArray(myVar);
goog.isFunction(callback);
goog.string.trim(userInput);
// JSON.parse is native, or use goog.json.parse for older browsers
var timestamp = goog.now();
ClojureScript Equivalents
;; ClojureScript
(array? my-var)
(fn? callback)
(clojure.string/trim user-input)
(js/JSON.parse json-string)
(.now js/Date)
DOM Manipulation
jQuery: DOM Ready
// jQuery - Shorthand (recommended)
$(function() {
console.log('DOM ready');
});
// jQuery - Explicit ready
$(document).ready(function() {
console.log('DOM ready');
});
// jQuery - Arrow function
$(() => {
console.log('DOM ready');
});
Google Closure
// Google Closure
goog.require('goog.dom');
goog.require('goog.events');
// Using DOMContentLoaded
goog.events.listen(document, 'DOMContentLoaded', function() {
console.log('DOM ready');
});
// Or check if already ready
if (document.readyState === 'complete' || document.readyState === 'interactive') {
console.log('DOM already ready');
} else {
goog.events.listen(document, 'DOMContentLoaded', function() {
console.log('DOM ready');
});
}
ClojureScript
;; ClojureScript
(require '[goog.events :as gevents])
;; Using goog.events
(gevents/listen js/document "DOMContentLoaded"
(fn [_] (println "DOM ready")))
;; Or with native JS
(.addEventListener js/document "DOMContentLoaded"
(fn [_] (println "DOM ready")))
;; Check if already loaded
(if (or (= "complete" (.-readyState js/document))
(= "interactive" (.-readyState js/document)))
(println "Already ready")
(gevents/listen js/document "DOMContentLoaded"
(fn [_] (println "DOM ready"))))
jQuery: Append/Remove Elements
// jQuery - Append
$('#container').append('<p>New paragraph</p>');
// jQuery - Prepend
$('#container').prepend('<p>First paragraph</p>');
// jQuery - Remove
$('#element').remove();
// jQuery - Empty (remove children)
$('#container').empty();
Google Closure
// Google Closure
goog.require('goog.dom');
const container = goog.dom.getElement('container');
// Append
const newP = goog.dom.createDom('p', null, 'New paragraph');
goog.dom.appendChild(container, newP);
// Prepend
const firstP = goog.dom.createDom('p', null, 'First paragraph');
goog.dom.insertChildAt(container, firstP, 0);
// Remove
const element = goog.dom.getElement('element');
goog.dom.removeNode(element);
// Empty (remove children)
goog.dom.removeChildren(container);
ClojureScript
;; ClojureScript with goog.dom
(require '[goog.dom :as gdom])
(def container (gdom/getElement "container"))
;; Append
(let [new-p (gdom/createDom "p" nil "New paragraph")]
(gdom/appendChild container new-p))
;; Prepend
(let [first-p (gdom/createDom "p" nil "First paragraph")]
(gdom/insertChildAt container first-p 0))
;; Remove
(gdom/removeNode (gdom/getElement "element"))
;; Empty
(gdom/removeChildren container)
;; With dommy library
(require '[dommy.core :as dommy :refer-macros [sel1]])
(dommy/append! (sel1 :#container) some-element)
(dommy/remove! (sel1 :#element))
jQuery: Class Manipulation
// jQuery
$('#element').addClass('active');
$('#element').removeClass('inactive');
$('#element').toggleClass('visible');
$('#element').hasClass('active'); // returns boolean
Google Closure
// Google Closure
goog.require('goog.dom.classlist');
const element = goog.dom.getElement('element');
goog.dom.classlist.add(element, 'active');
goog.dom.classlist.remove(element, 'inactive');
goog.dom.classlist.toggle(element, 'visible');
goog.dom.classlist.contains(element, 'active'); // returns boolean
// Multiple classes
goog.dom.classlist.addAll(element, ['class1', 'class2']);
goog.dom.classlist.removeAll(element, ['class1', 'class2']);
// Swap classes
goog.dom.classlist.swap(element, 'old-class', 'new-class');
ClojureScript
;; ClojureScript with goog.dom.classlist
(require '[goog.dom.classlist :as classlist])
(def element (gdom/getElement "element"))
(classlist/add element "active")
(classlist/remove element "inactive")
(classlist/toggle element "visible")
(classlist/contains element "active") ;; returns boolean
;; With dommy
(require '[dommy.core :as dommy :refer-macros [sel1]])
(dommy/add-class! (sel1 :#element) :active)
(dommy/remove-class! (sel1 :#element) :inactive)
(dommy/toggle-class! (sel1 :#element) :visible)
(dommy/has-class? (sel1 :#element) :active)
jQuery: CSS/Style Manipulation
// jQuery - Get style
const color = $('#element').css('color');
// jQuery - Set single style
$('#element').css('color', 'red');
// jQuery - Set multiple styles
$('#element').css({
color: 'red',
fontSize: '16px',
backgroundColor: '#fff'
});
Google Closure
// Google Closure
goog.require('goog.style');
const element = goog.dom.getElement('element');
// Get style
const color = goog.style.getStyle(element, 'color');
// Set single style
goog.style.setStyle(element, 'color', 'red');
// Set multiple styles
goog.style.setStyle(element, {
'color': 'red',
'font-size': '16px',
'background-color': '#fff'
});
// Other style utilities
goog.style.setOpacity(element, 0.5);
goog.style.setSize(element, 100, 200); // width, height
goog.style.setPosition(element, 10, 20); // x, y
goog.style.showElement(element, true); // show/hide
ClojureScript
;; ClojureScript with goog.style
(require '[goog.style :as gstyle])
(def element (gdom/getElement "element"))
;; Get style
(def color (gstyle/getStyle element "color"))
;; Set single style
(gstyle/setStyle element "color" "red")
;; Set multiple styles
(gstyle/setStyle element #js {:color "red"
:font-size "16px"
:background-color "#fff"})
;; With dommy
(dommy/set-style! (sel1 :#element) :color "red")
(dommy/set-style! (sel1 :#element) :font-size "16px")
jQuery: Text and HTML Content
// jQuery - Get/Set text
const text = $('#element').text();
$('#element').text('New text content');
// jQuery - Get/Set HTML
const html = $('#element').html();
$('#element').html('<strong>Bold text</strong>');
Google Closure
// Google Closure
goog.require('goog.dom');
const element = goog.dom.getElement('element');
// Get/Set text
const text = goog.dom.getTextContent(element);
goog.dom.setTextContent(element, 'New text content');
// Get/Set HTML (be careful with XSS)
const html = element.innerHTML;
element.innerHTML = '<strong>Bold text</strong>';
// Safe HTML setting (with Trusted Types support)
goog.require('goog.dom.safe');
goog.dom.safe.setInnerHtml(element, trustedHtml);
ClojureScript
;; ClojureScript with goog.dom
(def element (gdom/getElement "element"))
;; Get/Set text
(def text (gdom/getTextContent element))
(gdom/setTextContent element "New text content")
;; Get/Set HTML
(def html (.-innerHTML element))
(set! (.-innerHTML element) "<strong>Bold text</strong>")
;; With dommy
(dommy/text (sel1 :#element))
(dommy/set-text! (sel1 :#element) "New text")
(dommy/html (sel1 :#element))
(dommy/set-html! (sel1 :#element) "<strong>Bold</strong>")
jQuery: Attributes
// jQuery - Get attribute
const href = $('a').attr('href');
// jQuery - Set attribute
$('a').attr('href', 'https://example.com');
// jQuery - Set multiple attributes
$('#img').attr({
src: 'image.jpg',
alt: 'Description'
});
// jQuery - Remove attribute
$('#input').removeAttr('disabled');
Google Closure
// Google Closure
const element = goog.dom.getElement('link');
// Get attribute
const href = element.getAttribute('href');
// Set attribute
element.setAttribute('href', 'https://example.com');
// Set multiple with goog.dom
goog.dom.setProperties(element, {
'href': 'https://example.com',
'target': '_blank'
});
// Remove attribute
element.removeAttribute('disabled');
ClojureScript
;; ClojureScript
(def element (gdom/getElement "link"))
;; Get attribute
(def href (.getAttribute element "href"))
;; Set attribute
(.setAttribute element "href" "https://example.com")
;; Set multiple
(gdom/setProperties element #js {:href "https://example.com"
:target "_blank"})
;; Remove
(.removeAttribute element "disabled")
;; With dommy
(dommy/attr (sel1 :a) :href)
(dommy/set-attr! (sel1 :a) :href "https://example.com")
(dommy/remove-attr! (sel1 :#input) :disabled)
Event Handling
jQuery: Basic Event Binding
// jQuery - on() method
$('#button').on('click', function(e) {
console.log('Button clicked');
});
// jQuery - Shorthand
$('#button').click(function() {
console.log('Button clicked');
});
// jQuery - Multiple events
$('#input').on('focus blur', function(e) {
console.log('Focus state changed');
});
Google Closure
// Google Closure
goog.require('goog.events');
goog.require('goog.events.EventType');
const button = goog.dom.getElement('button');
// Basic listener
goog.events.listen(button, goog.events.EventType.CLICK, function(e) {
console.log('Button clicked');
});
// Multiple events
const input = goog.dom.getElement('input');
goog.events.listen(input, [goog.events.EventType.FOCUS, goog.events.EventType.BLUR],
function(e) {
console.log('Focus state changed');
}
);
ClojureScript
;; ClojureScript with goog.events
(require '[goog.events :as gevents])
(import '[goog.events EventType])
(def button (gdom/getElement "button"))
;; Basic listener
(gevents/listen button EventType.CLICK
(fn [e] (println "Button clicked")))
;; Multiple events
(doseq [event-type [EventType.FOCUS EventType.BLUR]]
(gevents/listen input event-type
(fn [e] (println "Focus state changed"))))
;; With dommy
(dommy/listen! (sel1 :#button) :click
(fn [e] (println "Button clicked")))
jQuery: Event Delegation
// jQuery - Event delegation
$('#container').on('click', '.item', function(e) {
console.log('Item clicked:', $(this).text());
});
Google Closure
// Google Closure - Manual delegation
goog.require('goog.events');
goog.require('goog.dom');
const container = goog.dom.getElement('container');
goog.events.listen(container, goog.events.EventType.CLICK, function(e) {
// Check if target matches selector
const target = e.target;
if (goog.dom.classlist.contains(target, 'item')) {
console.log('Item clicked:', goog.dom.getTextContent(target));
}
// Or find closest matching ancestor
const item = goog.dom.getAncestorByClass(target, 'item');
if (item) {
console.log('Item clicked:', goog.dom.getTextContent(item));
}
});
ClojureScript
;; ClojureScript - Manual delegation
(gevents/listen (gdom/getElement "container") EventType.CLICK
(fn [e]
(let [target (.-target e)]
(when (classlist/contains target "item")
(println "Item clicked:" (gdom/getTextContent target))))))
;; With dommy - built-in delegation
(dommy/listen! [(sel1 :#container) :.item] :click
(fn [e]
(println "Item clicked:" (dommy/text (.-selectedTarget e)))))
jQuery: One-time Events
// jQuery - one() for single execution
$('#button').one('click', function() {
console.log('This only fires once');
});
Google Closure
// Google Closure
goog.events.listenOnce(button, goog.events.EventType.CLICK, function(e) {
console.log('This only fires once');
});
ClojureScript
;; ClojureScript
(gevents/listenOnce button EventType.CLICK
(fn [e] (println "This only fires once")))
;; With dommy
(dommy/listen-once! (sel1 :#button) :click
(fn [e] (println "This only fires once")))
jQuery: Removing Event Handlers
// jQuery - Named handler for removal
function clickHandler(e) {
console.log('Clicked');
}
$('#button').on('click', clickHandler);
$('#button').off('click', clickHandler);
// Remove all click handlers
$('#button').off('click');
// Remove all handlers
$('#button').off();
Google Closure
// Google Closure
function clickHandler(e) {
console.log('Clicked');
}
// Listen returns a key for removal
const key = goog.events.listen(button, goog.events.EventType.CLICK, clickHandler);
// Remove specific handler
goog.events.unlistenByKey(key);
// Or remove by parameters
goog.events.unlisten(button, goog.events.EventType.CLICK, clickHandler);
// Remove all handlers from element
goog.events.removeAll(button);
// Remove all handlers of specific type
goog.events.removeAll(button, goog.events.EventType.CLICK);
ClojureScript
;; ClojureScript
(defn click-handler [e]
(println "Clicked"))
;; Store the key
(def key (gevents/listen button EventType.CLICK click-handler))
;; Remove by key
(gevents/unlistenByKey key)
;; Remove by parameters
(gevents/unlisten button EventType.CLICK click-handler)
;; Remove all
(gevents/removeAll button)
;; With dommy
(dommy/listen! (sel1 :#button) :click click-handler)
(dommy/unlisten! (sel1 :#button) :click click-handler)
jQuery: Focus Event Order (W3C in 4.0)
// jQuery 4.0 order (W3C compliant):
// blur → focusout → focus → focusin
const $input = $('#myInput');
$input.on('blur', () => console.log('1. blur'));
$input.on('focusout', () => console.log('2. focusout'));
$input.on('focus', () => console.log('3. focus'));
$input.on('focusin', () => console.log('4. focusin'));
Google Closure
// Google Closure - Uses native event order
const input = goog.dom.getElement('myInput');
goog.events.listen(input, goog.events.EventType.BLUR, function() {
console.log('blur');
});
goog.events.listen(input, goog.events.EventType.FOCUS, function() {
console.log('focus');
});
// focusin/focusout via FocusHandler for cross-browser support
goog.require('goog.events.FocusHandler');
const focusHandler = new goog.events.FocusHandler(input);
goog.events.listen(focusHandler, goog.events.FocusHandler.EventType.FOCUSIN, function() {
console.log('focusin');
});
ClojureScript
;; ClojureScript
(gevents/listen input EventType.BLUR #(println "blur"))
(gevents/listen input EventType.FOCUS #(println "focus"))
;; For focusin/focusout
(import '[goog.events FocusHandler])
(def focus-handler (FocusHandler. input))
(gevents/listen focus-handler (.-FOCUSIN FocusHandler.EventType) #(println "focusin"))
Traversal
jQuery: .find()
// jQuery - Find all paragraphs inside #content
$('#content').find('p').addClass('styled');
// Chained traversal
$('#nav')
.find('li')
.find('a')
.addClass('nav-link');
Google Closure
// Google Closure
const content = goog.dom.getElement('content');
const paragraphs = goog.dom.getElementsByTagName('p', content);
goog.array.forEach(paragraphs, function(p) {
goog.dom.classlist.add(p, 'styled');
});
// Or with query
const links = goog.dom.query('#nav li a');
goog.array.forEach(links, function(a) {
goog.dom.classlist.add(a, 'nav-link');
});
ClojureScript
;; ClojureScript
(let [content (gdom/getElement "content")
paragraphs (gdom/getElementsByTagName "p" content)]
(doseq [p (array-seq paragraphs)]
(classlist/add p "styled")))
;; With dommy
(doseq [p (sel (sel1 :#content) :p)]
(dommy/add-class! p :styled))
jQuery: .filter()
// jQuery - Filter by selector
$('li').filter('.active').css('font-weight', 'bold');
// Filter by function
$('input').filter(function() {
return $(this).val().length > 0;
}).addClass('has-value');
Google Closure
// Google Closure
goog.require('goog.array');
// Filter by class
const items = goog.dom.getElementsByTagName('li');
const activeItems = goog.array.filter(items, function(li) {
return goog.dom.classlist.contains(li, 'active');
});
goog.array.forEach(activeItems, function(li) {
goog.style.setStyle(li, 'font-weight', 'bold');
});
// Filter by function
const inputs = goog.dom.getElementsByTagName('input');
const filledInputs = goog.array.filter(inputs, function(input) {
return input.value.length > 0;
});
ClojureScript
;; ClojureScript
(->> (array-seq (gdom/getElementsByTagName "li"))
(filter #(classlist/contains % "active"))
(run! #(gstyle/setStyle % "font-weight" "bold")))
;; Filter inputs with values
(->> (array-seq (gdom/getElementsByTagName "input"))
(filter #(> (count (.-value %)) 0))
(run! #(classlist/add % "has-value")))
;; With dommy
(->> (sel :li)
(filter #(dommy/has-class? % :active))
(run! #(dommy/set-style! % :font-weight "bold")))
jQuery: .closest()
// jQuery - Find parent form
$('input').on('blur', function() {
const $form = $(this).closest('form');
validateForm($form);
});
Google Closure
// Google Closure
goog.require('goog.dom');
goog.events.listen(input, 'blur', function(e) {
const form = goog.dom.getAncestorByTagNameAndClass(e.target, 'form');
validateForm(form);
});
// By class
const container = goog.dom.getAncestorByClass(element, 'container');
ClojureScript
;; ClojureScript
(gevents/listen input "blur"
(fn [e]
(let [form (gdom/getAncestorByTagNameAndClass (.-target e) "form")]
(validate-form form))))
;; By class
(def container (gdom/getAncestorByClass element "container"))
jQuery: .has()
// jQuery - Only select divs that contain a span
$('div').has('span').addClass('has-span');
Google Closure
// Google Closure
const divs = goog.dom.getElementsByTagName('div');
goog.array.forEach(divs, function(div) {
if (goog.dom.getElementsByTagName('span', div).length > 0) {
goog.dom.classlist.add(div, 'has-span');
}
});
ClojureScript
;; ClojureScript
(->> (array-seq (gdom/getElementsByTagName "div"))
(filter #(pos? (.-length (gdom/getElementsByTagName "span" %))))
(run! #(classlist/add % "has-span")))
jQuery: .index()
// jQuery
$('li.active').index(); // Position among siblings
const $items = $('li');
$items.index($('.active')); // Position in collection
Google Closure
// Google Closure
goog.require('goog.array');
// Among siblings
const active = goog.dom.getElementByClass('active');
const siblings = goog.dom.getChildren(active.parentNode);
const index = goog.array.indexOf(siblings, active);
// In collection
const items = goog.dom.getElementsByTagName('li');
const activeItem = goog.dom.getElementByClass('active');
const indexInCollection = goog.array.indexOf(items, activeItem);
ClojureScript
;; ClojureScript
(let [active (gdom/getElementByClass "active")
siblings (gdom/getChildren (.-parentNode active))]
(.indexOf (array-seq siblings) active))
jQuery: .add()
// jQuery - Combine selections
$('p')
.add('span')
.add('div.highlight')
.addClass('styled');
Google Closure
// Google Closure - Combine into array
const combined = goog.array.concat(
goog.array.toArray(goog.dom.getElementsByTagName('p')),
goog.array.toArray(goog.dom.getElementsByTagName('span')),
goog.array.toArray(goog.dom.getElementsByClass('highlight'))
);
goog.array.forEach(combined, function(el) {
goog.dom.classlist.add(el, 'styled');
});
ClojureScript
;; ClojureScript
(let [combined (concat
(array-seq (gdom/getElementsByTagName "p"))
(array-seq (gdom/getElementsByTagName "span"))
(array-seq (gdom/getElementsByClass "highlight")))]
(run! #(classlist/add % "styled") combined))
Data Storage
jQuery: .data()
// jQuery - Set data
$('#element').data('userId', 123);
$('#element').data('config', { theme: 'dark', lang: 'en' });
// Get data
const userId = $('#element').data('userId'); // 123
const config = $('#element').data('config');
// Get all data
const allData = $('#element').data();
// HTML5 data attributes are auto-loaded
// <div id="user" data-user-id="456" data-role="admin">
$('#user').data('userId'); // 456
$('#user').data('role'); // 'admin'
// Remove data
$('#element').removeData('temp');
Google Closure
// Google Closure - Using dataset API (HTML5)
const element = goog.dom.getElement('element');
// Set data (via dataset)
element.dataset.userId = '123';
element.dataset.config = JSON.stringify({ theme: 'dark', lang: 'en' });
// Get data
const userId = element.dataset.userId;
const config = JSON.parse(element.dataset.config);
// Or use a WeakMap for complex data
const dataStore = new WeakMap();
function setData(element, key, value) {
let data = dataStore.get(element) || {};
data[key] = value;
dataStore.set(element, data);
}
function getData(element, key) {
const data = dataStore.get(element) || {};
return key ? data[key] : data;
}
ClojureScript
;; ClojureScript - Using dataset
(def element (gdom/getElement "element"))
;; Set via dataset
(set! (.. element -dataset -userId) "123")
;; Get
(.. element -dataset -userId)
;; For complex data, use atoms or state management
(def data-store (atom {}))
(defn set-data! [element key value]
(swap! data-store assoc-in [element key] value))
(defn get-data [element key]
(get-in @data-store [element key]))
;; Or use a library like reagent for reactive state
AJAX
jQuery: $.ajax() and Shortcuts
// jQuery - Full AJAX
$.ajax({
url: '/api/data',
method: 'GET',
dataType: 'json'
})
.done(function(data) {
console.log('Success:', data);
})
.fail(function(jqXHR, textStatus, error) {
console.error('Error:', error);
})
.always(function() {
console.log('Complete');
});
// jQuery - Shorthand GET
$.get('/api/data', function(data) {
console.log(data);
});
// jQuery - Shorthand POST
$.post('/api/data', { name: 'John' }, function(response) {
console.log(response);
});
// jQuery - Get JSON
$.getJSON('/api/data.json', function(data) {
console.log(data);
});
// jQuery - Get Script
$.getScript('https://example.com/script.js')
.done(function() {
console.log('Script loaded');
});
Google Closure
// Google Closure
goog.require('goog.net.XhrIo');
goog.require('goog.events');
// GET request
goog.net.XhrIo.send('/api/data', function(e) {
const xhr = e.target;
if (xhr.isSuccess()) {
const data = xhr.getResponseJson();
console.log('Success:', data);
} else {
console.error('Error:', xhr.getStatus());
}
console.log('Complete');
});
// POST request
goog.net.XhrIo.send('/api/data', function(e) {
const data = e.target.getResponseJson();
console.log(data);
}, 'POST', 'name=John', {'Content-Type': 'application/x-www-form-urlencoded'});
// With instance for more control
const xhr = new goog.net.XhrIo();
goog.events.listen(xhr, goog.net.EventType.COMPLETE, function(e) {
if (xhr.isSuccess()) {
console.log(xhr.getResponseJson());
}
});
goog.events.listen(xhr, goog.net.EventType.ERROR, function(e) {
console.error('Request failed');
});
xhr.send('/api/data', 'GET');
// JSON with headers
const headers = {'Content-Type': 'application/json'};
goog.net.XhrIo.send('/api/data', callback, 'POST',
JSON.stringify({name: 'John'}), headers);
ClojureScript
;; ClojureScript with goog.net.XhrIo
(require '[goog.net.XhrIo :as xhr])
;; GET request
(xhr/send "/api/data"
(fn [e]
(let [target (.-target e)]
(if (.isSuccess target)
(println "Success:" (.getResponseJson target))
(println "Error:" (.getStatus target))))))
;; POST request
(xhr/send "/api/data"
(fn [e] (println (.getResponseJson (.-target e))))
"POST"
(js/JSON.stringify #js {:name "John"})
#js {"Content-Type" "application/json"})
;; With cljs-ajax library (recommended)
(require '[ajax.core :refer [GET POST]])
(GET "/api/data"
{:handler (fn [response] (println "Success:" response))
:error-handler (fn [error] (println "Error:" error))})
(POST "/api/data"
{:params {:name "John"}
:format :json
:response-format :json
:handler (fn [response] (println response))})
;; With cljs-http and core.async
(require '[cljs-http.client :as http]
'[cljs.core.async :refer [<!]])
(require-macros '[cljs.core.async.macros :refer [go]])
(go
(let [response (<! (http/get "/api/data"))]
(println (:body response))))
jQuery: $.when() - Parallel Requests
// jQuery
function fetchUser() {
return $.ajax('/api/user');
}
function fetchPosts() {
return $.ajax('/api/posts');
}
$.when(fetchUser(), fetchPosts())
.done(function(userData, postsData) {
console.log('User:', userData[0]);
console.log('Posts:', postsData[0]);
})
.fail(function() {
console.error('One or more requests failed');
});
Google Closure
// Google Closure - Using goog.Promise.all (or native Promise.all)
goog.require('goog.Promise');
goog.require('goog.net.XhrIo');
function fetchUser() {
return new goog.Promise(function(resolve, reject) {
goog.net.XhrIo.send('/api/user', function(e) {
if (e.target.isSuccess()) {
resolve(e.target.getResponseJson());
} else {
reject(e.target.getStatus());
}
});
});
}
function fetchPosts() {
return new goog.Promise(function(resolve, reject) {
goog.net.XhrIo.send('/api/posts', function(e) {
if (e.target.isSuccess()) {
resolve(e.target.getResponseJson());
} else {
reject(e.target.getStatus());
}
});
});
}
goog.Promise.all([fetchUser(), fetchPosts()])
.then(function(results) {
console.log('User:', results[0]);
console.log('Posts:', results[1]);
})
.thenCatch(function(error) {
console.error('Failed:', error);
});
ClojureScript
;; ClojureScript with core.async
(require '[cljs-http.client :as http]
'[cljs.core.async :refer [<! go]])
(go
(let [user-chan (http/get "/api/user")
posts-chan (http/get "/api/posts")
user (<! user-chan)
posts (<! posts-chan)]
(println "User:" (:body user))
(println "Posts:" (:body posts))))
;; Or with promesa library
(require '[promesa.core :as p])
(-> (p/all [(fetch-user) (fetch-posts)])
(p/then (fn [[user posts]]
(println "User:" user)
(println "Posts:" posts)))
(p/catch (fn [error]
(println "Failed:" error))))
Deferred/Promises
jQuery: $.Deferred()
// jQuery
function asyncOperation() {
const deferred = $.Deferred();
setTimeout(function() {
const success = Math.random() > 0.5;
if (success) {
deferred.resolve('Operation succeeded');
} else {
deferred.reject('Operation failed');
}
}, 1000);
return deferred.promise();
}
asyncOperation()
.done(function(result) {
console.log(result);
})
.fail(function(error) {
console.error(error);
})
.always(function() {
console.log('Complete');
});
Google Closure
// Google Closure - goog.Promise (or native Promise)
goog.require('goog.Promise');
function asyncOperation() {
return new goog.Promise(function(resolve, reject) {
setTimeout(function() {
const success = Math.random() > 0.5;
if (success) {
resolve('Operation succeeded');
} else {
reject('Operation failed');
}
}, 1000);
});
}
asyncOperation()
.then(function(result) {
console.log(result);
})
.thenCatch(function(error) {
console.error(error);
})
.thenAlways(function() {
console.log('Complete');
});
// Or with native Promise
function asyncOperation() {
return new Promise(function(resolve, reject) {
// ... same as above
});
}
ClojureScript
;; ClojureScript with core.async channels
(require '[cljs.core.async :refer [<! >! chan go timeout]])
(defn async-operation []
(let [c (chan)]
(go
(<! (timeout 1000))
(if (> (rand) 0.5)
(>! c {:status :success :result "Operation succeeded"})
(>! c {:status :error :error "Operation failed"})))
c))
(go
(let [{:keys [status result error]} (<! (async-operation))]
(case status
:success (println result)
:error (println "Error:" error))
(println "Complete")))
;; With promesa library (Promise-based)
(require '[promesa.core :as p])
(defn async-operation []
(p/create
(fn [resolve reject]
(js/setTimeout
(fn []
(if (> (rand) 0.5)
(resolve "Operation succeeded")
(reject "Operation failed")))
1000))))
(-> (async-operation)
(p/then #(println %))
(p/catch #(println "Error:" %))
(p/finally #(println "Complete")))
Callbacks
jQuery: $.Callbacks()
// jQuery - Basic usage
const callbacks = $.Callbacks();
function fn1(value) {
console.log('fn1:', value);
}
function fn2(value) {
console.log('fn2:', value);
}
callbacks.add(fn1);
callbacks.add(fn2);
callbacks.fire('hello');
// Output:
// fn1: hello
// fn2: hello
// With flags
const onceCallbacks = $.Callbacks('once');
onceCallbacks.add(fn1);
onceCallbacks.fire('first'); // fn1: first
onceCallbacks.fire('second'); // (nothing - already fired)
// Memory flag
const memoryCallbacks = $.Callbacks('memory');
memoryCallbacks.fire('done');
memoryCallbacks.add(fn1); // Immediately called with 'done'
Google Closure
// Google Closure - Custom implementation
goog.provide('app.Callbacks');
app.Callbacks = function(flags) {
this.list_ = [];
this.fired_ = false;
this.lastArgs_ = null;
this.once_ = flags && flags.indexOf('once') !== -1;
this.memory_ = flags && flags.indexOf('memory') !== -1;
};
app.Callbacks.prototype.add = function(fn) {
this.list_.push(fn);
if (this.memory_ && this.fired_) {
fn.apply(null, this.lastArgs_);
}
};
app.Callbacks.prototype.fire = function() {
if (this.once_ && this.fired_) return;
this.fired_ = true;
this.lastArgs_ = arguments;
for (var i = 0; i < this.list_.length; i++) {
this.list_[i].apply(null, arguments);
}
};
// Usage
var callbacks = new app.Callbacks();
callbacks.add(fn1);
callbacks.fire('hello');
ClojureScript
;; ClojureScript - Using atoms
(defn create-callbacks [& {:keys [once memory]}]
(let [callbacks (atom [])
fired (atom false)
last-args (atom nil)]
{:add (fn [f]
(swap! callbacks conj f)
(when (and memory @fired)
(apply f @last-args)))
:fire (fn [& args]
(when-not (and once @fired)
(reset! fired true)
(reset! last-args args)
(doseq [f @callbacks]
(apply f args))))}))
;; Usage
(def cbs (create-callbacks))
((:add cbs) (fn [v] (println "fn1:" v)))
((:add cbs) (fn [v] (println "fn2:" v)))
((:fire cbs) "hello")
;; With memory
(def mem-cbs (create-callbacks :memory true))
((:fire mem-cbs) "done")
((:add mem-cbs) (fn [v] (println "Late callback:" v)))
;; Immediately prints: Late callback: done
Animation
jQuery: fadeIn/fadeOut
// jQuery
$('.dynamic-content').fadeIn();
$('.dynamic-content').fadeOut();
$('.dynamic-content').fadeToggle();
// With duration
$('#element').fadeIn(400);
$('#element').fadeOut('slow');
// With callback
$('#element').fadeIn(400, function() {
console.log('Fade complete');
});
Google Closure
// Google Closure
goog.require('goog.fx.dom');
goog.require('goog.events');
const element = goog.dom.getElement('element');
// Fade in
const fadeIn = new goog.fx.dom.FadeIn(element, 400);
fadeIn.play();
// Fade out
const fadeOut = new goog.fx.dom.FadeOut(element, 400);
fadeOut.play();
// With callback
goog.events.listen(fadeIn, goog.fx.Transition.EventType.END, function() {
console.log('Fade complete');
});
// Fade to specific opacity
const fade = new goog.fx.dom.Fade(element, 0, 0.5, 400);
fade.play();
ClojureScript
;; ClojureScript with goog.fx.dom
(require '[goog.fx.dom :as fx])
(import '[goog.fx Transition])
(def element (gdom/getElement "element"))
;; Fade in
(let [anim (fx/FadeIn. element 400)]
(.play anim))
;; Fade out
(let [anim (fx/FadeOut. element 400)]
(.play anim))
;; With callback
(let [anim (fx/FadeIn. element 400)]
(gevents/listen anim (.-END Transition.EventType)
(fn [_] (println "Fade complete")))
(.play anim))
jQuery: slide animations
// jQuery
$('#element').slideDown();
$('#element').slideUp();
$('#element').slideToggle();
// With duration and callback
$('#element').slideDown(400, function() {
console.log('Slide complete');
});
Google Closure
// Google Closure
goog.require('goog.fx.dom');
const element = goog.dom.getElement('element');
const size = goog.style.getSize(element);
// Slide down (from 0 height)
const slideDown = new goog.fx.dom.Resize(
element,
[size.width, 0],
[size.width, size.height],
400
);
slideDown.play();
// Slide up (to 0 height)
const slideUp = new goog.fx.dom.Resize(
element,
[size.width, size.height],
[size.width, 0],
400
);
slideUp.play();
ClojureScript
;; ClojureScript
(require '[goog.fx.dom :as fx]
'[goog.style :as gstyle])
(def element (gdom/getElement "element"))
(def size (gstyle/getSize element))
;; Slide down
(let [anim (fx/Resize. element
#js [(.-width size) 0]
#js [(.-width size) (.-height size)]
400)]
(.play anim))
jQuery: Custom Animations
// jQuery - animate()
$('#element').animate({
opacity: 0.5,
left: '+=50px',
height: 'toggle'
}, 1000, 'swing', function() {
console.log('Animation complete');
});
Google Closure
// Google Closure - Combining animations
goog.require('goog.fx');
goog.require('goog.fx.dom');
goog.require('goog.fx.AnimationParallelQueue');
const element = goog.dom.getElement('element');
// Create parallel animations
const queue = new goog.fx.AnimationParallelQueue();
// Fade to 0.5
queue.add(new goog.fx.dom.Fade(element, 1, 0.5, 1000));
// Move right
const pos = goog.style.getPosition(element);
queue.add(new goog.fx.dom.Slide(
element,
[pos.x, pos.y],
[pos.x + 50, pos.y],
1000
));
goog.events.listen(queue, goog.fx.Transition.EventType.END, function() {
console.log('Animation complete');
});
queue.play();
ClojureScript
;; ClojureScript
(require '[goog.fx :as gfx]
'[goog.fx.dom :as fx])
(import '[goog.fx AnimationParallelQueue Transition])
(def element (gdom/getElement "element"))
(def pos (gstyle/getPosition element))
(let [queue (AnimationParallelQueue.)]
(.add queue (fx/Fade. element 1 0.5 1000))
(.add queue (fx/Slide. element
#js [(.-x pos) (.-y pos)]
#js [(+ 50 (.-x pos)) (.-y pos)]
1000))
(gevents/listen queue (.-END Transition.EventType)
(fn [_] (println "Animation complete")))
(.play queue))
Security Features
jQuery: Prototype Pollution Prevention
// jQuery 4.0 blocks these attempts
$.extend(true, {}, JSON.parse('{"__proto__": {"polluted": true}}'));
$.extend(true, {}, { constructor: { prototype: { polluted: true } } });
// Object.prototype remains unpolluted
console.log({}.polluted); // undefined
Google Closure
// Google Closure - goog.object doesn't have deep merge by default
// Use Object.freeze for protection
const safeDefaults = Object.freeze({
setting1: 'value1'
});
// Manual safe merge
function safeMerge(target, source) {
for (const key in source) {
if (source.hasOwnProperty(key) &&
key !== '__proto__' &&
key !== 'constructor') {
target[key] = source[key];
}
}
return target;
}
ClojureScript
;; ClojureScript - Immutable data structures prevent this by design
;; Clojure maps don't have prototype chain issues
(def defaults {:setting1 "value1"})
(def user-input (js->clj (js/JSON.parse "{\"__proto__\": {\"polluted\": true}}")))
;; merge just treats __proto__ as a regular key
(def merged (merge defaults user-input))
;; => {:setting1 "value1", "__proto__" {"polluted" true}}
;; The JS Object.prototype is unaffected
jQuery: XSS Mitigation
// jQuery - Escape selectors
const userInput = 'div"><script>alert("xss")</script>';
const escaped = $.escapeSelector(userInput);
$(escaped); // Safe
Google Closure
// Google Closure - String escaping
goog.require('goog.string');
goog.require('goog.html.SafeHtml');
// HTML escaping
const escaped = goog.string.htmlEscape(userInput);
// Safe HTML creation
const safeHtml = goog.html.SafeHtml.htmlEscape(userInput);
ClojureScript
;; ClojureScript
(require '[goog.string :as gstring])
;; HTML escaping
(def escaped (gstring/htmlEscape user-input))
;; With hiccup libraries, escaping is automatic
;; (hipo/create [:div user-input]) - auto-escaped
jQuery: Trusted Types Support
// jQuery 4.0 accepts TrustedHTML objects
const policy = trustedTypes.createPolicy('myPolicy', {
createHTML: (string) => DOMPurify.sanitize(string)
});
const trustedContent = policy.createHTML('<p>Safe content</p>');
$('#container').html(trustedContent);
Google Closure
// Google Closure - Built-in safe DOM methods
goog.require('goog.dom.safe');
goog.require('goog.html.SafeHtml');
// Create safe HTML
const safeHtml = goog.html.SafeHtml.create('p', {}, 'Safe content');
// Set inner HTML safely
goog.dom.safe.setInnerHtml(container, safeHtml);
ClojureScript
;; ClojureScript
(require '[goog.dom.safe :as safe]
'[goog.html.SafeHtml :as SafeHtml])
(def safe-html (SafeHtml/create "p" nil "Safe content"))
(safe/setInnerHtml container safe-html)
Summary: Choosing Between Libraries
| Use Case | Recommendation |
|---|---|
| Quick prototyping | jQuery - simplest API |
| Large-scale app | ClojureScript - functional, optimized |
| Legacy codebase | jQuery + migrate plugin |
| Functional programming | ClojureScript |
| Maximum optimization | ClojureScript (Closure Compiler) |
| React/modern framework | Consider native APIs or framework tools |
Migration Path
If migrating from jQuery:
- To Google Closure: Replace jQuery calls with
goog.dom,goog.events,goog.style - To ClojureScript: Use
goog.domnamespace or wrapper libraries likedommy
Key Takeaways
- jQuery provides the most concise syntax for DOM manipulation
- Google Closure offers modular, optimizable code (but is sunset)
- ClojureScript combines functional programming with Closure Library benefits
- Modern browsers support many jQuery features natively (
querySelector,fetch,classList)
Resources
jQuery
Google Closure Library
ClojureScript
Comparison document for jQuery 4.0.0 (January 2026) vs Google Closure Library vs ClojureScript