cljs-ajax

repository·master·Indexed 20 days ago

https://github.com/julianbirch/cljs-ajax

A simple, asynchronous Ajax client for Clojure and ClojureScript. It provides a unified interface for GET, POST, and PUT requests, wrapping XHR/goog.net.XhrIo in ClojureScript and Apache HttpAsyncClient on the JVM. Supports multiple formats including Transit, JSON, text, and raw, with customizable request/response formats and interceptors.

Tokens
6.5K
Snippets
19
Records
37
Agent score
22%

What's inside cljs-ajax

  1. Handle responses with `ajax-request`

    master

    Unlike the high-level GET/POST APIs, ajax-request uses a single :handler function to manage both successful responses and errors. The handler must be a function that accepts a single vector argument in the form [ok result].

    • If ok is true, result contains the successful response data.
    • If ok is false, result contains the error response data.
    (defn handler [[ok response]]
      (if ok
        (println "Success:" response)
        (println "Error:" response)))
    
    (ajax-request {:uri "/api" :method :get :handler handler})
  2. Understand the interceptor life cycle

    master

    The cljs-ajax request/response cycle follows this sequence:

    1. Normalization: The request map is converted to a form accepted by ajax-request and the method is normalized (e.g., :get becomes "GET").
    2. Format Determination: The response format is determined and fixed.
    3. Interceptor Setup: If no interceptors are provided, they are replaced with @default-interceptors. Standard interceptors (:response-format at the start and :request-format at the end) are wrapped around them.
    4. Request Phase: Request interceptors are run via reduce. By the end of this phase, the request should have a :body entry (unless it is a GET).
    5. Execution: The AJAX query is executed, returning an AjaxResponse.
    6. Response Phase: Response interceptors are run in reverse order. This means the :response-format interceptor runs last.
    7. Completion: The :handler is called with the final result.

    Note on reduced: Because interceptors use reduce, you can use reduced to skip remaining interceptors. For response interceptors, you must convert the result to the [ok result] format before calling reduced.

  3. Define custom request and response formats

    master

    The ajax-request function and other main API commands allow you to pass arbitrary maps as request and response formats instead of using keywords. This provides full control over how data is serialized for the request and deserialized from the response.

    Request Format Structure

    A request format map must contain:

    • :content-type: The string content type to send to the server.
    • :write: A function that takes :params and returns a string.

    Response Format Structure

    A response format map contains:

    • :description: A string description used in error messages.
    • :read: A function that takes the underlying goog.net.XhrIo and converts it to a response. Exceptions in this function are caught by the library.
    • :content-type: The content type to include in the Accept header.
    • :type: (Optional) One of :blob, :document, :json (uses browser JSON decoding to JS object), :text, or :arraybuffer. To use :pr/-body, you must add [ajax.protocols :as pr] to your :require list.
    ;; Example of a custom response format to get the raw XhrIo object
    {:read identity :description "raw"}
  4. Handle empty responses

    master

    When using JSON, EDN, or Transit response formats, the library returns ajax.core/empty-response if the response body is completely empty.

    This behavior occurs regardless of the HTTP status code (e.g., 204 No Content or 205 Reset Content).

    Important: This is distinct from an encoded null value. For example, a JSON response body containing null will decode to nil, whereas a truly empty body decodes to ajax.core/empty-response.

  5. Why `cljs-ajax` does not use the `ring` data model

    master

    While the ring model is common in Clojure, cljs-ajax avoids it for two main reasons:

    1. Request Specification: In a client-side request, distinguishing between :params and :json-params is critical. The ring model often blurs these, which can lead to an inconsistent API when adding new features or formats.
    2. Optimization and Extensibility: The ring model typically relies on synchronous wrapping handlers. For a client-side library, this pattern is difficult to extend and is hostile to Google Closure's advanced optimizations.
  6. Why `cljs-ajax` does not use `core.async` by default

    master

    cljs-ajax is designed to be compatible with core.async, but it does not include it as a mandatory dependency. This decision was made to:

    1. Avoid adding unnecessary overhead (callback handlers writing to channels) that doesn't add significant value.
    2. Minimize dependencies to support advanced optimization goals.

    If your project uses core.async, cljs-ajax works well with it, but if you don't need it, you aren't forced to include it.

  7. Handle different data formats in a Ring server

    master

    When building a server to respond to cljs-ajax requests, use the following Ring middleware depending on your chosen data format:

    • Transit: Use ring-transit. It populates the request :params with the contents of the transit request.
    • JSON: Use ring-json. It populates the :body tag. Warning: Do not use ring-json with JSON format GET requests due to potential JSON hijacking vulnerabilities. For lower-level JSON formatting needs, Cheshire is recommended over data.json.
    • EDN: Use ring-edn. It populates the request :params with the contents of the EDN request.
    • Multi-format REST: Use ring-middleware-format for a standardized approach to multiple formats.
  8. System requirements for cljs-ajax development

    master

    To build and test cljs-ajax from source, your machine must have the following installed and available on your PATH:

    • Java JDK
    • Clojure CLI
    • Node.js and npm
    • Google Chrome or Chromium (required for browser tests)
    • bubblewrap

    If Chrome/Chromium is not in your PATH, you can set the CHROME_BIN environment variable to the path of the browser executable.

  9. Build and test cljs-ajax

    master

    Run the following commands from the repository root to execute the full test suite and build the project JAR. Note: Do not run these commands with sudo to avoid permission issues with generated files and caches.

    clojure -X:test
    npm run test:cljs:node
    npm run test:cljs:browser
    npm run test:integration
    clojure -T:build jar
  10. Configure CORS for cross-origin AJAX requests

    master

    Browsers block AJAX requests to a different origin by default. To allow cljs-ajax to communicate with your server from a different domain, you must include Access-Control-Allow-Origin and Access-Control-Allow-Headers in your response.

    It is recommended to use the ring-cors library to wrap your Ring routes with CORS middleware. For Google Chrome specifically, you must include the Access-Control-Allow-Headers header to prevent the browser from stripping the Content-Type header from your requests.

    Note that for non-simple cross-origin requests (like GET or HEAD), the browser will perform a 'preflight' OPTIONS request before the actual target request.

    (require '[ring.middleware.cors :refer [wrap-cors]])
    
    (def allowed-origins [#"https://example-a.com" #"https://example-b.com"])
    
    (def allowed-methods [:get :post :put :delete])
    
    (def allowed-headers #{:accept :content-type})
    
    ;; my-routes already defined somewhere
    
    (def handler
      (wrap-cors my-routes :access-control-allow-origin allowed-origins
                           :access-control-allow-methods allowed-methods
                           :access-control-allow-headers allowed-headers))