{"id":13760429,"url":"https://github.com/tatut/ripley","last_synced_at":"2025-04-05T01:07:44.746Z","repository":{"id":37809759,"uuid":"265867578","full_name":"tatut/ripley","owner":"tatut","description":"Server rendered UIs over WebSockets","archived":false,"fork":false,"pushed_at":"2024-04-03T12:57:24.000Z","size":354,"stargazers_count":300,"open_issues_count":5,"forks_count":10,"subscribers_count":10,"default_branch":"master","last_synced_at":"2024-10-13T22:52:02.204Z","etag":null,"topics":["clojure","websockets"],"latest_commit_sha":null,"homepage":"","language":"Clojure","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/tatut.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-05-21T14:19:25.000Z","updated_at":"2024-09-22T22:11:15.000Z","dependencies_parsed_at":"2024-08-03T13:14:58.100Z","dependency_job_id":null,"html_url":"https://github.com/tatut/ripley","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatut%2Fripley","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatut%2Fripley/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatut%2Fripley/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/tatut%2Fripley/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/tatut","download_url":"https://codeload.github.com/tatut/ripley/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247271530,"owners_count":20911587,"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","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":["clojure","websockets"],"created_at":"2024-08-03T13:01:10.094Z","updated_at":"2025-04-05T01:07:44.739Z","avatar_url":"https://github.com/tatut.png","language":"Clojure","funding_links":[],"categories":["Clojure"],"sub_categories":[],"readme":"# ripley\n\n![test workflow](https://github.com/tatut/ripley/actions/workflows/test.yml/badge.svg)\n\nRipley is a fast server-side rendered web UI toolkit with live components.\n\nCreate rich webapps without the need for a SPA frontend.\n\n## Comparison with SPA\n\nSingle Page Appplications are complicated things, ripley is a traditional server side\nrendered model with websocket enhancement for live parts.\n\nPros:\n- No need for an API just for the frontend\n- No need for client-side state management\n- No need to wait for large JS download before rendering (or setup complicated SSR for client side apps)\n- Leverages browser's native routing\n- No separate backend and frontend build complexity, just use Clojure\n- Use functions and hiccup to build the UI, like Reagent or other ClojureScript React wrappers\n\nCons:\n- Client browser needs constant connection to server\n- Interaction latency is limited by network conditions\n- Unsuitable for serverless cloud platforms\n\n## Usage\n\nUsing ripley is from a regular ring app is easy. You call `ripley.html/render-response` from a ring\nhandler to create a response that sets up a live context.\n\nThe render response takes a root component that will render the page.\nAny rendered live components are registered with the context and updates are sent to the client if\ntheir sources change. Use `ripley.html/html` macro to output HTML with hiccup style markup.\n\nLive components are rendered with the special `:ripley.html/live` element. The live component takes\na source (which implements `ripley.live.protocols/Source`) and a component function. The component\nfunction is called with the value received from the source.\n\n```clojure\n(def counter (atom 0))\n\n(defn counter-app [counter]\n  (h/html\n    [:div\n      \"Counter value: \" [::h/live {:source (atom-source counter)\n                                   :component #(h/html [:span %])}]\n      [:button {:on-click #(swap! counter inc)} \"increment\"]\n      [:button {:on-click #(swap! counter dec)} \"decrement\"]]))\n```\n\nAll event handling attributes (like `:on-click` or `:on-change`) are registered as callbacks\nthat are sent via the websocket to the server. See `ripley.js` namespace for helpers in creating\ncallbacks with more options. You can add debouncing and client side condition and success/failure\nhandlers.\n\nSee more details and fully working example in the examples folder.\n\n## Sources\n\nThe main abstraction for working with live components in ripley is the Source.\nIt provides a way for rendering to get the current value (if available)\nand allows the live context to listen for changes.\n\nThe source value can be an atomic value (like string, number or boolean) or\na map or collection. The interpretation of the value of the source if entirely\nup to the component that is being rendered.\n\n\n### Built-in sources\n\nRipley provides built-in sources that integrate regular Clojure\nmechanisms into sources. Built-in sources don't require any external\nextra dependencies.\n\nYou can create sources by calling the specific constructor functions\nin `ripley.live.source` namespace or the `to-source` multimethod.\n\n\n| Type | Description |\n| ---- | --- |\n| atom | Regular Clojure atoms. Listens to changes with `add-watch`. See: `use-state` |\n| use-state | Convenient light weight source for per render local state |\n| core.async channel | A core async channel |\n| future | Any future value, if realized by render time, used directly. Otherwise patched in after the result is available. |\n| promise | A promise, if delivered by render time, used directly. Otherwise patched in after the promise is delivered. |\n| computed | Takes one or more input sources and a function. Listens to input sources and calls function with their values to update. See also `c=` convenience macros. |\n| split | Takes a map valued input source and keysets. Distributes changes to sub sources only when their keysets change. |\n\n### Integration sources\n\nRipley also contains integration sources that integrate external state into usable sources.\nIntegration sources may need external dependencies (not provided by ripley)\nsee namespace docstring for an integration source in `ripley.integration.\u003ctype\u003e`.\n\n| Type | Description |\n| ---- | --- |\n| redis | Integrate Redis pubsub channels as sources (uses carmine library) |\n| manifold | Integrate manifold library `deferred` and `stream` as source |\n| xtdb | Integrate XTDB query as an automatically updating source |\n\n## Working with components\n\n### Component functions\n\nIn Ripley, components are functions that take in parameters and **output** HTML fragment\nas a side-effect. They do not return a value. This is different from normal hiccup, where\nfunctions would return a hiccup vector describing the HTML.\n\nRipley uses the `ripley.html/html` macro to convert a hiccup style body into plain Clojure\nthat writes HTML. The macro also adds Ripley's internal tracking attributes so components\ncan be updated on the fly.\n\nAny Clojure code can be called inside the body, but take note that return values are discarded.\nThis is a common mistake, forgetting to use the HTML macro in a function and returning a vector.\nThe caller will simply discard it and nothing is output.\n\n### Child components\n\nComponents form a tree so a component can have child components with their own sources. The children\nare registered under the parent and if the parent fully rerenders, the children are recursively\ncleaned up. A component does not need to care if it is at the top level or a child of some other\ncomponent.\n\nThe main consideration comes from the sources used. If the parent component creates per render\nsources for the children, the children will lose the state when the parent is rerendered.\n\n### Dynamic scope\n\nRipley supports capturing dynamic scope that was in place when a component or callback was created.\nThis can be used to avoid passing in every piece of context to all components (like user information\nor db connection pools). The set of vars to capture must be configured when calling\n`ripley.html/render-response`.\n\n### Dev mode\n\nIn development mode, Ripley can be made to replace any `html` macro with an error description panel\nthat shows exception information and the body source of the form.\nThis has some performance penalty as all components will first be output into an in-memory `StringWriter`\ninstead of directly to the response.\n\nDev mode can be enabled with the system property argument `-Dripley.dev-mode=true` or by setting the\n`ripley.html/dev-mode?` atom to true before any `ripley.html/html` macroexpansions take place.\n\n### Client side state\n\nRipley supports a custom attribute `::h/after-replace` in the component's root\nelement. When the component is replaced when the source value changes, this\nJS fragment is evaluated after the DOM update. This can be used to reinitialize\nany client side scripts that are attached to this component. The DOM element that\nwas replaced is bound to `this` during evaluation.\n\n## Changes\n\n### 2025-03-31\n- Fix: when the last listener of a computed source unlistens, close the source\n\n### 2024-04-03\n- Use new Function instead of eval for after replace JS code\n\n### 2024-04-02\n- Add `::h/after-replace` attribute (see Client side state above)\n\n### 2024-03-18\n- Add `inert` boolean attribute\n\n### 2024-03-06\n- Dev mode: replace component with an error display when an exception is thrown\n\n### 2023-12-27\n- Bugfix: also cleanup source that is only used via other computed sources\n\n### 2023-12-05\n- Bugfix: handle callback arities correctly when using bindings and no success handler\n\n### 2023-09-21\n- Add support for undertow server (thanks @zekzekus)\n\n### 2023-09-20\n- Bugfix: proper live collection cleanup on long-lived source (like atom)\n\n### 2023-09-19\n- Bugfix: support 0-arity callbacks when wrapping failure/success handlers\n\n### 2023-09-16\n- Alternate server implementation support (with pedestal+jetty implementation)\n\n### 2023-09-09\n- Support dynamic binding capture\n\n### 2023-09-02\n- Support a `::h/live-let` directive that is more concise\n\n### 2023-08-30\n- Log errors in component render and callback processing\n\n### 2023-08-26\n- Fix bug in live collection cleanup not being called\n\n### 2023-08-04\n- Fix computed source when calculation fn is not pure (eg. uses current time)\n\n### 2023-07-01\n- Added `ripley.js/export-callbacks` to conveniently expose server functions as JS functions\n- Added `static` utility to use a static value as a source\n\n### 2023-06-28\n- Source value can be `nil` now and component is replaced with placeholder\n\n### 2023-06-10\n- Support client side success and failure callbacks\n\n### 2023-06-07\n- `ripley.html/compile-special` is now a multimethod and can be extended\n\n### 2023-03-18\n- Support specifying `:should-update?` in `::h/live`\n- `use-state` now returns a third value (update-state! callback)\n\n### See commit log for older changes\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftatut%2Fripley","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Ftatut%2Fripley","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Ftatut%2Fripley/lists"}