Giter VIP home page Giter VIP logo

zero's Introduction

Clojars Project Test Badge cljdoc badge

Zero

A toolkit for building web components in Clojure and ClojureScript.

What Can I Build with Zero?

Simple extensions to the browser. Extend the browser as a hypermedia client. Do you wish the form element would update part of the DOM rather than reload the whole page? Build a custom component that does just that.

Design Systems. Web components are great for design systems. Build a design system in Clojure(Script), and share it with other teams regardless of the stack they use.

Micro-frontends. Web components are ideal for micro-frontends. Your team can use Zero alongside other teams using React or Svelte - in the same application.

State-heavy SPAs. Zero provides a set of state management tools from the simple and easy to the simple and sophisticated. Start easy and scale up as you need to.

Highlights

  • Depends only on ClojureScript core
  • Small, with optional 'extras' modules for more features
  • Hot reload friendly
  • Focus preserving updates
  • 'Good enough' performance

Rationale

  • Web components have many advantages over React and similar frameworks, including:
    • Easy to render server-side
    • Seamless interop with other frameworks and libraries
    • Encapsulation, no style bleed, often better performance
    • Native DevTools support
  • Web components are cool, but the native API for building them isn't very convenient
    • Zero makes web components easy

Reactive State

Zero gives you tools for reactive state. You can choose to stick close to the DOM, following HATEOS principles. You can use a central in-memory application database similar to re-frame. Or you can choose from intermediate options -- how you manage state is up to you.

Let's show the different options using the same example: an incrementing button. We want a button that reads "Clicked 0 Times". Every time you click it, it should increment the counter.

You can render this button wherever you choose: in React, Svelte, pure HTML. It looks like this:

<increment-button clicks="0"></increment-button>

This can be implemented a few different ways. We will consider several examples, moving from the low level to the high level.

Manual Event Handling

Zero makes custom component reactive. Similar to React's notion that a view is a function of its props, we can render a web component as a function of its properties.

(ns increment-counter
  (:require [zero.core :as z]
            [zero.config :as zc]
            [zero.component]))

(defn on-click
  [event]
  (let [increment-button (.-host (.-currentTarget event))
        clicks           (js/parseInt (.-clicks increment-button))]
    (set! (.-clicks increment-button) (inc clicks))))

(defn button-view
  [{:keys [clicks]}]
  [:root> {::z/on {:click on-click}}
   [:button (str "Clicked " clicks " times")]])

(zc/reg-components
 :incrementing-button {:view button-view
                       :props #{:clicks}})

When we register our component, we declare clicks as a prop. When we update the incrementing-button's clicks property, it will re-render.

To update our component, we use use the zero.core/on prop on the root to listen for click events. When the click occurs, on-click is called, and it increments the incrementing-button's clicks property. incrementing-button re-renders automatically.

Atoms

We can also use ClojureScript's built-in tools for state. Let's look at an example using an atom.

(ns increment-counter
  (:require [zero.core :as z]
            [zero.config :as zc]
            [zero.component]))

(defonce clicks*
  (atom 0))

(defn on-click
  [_event]
  (swap! clicks* inc))

(defn button-view
  [{:keys [clicks]}]
  [:root> {::z/on {:click on-click}}
   [:button (str "Clicked " clicks " times")]])

(zc/reg-components
 :incrementing-button {:view button-view
                       :props {:clicks clicks*}})

Here we have bound the clicks prop to an atom. Similar to reagent, when that atom updates, our incrementing-button component re-renders.

App DB

Zero also provides facilities for state management that resemble re-frame.

(ns increment-counter.client
  (:require [zero.core :as z]
            [zero.config :as zc]
            [zero.component]
            [zero.extras.db :as db]))

;; Init db value
(db/patch! [{:path [:clicks] :value 0}])

(defn button-view
  [{:keys [clicks]}]
  [:root> {::z/on {:click (z/act [::db/patch [{:path [:clicks]
                                               :fn inc}]])}}
   [:button (str "Clicked " clicks " times")]])

(zc/reg-components
 :incrementing-button {:view button-view
                       :props {:clicks (z/bnd ::db/path [:clicks])}})

Here we have an in-memory database for our application. We bind our clicks prop to a path in the database, and then use the ::db/patch effect to update the value at the :clicks path.

And Beyond

Zero aims to be both simple and easy. It gives you options to follow familiar patterns for state management. But it also gives you the flexibility to manage state as you need to.

For more details, check out the User's Guide. The SSR Demo application provides further examples of Zero's state management.

Warning

Depends on modern browser APIs, works on the latest versions of all major browsers... but will break in some not-so-old versions. In particular, it depends on:

Learning

Check out the Zero User's Guide for a (reasonably) complete guide. Also, there are a few demos:

Feel free to DM me @Ray Stubbs in the Clojurians Slack for any questions.

zero's People

Contributors

raystubbs avatar thomascothran avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar

zero's Issues

Fix Logging

Right now the logging situation within Zero is pretty sad (stupid formatting). I didn't want Zero to have to depend on a logging library... but also don't want to build a full featured logging library into Zero. Instead I think Zero's logger should look dynamically check for existing logging libraries in the project. If any exist, it should use/wrap one of them. Otherwise it'll fallback to something very basic.

Hot Reload for component CSS

Currently hot reload for CSS URL's found in components will only work if the same URL is also referenced from the top level DOM. This is because those top level links are the only ones that shadow-cljs and figwheel will hot reload; so we observe them to determine when to update the component CSS.

But I have an idea that may work to enable hot reload for CSS not referenced in the top level.

The Idea:
For any CSS URLs referenced from a component and not the top level, dynamically add a link in the top level, with a media query that always fails. This will give the hot reload engine and Zero a node to synchronize on, without affecting the styling of the top level document.

Better Example In Readme

Need to think up a better example for the readme. Current one is convoluted and makes Zero look less capable + more complicated to use than it is.

Declarative Shadow DOM

Support declarative shadow DOM.

  • Add support for SSR pre-rendering of components to a declarative shadow DOM via zero.html/html
  • Make components respect existing shadow DOM when setting up on the client side (right now we blindly .attachShadow)

This actually (I hope/think) shouldn't be that difficult. Most pieces are already in place to support it.

Data stream updates need to be throttled

We repeat a lot of work, and can end up with some inconsistencies in binding values with the way things are now. An alternative is to have an actual dependency graph, but we'd give up some flexibility: it would turn into a pull based (instead of push based) system, and working with normal atoms would be more difficult. And the API would have to change.

Solution

  1. When a stream's rx is called, save the new value as pending (derefs don't get this value) and schedule a flush (maybe in 5ms).
  2. On flush, swap out current values with pending values, and call watchers with the new values. Pending values produced by rx calls during the flush must also be flushed.

Render order matters

Right now, when a component instance's props change, it's placed in a set of 'dirty elements', to be updated the next time Zero renders.

While this works in most cases. It isn't always good enough.

For example if a parent and child are both in the dirty set; which one renders first? Undefined. What if the parent removes the child on this render? Then we do unneeded work, and can have some other weirdness. What if the parent changes the child's props on this render? Again, we duplicate the work.

So, we need a predictable render order; preferably with children coming after parents. One option is to give each a sequence number on creation, then sort the the dirty set by this before rendering.

Declarative shadow DOM should be opt-in only

Trying to render all registered components with declarative shadow dom may cause some issues. I think it should be disabled unless a component explicitly opts-in by setting ::html/render? true.

Support `extends` option

Previously I'd thought the extends option for custom element definitions wasn't supported by several browsers, so didn't expose it in Zero. Turns out only WebKit/Safari doesn't support it these days, and there's a polyfill for it.

I myself would probably avoid using this feature, both because a polyfill is required, and it sounds like there are some gotchas to it (someone here mentions shadow DOM doesn't work for all customized built-in elements).

This issue has two parts:

  • Research the implications, gotchas, limitations with exposing this feature in Zero
  • Actually expose it in a useful way

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.