├── .gitignore ├── README.md ├── deps.edn ├── doc ├── arborist-protocols.md ├── arch │ ├── arborist-protocols.md │ ├── big-picture.md │ ├── collections.md │ ├── composability.md │ ├── forms.md │ └── scheduler.md ├── async.md ├── components.md ├── events.md ├── fragments.md ├── reusable-components.md ├── what-the-heck-just-happened.md └── worker.md ├── examples ├── app │ ├── .gitignore │ └── public │ │ ├── css │ │ ├── codemirror.css │ │ └── tailwind.min.css │ │ └── index.html ├── bench-fragment │ ├── .gitignore │ └── index.html ├── conduit │ ├── .gitignore │ ├── index.html │ └── manifest.json ├── fulcro │ ├── .gitignore │ └── index.html ├── reusable-components │ ├── .gitignore │ ├── package-lock.json │ ├── package.json │ ├── public │ │ ├── css │ │ │ ├── material-design-iconic-font.min.css │ │ │ ├── re-com.css │ │ │ └── tailwind.min.css │ │ └── index.html │ ├── shadow-cljs.edn │ └── src │ │ └── main │ │ └── test │ │ └── samples.cljs ├── suspense │ ├── .gitignore │ ├── css │ │ └── tailwind.min.css │ └── index.html ├── todomvc-split │ ├── .gitignore │ ├── index.html │ ├── learn.json │ ├── loading.svg │ └── vendor │ │ ├── todomvc-app-css │ │ ├── index.css │ │ ├── license │ │ ├── package.json │ │ └── readme.md │ │ └── todomvc-common │ │ ├── base.css │ │ ├── base.js │ │ ├── license │ │ ├── package.json │ │ └── readme.md ├── todomvc │ ├── .gitignore │ ├── index.html │ ├── learn.json │ ├── loading.svg │ └── vendor │ │ ├── todomvc-app-css │ │ ├── index.css │ │ ├── license │ │ ├── package.json │ │ └── readme.md │ │ └── todomvc-common │ │ ├── base.css │ │ ├── base.js │ │ ├── license │ │ ├── package.json │ │ └── readme.md ├── ui │ ├── .gitignore │ └── index.html └── website │ └── dev.edn ├── package-lock.json ├── package.json ├── packages └── examples │ ├── package-lock.json │ └── package.json ├── project.clj ├── public └── index.html ├── shadow-cljs.edn └── src ├── dev ├── dummy │ ├── services │ │ └── foo.clj │ ├── suspense.cljs │ ├── system.clj │ ├── ui.cljs │ └── website.clj ├── shadow │ └── experiments │ │ └── grove │ │ └── test_app │ │ └── dom.cljs └── todomvc │ ├── model.cljs │ ├── simple.cljs │ └── split │ ├── db.cljs │ ├── env.cljs │ ├── main.cljs │ ├── single.cljs │ ├── views.cljs │ └── worker.cljs ├── main └── shadow │ └── experiments │ ├── arborist.clj │ ├── arborist.cljs │ ├── arborist │ ├── attributes.cljs │ ├── collections.clj │ ├── collections.cljs │ ├── common.cljs │ ├── dom_scheduler.clj │ ├── dom_scheduler.cljs │ ├── fragments.cljc │ ├── fragments.cljs │ ├── interpreted.cljs │ └── protocols.cljs │ ├── archetype │ └── website.clj │ ├── grove.clj │ ├── grove.cljs │ ├── grove │ ├── cards │ │ ├── env.cljs │ │ └── runner.cljs │ ├── components.clj │ ├── components.cljs │ ├── css_transition.cljs │ ├── db.cljc │ ├── dev_support.cljs │ ├── edn.cljs │ ├── effects.cljs │ ├── eql_query.cljc │ ├── event_fsm.cljs │ ├── events.clj │ ├── events.cljs │ ├── examples │ │ ├── app.cljs │ │ ├── cljs_editor.cljs │ │ ├── env.cljs │ │ ├── js_editor.cljs │ │ └── model.cljs │ ├── history.cljs │ ├── http_fx.cljs │ ├── keyboard.cljs │ ├── local.cljs │ ├── preload.cljs │ ├── protocols.cljs │ ├── runtime.clj │ ├── runtime.cljs │ ├── server.clj │ ├── timeouts.cljs │ ├── transit.cljs │ ├── ui │ │ ├── atoms.cljs │ │ ├── data.cljs │ │ ├── dnd_sortable.cljs │ │ ├── forms.cljs │ │ ├── grid.cljs │ │ ├── loadable.clj │ │ ├── loadable.cljs │ │ ├── portal.cljs │ │ ├── suspense.cljs │ │ ├── testing.cljs │ │ ├── util.cljs │ │ └── vlist.cljs │ └── websocket_engine.cljs │ ├── system.clj │ ├── system │ └── runtime.clj │ └── system_dev.clj └── test └── shadow └── experiments ├── arborist ├── components.js ├── keyed_seq_test.cljs └── wc_test.cljs ├── arborist_test.clj └── grove ├── bench_db.cljs ├── bench_fragment.cljs ├── builder_test.clj ├── collections_test.clj ├── db_test.clj ├── forms_test.cljs ├── html_test.clj ├── others ├── builder.clj ├── builder.cljs ├── html.clj ├── html.cljs ├── protocols.cljc ├── react.clj └── react.cljs └── react ├── bench_dom.cljs └── dump.cljs /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /tmp 6 | /checkouts 7 | /src/gen 8 | /out 9 | 10 | /.classpath 11 | /.project 12 | /.settings 13 | /.cpcache 14 | 15 | pom.xml 16 | pom.xml.asc 17 | *.iml 18 | *.jar 19 | *.log 20 | .shadow-cljs 21 | .idea 22 | .lein-* 23 | .nrepl-* 24 | .DS_Store 25 | 26 | .hgignore 27 | .hg/ 28 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths 2 | ["src/dev" 3 | "src/main" 4 | "src/test"] 5 | 6 | :deps 7 | {} 8 | 9 | :aliases 10 | {:server 11 | {:extra-deps {thheller/shadow-cljs {:mvn/version "2.17.8"}} 12 | :jvm-opts ["-Dfile.encoding=UTF-8"] 13 | :main-opts ["-m" "shadow.cljs.devtools.cli" "clj-repl"]}}} 14 | -------------------------------------------------------------------------------- /doc/arch/collections.md: -------------------------------------------------------------------------------- 1 | # Collections 2 | 3 | As previously described the core Arborist protocol only covers how elements are created, updated and destroyed. There was no mention how collections of elements are handled. The core implementation indeed doesn't cover collections of elements at all. Instead this is left to specialized implementations of the core Arborist protocol. All other implementations as such only handle a single node in the tree and they look just like any other node. 4 | 5 | Other react-like implementations usually only have one algorithm for dealing with collections. So end users create an array of elements and the library handles dealing with them in a uniform way. As far as I can tell there is no way to teach these libraries about new algos. 6 | 7 | In Arborist there are 2 core implementations and a couple more specialized ones for certain situations. 8 | 9 | 10 | ## simple-seq 11 | 12 | `simple-seq` is a very basic implementation rendering elements in order. It makes no attempts to minimize the DOM operations in any way. This is by far the fastest method of rendering a collection of elements that very rarely change. 13 | 14 | Say you want to get this piece of DOM from a simple `["a" "b" "c"]` vector. 15 | 16 | ```html 17 | 22 | ``` 23 | With `simple-seq` that becomes 24 | ```clojure 25 | (<< [:ul 26 | (sg/simple-seq ["a" "b" "c"] 27 | (fn [item] 28 | (<< [:li item])))]) 29 | ``` 30 | 31 | Append only collections can also be very efficient using this but anything that changes the order or deletes items at the start or middle will end up doing a lot more DOM operation than its more optimized variant the `keyed-seq`. 32 | 33 | ## keyed-seq 34 | 35 | This is basically the more commonly key-based algorithm many other implemenations such as react use. Each element in the collection needs to supply a key and that key is used to re-order DOM elements instead of rendering over them. When Element #5 was moved to #2 the actual DOM element is just moved, instead of rendering the contents of #5 into what was previously #2. 36 | 37 | However instead of the developer providing elements with a key, the `keyed-seq` instead will just take the regular collection and a key-fn to it can construct the necessary keys itself. It also happens to safe one iteration over all the elements, which saves a bit of time. 38 | 39 | Collections such as `["a" "b" "c"]` that don't have a natural key are probably better handled by `simple-seq`. However many collections you may end up working with might have a natural key already, so we just use them. 40 | 41 | ```clojure 42 | (def data 43 | [{:id 1 :text "a"} 44 | {:id 2 :text "b"} 45 | {:id 3 :text "c"}]) 46 | 47 | (<< [:ul 48 | (sg/keyed-seq data :id 49 | (fn [item] 50 | (<< [:li (:text item)])))]) 51 | ``` 52 | 53 | `keyed-seq` here takes the additional `:id` arguments which in this case is the `key-fn`. Since keywords implement the core clojure IFn protocol they can just be called as a function, so essentially this will extract the `:id` from each item in the collection and use that for `keyed-seq` purposes. And function taking one argument (the collection item) is valid here. `identity` is fine too. 54 | 55 | Note that the above example is misleading. `data` is fixed and cannot change, so even though the collection has a natural key using `simple-seq` here would still be more efficient. Just imagine for a sec that `data` isn't actually fixed and may be changing while visible on screen. 56 | 57 | ## simple-seq vs keyed-seq 58 | 59 | It is absolutely fine to only use `simple-seq` and only used `keyeq-seq` to optimize certain places. 60 | 61 | Whether the overhead of `keyed-seq` is actually worth it largely depends on how often the collection is actually modified. The more items are added, removed or re-ordered the more relevant it becomes. 62 | 63 | ## reagent/react comparison 64 | 65 | In react-based libraries such as reagent very commonly `for` or `map` are used to render collections. 66 | 67 | ```clojure 68 | ;; grove 69 | (<< [:ul 70 | (sg/simple-seq ["a" "b" "c"] 71 | (fn [item] 72 | (<< [:li item])))]) 73 | 74 | ;; reagent 75 | [:ul 76 | (for [item ["a" "b" "c"]] 77 | [:li item])] 78 | ``` 79 | 80 | While this looks a little shorter syntax wise this has a couple of problems. First we really can't have laziness here at all. All collections must be forced in the render phase. If react for example would delay forcing this collection, maybe due to concurrent mode, things will get hairy very quickly. 81 | 82 | Secondly react will yell at you since no key is provided. So often you need to invent a key here. I have seen apps that either just use the `item` itself, using the index together with `map-indexed` or something worse such as `(random-uuid)`. These defeat the purpose of keys completely and nullify all they are meant to optimize. I have also seen the alternative of using `into`, such as 83 | 84 | ```clojure 85 | (into [:ul] 86 | (for [item ["a" "b" "c"]] 87 | [:li item])) 88 | ``` 89 | 90 | This is reasonable since it makes react happy. As far as it is concerned it is no longer seeing a collection, just some elements. However, given how reagent works this is much less efficient and leads to many more iterations of the collection than necessary. 91 | 92 | # Conclusion 93 | 94 | I think `simple-seq` and `keyed-seq` provide a reasonable alternative that still look friendly enough as to not miss `for` too much. An alternative macro might be useful. 95 | 96 | The burden is on the developer to pick the best variant but using either is fine for most cases. This is fine and also leaves the door open for more specialized implementations such as virtual lists. Or maybe collections that can be sorted via drag&drop. They can also do what is best for them and can re-use the existing implementations or just to something entirely custom. -------------------------------------------------------------------------------- /doc/arch/composability.md: -------------------------------------------------------------------------------- 1 | # Composability 2 | 3 | One major goal the arborist protocols are designed for is that everything composes effortlessly together. Let me use an example to show what I mean by that. 4 | 5 | Suppose there is a button in your UI that you want to use in several places. 6 | ```clojure 7 | (defn ui-fancy-button [ev label] 8 | (<< [:button {:class "fancy-button" :on-click ev} label])) 9 | ``` 10 | 11 | `:on-click` it should trigger an event and it should use a text `label`. I'm trying to keep this example as simple as possible. You can make buttons much more complicated if you want to. 12 | 13 | One might use it like 14 | 15 | ```clojure 16 | (defn ui-example [] 17 | (<< [:div "My UI"] 18 | [:div "look at my fancy button: " 19 | (ui-fancy-button ::click! "Click me!")])) 20 | ``` 21 | 22 | Simple enough. Events are allowed to be keywords or maps with an `:e` keyword. 23 | 24 | 25 | ```clojure 26 | (ui-fancy-button {:e ::click! :some "data"} 27 | "Click me!") 28 | ``` 29 | 30 | Since events are just data they compose just fine. 31 | 32 | The label however is a different story. Naively you might restrict this to be a `string?` only, which on the surface certainly looks reasonable. However, since everything is meant to compose this is also valid. 33 | 34 | ```clojure 35 | (ui-fancy-button {:e ::click! :some "data"} 36 | (<< "You should " [:b "Click me!"])) 37 | ``` 38 | 39 | So, now this is no longer just a string but "actual" HTML, yielding the expected DOM structure. (`:on-click` omitted) 40 | 41 | ```html 42 | 43 | ``` 44 | 45 | So, the fragments compose nicely with each other and can be passed as arguments just fine. Of course this isn't limited to fragments. You could pass in a component, `sg/keyed-seq` and all the other arborist protocol implementations as well. 46 | 47 | Since fragments also cover passing multiple children it is never necessary to fall back to CLJS varargs and splicing children in somehow. Each function/component can just take one "child" argument and that covers zero or more actual children being passed. -------------------------------------------------------------------------------- /doc/arch/forms.md: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | Forms are one of the most essential things in frontend UIs. Just displaying data is quite boring. At some point you'll want to let your users modify it in some way. 4 | 5 | Although all the mechanisms for working with forms are present in `shadow-grove` currently I consider it a substantial missing piece to have no first class form support. Forms can get complex rather quickly and unreasonably complex if you factor in accessibility concerns (eg. `aria-*` attributes). 6 | 7 | ## Form Problem: Rendering 8 | 9 | There are certain aspects in the rendering mode used by `shadow-grove` (as well as `react` and others) where you end up in a render cycle that can be problematic. 10 | 11 | ```clojure 12 | (defc ui-form [] 13 | (bind data-ref (atom {:hello "world"})) 14 | (bind {:keys [hello] :as data} (sg/watch data-ref)) 15 | 16 | (render 17 | (<< [:form {:on-submit ::submit!} 18 | [:label {:for "hello"} "Hello: "] 19 | [:input {:id "hello" :type "text" :value hello :on-input ::input!}] 20 | [:button {:type "submit"} "Go!"]])) 21 | 22 | (event ::submit! [env ev e] 23 | (js/console.log "submit" data)) 24 | 25 | (event ::input! [env ev e] 26 | (swap! data-ref assoc :hello (.. e -target -value)))) 27 | ``` 28 | 29 | I'm using a local `atom` here, which I consider a total anti-pattern, but it is useful for keeping this example short and concise. The same problem arises when using the properly normalized db and EQL queries. The steps that occur are: 30 | 31 | - On first mount the `data-ref` atom is created with the initial state of `{:hello "world"}`. This will only run once. 32 | - The second `bind` will watch the first binding `data-ref`. It will trigger whenever the value in `data-ref` is modified. 33 | - It gets the `:hello` value out of the data and the `render` uses it to set the `:value` of the input. 34 | - The `input` event of the text input will trigger with any input made. So suppose we add a `!`. It'll trigger the `::input!` event with `(.. e -target -value)` being `hello!`. 35 | - We then update the `data-ref` accordingly, which will in turn trigger the second hook to update. 36 | - Since `hello` changed we will also and up in `render` again to set `:value` to `"hello!"`. 37 | 38 | As you may have guessed this render was entirely unnecessary. It was already at that value since it originated from there. 39 | 40 | This can get way out of control if you do something async (eg. talk to a server) which may take some time. If the user continues typing you now may need to abort that work. At the very least you need to make sure you don't end up resetting the value back to something outdated. Just debouncing the event is not a solution. 41 | 42 | One way for dealing with this in `react` is via "controlled" and "uncontrolled" components. You could do the same in `shadow-grove` but I consider this lacking in several regards. 43 | 44 | ## Form Goals 45 | 46 | - Avoid unnecessary renders. 47 | - Deal with `id` and `aria-*` related attributes in some declarative manner. 48 | - Neatly integrated into the core library 49 | - Extensible. Probably protocol based so custom input components can be created. 50 | - Composable. Nesting form inputs is common, one form may become an attribute of another form. 51 | - Validation. HTML standard form validation is not enough, full current form state needs to be considered for some validations. 52 | - Error Messages. Ideally should also cover with error messages in some way, although that may be best dealt with directly in render. 53 | 54 | **I have not yet settled on something for this.** For now everything is in the exact same place that `react` has been in since release. It is definitely workable but I'd like something better integrated and less manual. -------------------------------------------------------------------------------- /doc/events.md: -------------------------------------------------------------------------------- 1 | ## Thoughts on DOM Events 2 | 3 | In React all events are handled by functions. Creating these functions in "render" is problematic because the engine can only see that they are different and accordingly must update the DOM to apply a new handler. 4 | 5 | 6 | ``` 7 | [:div {:onClick (fn [e] ...)} ...] 8 | ``` 9 | 10 | To make things a bit cleaner in React you'd move them to a hook or class-bound fn so the engine can "see" that fns didn't change and skip the DOM update. But that makes handling these functions a bit harder since without a class we can't have the bound fn and hooks need to manage captured state properly (easy to miss). 11 | 12 | ``` 13 |
...
14 | ``` 15 | 16 | This makes it impossible to pass arguments to the event handler. It must manage that via state/props or component attributes. Thats not necessarily a bad thing but may require creating multiple event fns that then dispatch to some common implementation. 17 | 18 | ``` 19 |
...
20 |
...
21 | ``` 22 | 23 | 24 | In `shadow.arborist` I want to discourage the inline-fn as much as possible since its the worst case and breaks the "declarative" nature of hiccup completely. 25 | 26 | One approach I currently prefer is using a vector and a keyword with additional optional args. 27 | 28 | ``` 29 | [:div {:on-click [::foo! 1 2 3]} ...] 30 | ``` 31 | 32 | If the arguments are completely static the fragment macro detects that and never needs to update the event listener on the actual DOM node. 33 | 34 | Args however would still cause an update in each render though. 35 | 36 | ``` 37 | [:div {:on-click [::foo! some-arg]} ...] 38 | ``` 39 | 40 | Not worth worrying about since its cheap enough BUT there may be cases where render can be skipped completely if only `some-arg` changed. 41 | 42 | ``` 43 | (defc ui-example [{:keys [some-arg] :as props}] 44 | [::foo! 45 | (fn [env e] (js/console.log e some-arg))] 46 | 47 | (<< [:div {:on-click [::foo!]} ...])) 48 | ``` 49 | 50 | In this the hooks would trigger the update but since the body is not using the changed `some-arg` the render and DOM update is skipped completely. Not sure how common this would be. Probably unlikely. 51 | 52 | ``` 53 | (defc ui-example [{:keys [some-arg] :as props}] 54 | [foo! (some-hook-creating-event-fn)] 55 | 56 | (<< [:div {:on-click [foo!]} ...])) 57 | ``` 58 | 59 | This is currently also allowed. Not a fan but makes it easier to pass around event handlers to child components and so on. 60 | 61 | Overall this should have all the power of inline functions without the "mess". Keyword events also make it a bit easier to move events out of the component into some kind of multi-method or general "transaction" system. The only reason to process events in the components is to extract the required data out of the DOM event `e` (eg. `e.target.value`). -------------------------------------------------------------------------------- /doc/fragments.md: -------------------------------------------------------------------------------- 1 | ## Thoughts on Fragments 2 | 3 | Fragments are "optimized" Hiccup so that it doesn't have to allocate the entire structure each render and can also skip over the diffing as much as possible. 4 | 5 | 6 | ``` 7 | (defn ui-fragment [v] 8 | (<< [:div.card 9 | [:div.card-title "title"] 10 | [:div.card-body v] 11 | (when v 12 | (<< [:div.card-footer 13 | [:div.card-actions 14 | [:button "ok" v] 15 | [:button "cancel"]]]))])) 16 | ``` 17 | 18 | I kinda like `<<` but `<>` or `html` would also work. Naming is hard. 19 | 20 | 21 | ``` 22 | (defn ui-hiccup [v] 23 | [:div.card 24 | [:div.card-title "title"] 25 | [:div.card-body v] 26 | (when v 27 | [:div.card-footer 28 | [:div.card-actions 29 | [:button "ok" v] 30 | [:button "cancel"]]])]) 31 | ``` 32 | 33 | "Interpreted" hiccup won't be enabled by default but it is easy to support. Just 2-10x slower for simple example and lots more for complex examples using actual maps as props. It just has to allocate too much and diff too much in each render. Same as React. With the fragment macro it can detect most of it at compile time and emit optimized create functions and update functions that skip over the "static" things. Similar to Svelte. 34 | 35 | The question is how smart should be macro be. I prefer it to be kinda dumb in that it only analyzes vectors and as soon as it encounters anything else it backs off and treats everything as normal code. In the fragment example however that means then `(when v ...)` stops the fragment processing so the body needs to use `<<` again. For me this is natural already but may confuse people used to Hiccup/Reagent. With a good runtime error this should be easy enough to adapt to. 36 | 37 | There could be a "strict" variant of the fragment macro that only allows a basic subset of clojure code inside the fragment itself. So that it doesn't allow most forms but knows how to process `if` and `when` and maybe `for` or so. Similar to other directives in vue/svelte/etc. So that fragments don't allow ALL clojure code since that is too hard to actually analyze at compile time. We also need `<<` anyways when passing fragments as args and so on. Easy to detect `[:div {:attr (some-code) ...]` not so easy to detect 38 | 39 | ``` 40 | [:div 41 | (some-code-that-has-a-nested-fragment 42 | [:div ...])] 43 | ``` 44 | 45 | 46 | ## Components in Fragments 47 | 48 | Hiccup and JSX have special syntax for Components too look like regular DOM elements. That is not a pattern I want to repeat. For one there should be no difference between a regular function call (returning anything renderable) and a component (stateful, with lifecycle). 49 | 50 | ``` 51 | (<< [:div "before" [some-component foo] "after"]) 52 | ``` 53 | 54 | This is ambigious to parse since we can't know if `foo` is a map of props or something renderable like a string. 55 | 56 | ``` 57 | (<< [:div "before" (some-component foo) "after"]) 58 | ``` 59 | 60 | works just as well and makes it clearer that hiccup notation is for DOM elements only and everything else is actual "code". 61 | 62 | The only area where allowing dynamic "tags" is interesting is for passing nested elements. 63 | 64 | ``` 65 | [:div 66 | [some-component {:static "props} 67 | [:div ...]]] 68 | ``` 69 | 70 | React doesn't support this at all and requires passing functions. I think that is fine but makes diffing problematic again. I actually prefer the web components "slot" method that could be easily adapted. 71 | 72 | ``` 73 | [:div 74 | [:> (some-component {:static "props"}) 75 | [:div ...]]] 76 | ``` 77 | 78 | `:>` for components and nested elements for slot content where the component itself just leaves a marker where that content should go similar to web components. 79 | 80 | ``` 81 | (defc some-components [props] 82 | [] 83 | (<< [:div "component-content" (sc/slot)])) 84 | ``` 85 | 86 | Slots could also be named to allow multiple. Without shadow/light DOM separation however I'm not sure how useful this actually is. Not sure how useful this is but I do like the idea. Maybe it just feels weird because React doesn't have it. I can't think of a situation where I'd use it because of using React too much? It just feels better to pass data to the actual component and let it render stuff however it needs? 87 | -------------------------------------------------------------------------------- /doc/worker.md: -------------------------------------------------------------------------------- 1 | ## Write framework using a web-worker? 2 | 3 | In a discussion with @jacekschae about web frameworks in general and why Workers aren't used more I just dismissed it outright claiming that serializing data back and forth adds more overall cost than what you'd gain by moving code into a worker. Shortly after I however realized that I didn't test this in a long time and the last time I tested this was using EDN and naively just shipping the entire app-state map in each frame. That of course blew the 16ms budget with even very small app-state maps. 4 | 5 | I decided to revisit my assumptions about all of this and starting adapting the "grove" framework to allow moving all "application" code to a worker and only keeping UI "views" in the main thread. That actually turned out to be real easy given that the component code already is designed to abstract away async code as much as possible. 6 | 7 | There are now two variants for "todomvc". One using the worker split and one with no split. 8 | 9 | - https://code.thheller.com/demos/todomvc/ 10 | - https://code.thheller.com/demos/todomvc-split/ 11 | 12 | Code in `src/dev/todomvc`. 13 | 14 | The "views" and "tx" (state ops) code is almost identical. It isn't currently since I need to sort out some framework namespaces and probably introduce a protocol or something. They can be 100% identical. 15 | 16 | So whats left is a bit of glue code either starting a worker or not and ensuring that code can be split properly so the worker doesn't end up loading view code (which may contain DOM interop code it can't run). 17 | 18 | ## API Design 19 | 20 | `defc` components currently use `sg/query` to "query" the application state. In the default variant that just accesses the data directly. In the worker variant it instead sends the query (already data) to the worker using transit. The worker then processes the actual query and sends back the data. The query hook "suspended" when it sent the query and can "resume" processing when the data arrives. 21 | 22 | ```clojure 23 | (defc ui-sample [todo-ident] 24 | [data 25 | (sg/query todo-ident 26 | [::m/todo-text 27 | ::m/editing? 28 | ::m/completed?])] 29 | 30 | (<< [:div ... data])) 31 | ``` 32 | 33 | The worker variant obviously makes everything async but from the User API perspective this looks identical. The implementation takes care of async and the `defc` macro makes it look like regular sync code. 34 | 35 | It does make scheduling/coordinating all that work a lot harder but that work needs to be done anyways since queries are supposed to be async in non-worker code too (to support fetching remote data). So its really just an implementation detail, the component author shouldn't see it. 36 | 37 | The assumption is that everything can go async anyways, so actually doing it to talk to a worker might not be that bad. 38 | 39 | ### Moving to Worker 40 | 41 | Since queries allow us to only ship small chunks of data at a time (and updating that incrementally) the "main" thread never needs the full database. Turns out `transit-cljs` is already quite fast and probably fast enough to not worry about it. So serialization performance is actually not the limiting factor anymore and could be optimized further if needed. 42 | 43 | Overall however for a simple app like todomvc using the worker split actually still makes the app slower. I do however believe that larger actual apps could benefit a lot from moving more code to the worker. Enough so that I'll will try to explore this as the default for the framework. Its about time we moved off the main thread anyways. 44 | 45 | todomvc is "slower" since startup basically happens twice. Since we need `cljs.core` available in both the main thread and the worker we'll have a `shared.js` containing all shared code. `cljs.core` and `transit-cljs` at the very least. The non-worker code didn't use `transit-cljs` at all so was obviously smaller but real apps likely have that dependency anyways. The `shared.js` however needs to be loaded and eval'd in the main thread and the worker. I think the Browser will be smart enough to actually only send out the request once but it'll still load the code twice. This isn't noticeable on my desktop but running with 6x slowdown (emulating a slow mobile) this is still noticeable. I also can't seem to get the Worker to actually start faster/sooner. It always seems to take a bit of time longer thus causing a bit of an additional delay. 46 | 47 | On the desktop none of this is noticeable (at most 100ms difference on start). Making everything async actually performs better when using 6x slowdown since the "blocking" periods are smaller but the startup delay is also longer. 48 | 49 | ### Work TBD 50 | 51 | - The UI code will need a "Suspense" style component so the UI becomes less "jumpy" but even without just looks like a regular UI talking to the network. This will need to exist for other async IO purposes anyways so it isn't worker related. 52 | 53 | - Framework re-org so that using a worker becomes opt-in/out and doesn't require any changes to UI code. Currently this requires changing a `ns` `:require` but shouldn't. 54 | 55 | - The DB should be separated anyways but there should be ways to "force" that no view code can access DB code directly and has to go through `sg/query`. 56 | 57 | - Can't use the `fulcro` patterns either since the Worker code can't access the View Components either. Also creating one big root query is a problem for serialization so instead of passing "data" down we only pass down idents that child components then query for data. Maybe there should be a "shared" schema of sorts. 58 | 59 | - Limiting the "event" system. The mutable JS `event` object can never cross over to the worker but the main thread should still be able to do everything it needs. This works already but is a bit ugly. 60 | 61 | -------------------------------------------------------------------------------- /examples/app/.gitignore: -------------------------------------------------------------------------------- 1 | public/js 2 | public/bootstrap 3 | -------------------------------------------------------------------------------- /examples/app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | shadow-grove 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/bench-fragment/.gitignore: -------------------------------------------------------------------------------- 1 | /js 2 | -------------------------------------------------------------------------------- /examples/bench-fragment/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 |
8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /examples/conduit/.gitignore: -------------------------------------------------------------------------------- 1 | /js 2 | -------------------------------------------------------------------------------- /examples/conduit/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | Conduit 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /examples/conduit/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Conduit", 3 | "name": "Conduit with ClojureScript and re-frame", 4 | "start_url": "/index.html", 5 | "icons": [ 6 | { 7 | "src": "favicon.ico", 8 | "sizes": "64x64 32x32 24x24 16x16", 9 | "type": "image/x-icon" 10 | }, 11 | { 12 | "src": "/img/icon@1.png", 13 | "sizes": "128x128", 14 | "type": "image/png" 15 | }, 16 | { 17 | "src": "/img/icon@1.5x.png", 18 | "sizes": "192x192", 19 | "type": "image/png" 20 | }, 21 | { 22 | "src": "/img/icon@2x.png", 23 | "sizes": "256x256", 24 | "type": "image/png" 25 | }, 26 | { 27 | "src": "/img/icon@3x.png", 28 | "sizes": "384x384", 29 | "type": "image/png" 30 | }, 31 | { 32 | "src": "/img/icon@4x.png", 33 | "sizes": "512x512", 34 | "type": "image/png" 35 | } 36 | ], 37 | "display": "standalone", 38 | "theme_color": "#000000", 39 | "background_color": "#ffffff" 40 | } 41 | -------------------------------------------------------------------------------- /examples/fulcro/.gitignore: -------------------------------------------------------------------------------- 1 | js/ 2 | -------------------------------------------------------------------------------- /examples/fulcro/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | fulcro test 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/reusable-components/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | public/js 3 | 4 | /target 5 | /checkouts 6 | /src/gen 7 | 8 | pom.xml 9 | pom.xml.asc 10 | *.iml 11 | *.jar 12 | *.log 13 | .shadow-cljs 14 | .idea 15 | .lein-* 16 | .nrepl-* 17 | .DS_Store 18 | 19 | .hgignore 20 | .hg/ 21 | -------------------------------------------------------------------------------- /examples/reusable-components/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "instruction-counter", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "shadow-cljs": "2.11.18" 7 | }, 8 | "dependencies": { 9 | "react": "17.0.1", 10 | "react-dom": "17.0.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /examples/reusable-components/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 |
13 |
14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /examples/reusable-components/shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths 3 | ["src/dev" 4 | "src/main" 5 | "src/test" 6 | "../../src/main"] 7 | 8 | :dependencies 9 | [[reagent "1.0.0"] 10 | [re-com "2.12.0"] 11 | [thheller/shadow-experiments "0.0.3"]] 12 | 13 | :dev-http {4008 "public"} 14 | 15 | :builds 16 | {:demo 17 | {:target :browser 18 | :output-dir "public/js" 19 | :modules {:main {:init-fn test.samples/init}}}}} 20 | -------------------------------------------------------------------------------- /examples/reusable-components/src/main/test/samples.cljs: -------------------------------------------------------------------------------- 1 | (ns test.samples 2 | (:require 3 | ["react-dom" :as rdom] 4 | ["react" :as react :rename {createElement el}] 5 | [reagent.core :as reagent] 6 | [re-com.core :as rc] 7 | [shadow.experiments.arborist :as sa] 8 | [shadow.experiments.grove :as sg :refer (<<)] 9 | [shadow.experiments.arborist.interpreted])) 10 | 11 | (def dom-root 12 | (js/document.getElementById "root")) 13 | 14 | (defn grey-box [] 15 | [:div.border.border-grey-500.bg-gray-100.p-8 "box"]) 16 | 17 | (defn example-recom [] 18 | [rc/h-box 19 | :gap "10px" 20 | :children [[grey-box] 21 | [grey-box] 22 | [rc/gap :size "5px"] 23 | [grey-box]]]) 24 | 25 | (defn start-recom [] 26 | (dotimes [x 10] 27 | (time 28 | (let [node (js/document.createElement "div")] 29 | (.append dom-root node) 30 | 31 | (rdom/render 32 | (reagent/as-element [example-recom]) 33 | node))))) 34 | 35 | (defn example-reagent [] 36 | [:div.flex 37 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-4 "box"] 38 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-8 "box"] 39 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-4 "box"]]) 40 | 41 | (defn start-reagent [] 42 | (dotimes [x 10] 43 | (time 44 | (let [node (js/document.createElement "div")] 45 | (.append dom-root node) 46 | 47 | (rdom/render 48 | (reagent/as-element [example-recom]) 49 | node))))) 50 | 51 | (defn example-react [] 52 | (el "div" #js {:className "flex"} 53 | (el "div" #js {:className "border border-grey-500 bg-gray-100 p-8 mr-4"} "box") 54 | (el "div" #js {:className "border border-grey-500 bg-gray-100 p-8 mr-4"} "box") 55 | (el "div" #js {:className "border border-grey-500 bg-gray-100 p-8 mr-4"} "box") 56 | )) 57 | 58 | 59 | (defn start-react [] 60 | (dotimes [x 10] 61 | (time 62 | (let [node (js/document.createElement "div")] 63 | (.append dom-root node) 64 | 65 | (rdom/render 66 | (el example-react nil) 67 | node))))) 68 | 69 | (defn example-grove [] 70 | (<< [:div.flex 71 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-4 "box"] 72 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-8 "box"] 73 | [:div.border.border-grey-500.bg-gray-100.p-8.mr-4 "box"]])) 74 | 75 | (defn box [title body] 76 | (<< [:div.border.shadow-lg 77 | [:div title] 78 | body])) 79 | 80 | (defn example-grove2 [] 81 | (<< [:div.flex 82 | [:div.mr-4 (box "title a" "box")] 83 | [:div.mr-8 (box "title b" "box")] 84 | [:div.mr-4 (box "title c" "box")]])) 85 | 86 | (defn example-grove3 [] 87 | (<< [:div.flex 88 | [:div.mr-4 89 | (box "title a" 90 | (<< [:div.font-bold "box"]))] 91 | [:div.mr-8 92 | (box "title b" 93 | (<< [:div.p-8 "box"]))] 94 | [:div.mr-4 95 | (box "title c" 96 | (<< [:div.text-2xl "box"]))]])) 97 | 98 | (defn example-grove4 [] 99 | (<< [:div.flex 100 | [:div.mr-4 101 | (box 102 | (<< "title a" [:sup "1"]) 103 | (<< [:div.font-bold "box"]))] 104 | [:div.mr-8 105 | (box 106 | (<< "title " [:span.font-bold "b"]) 107 | (<< [:div.p-8 "box"]))] 108 | [:div.mr-4 109 | (box 110 | "title c" 111 | (<< [:div.text-2xl "box"]))]])) 112 | 113 | (defn ^:dev/after-load start-grove [] 114 | (dotimes [x 10] 115 | (time 116 | (let [node (js/document.createElement "div")] 117 | (.append dom-root node) 118 | 119 | (let [root (sa/dom-root node {})] 120 | (sa/update! root (example-recom)) 121 | ))))) 122 | 123 | (def example-html 124 | "
\n
box
\n
\n
box
\n
\n
\n
\n
box
\n
") 125 | 126 | (defn start-innerhtml [] 127 | (dotimes [x 10] 128 | (time 129 | (let [node (js/document.createElement "div")] 130 | (.append dom-root node) 131 | 132 | (set! node -innerHTML example-html))))) 133 | 134 | (defn init [] 135 | ;; give stuff time to settle before rendering anything 136 | (js/setTimeout start-grove 2000)) 137 | -------------------------------------------------------------------------------- /examples/suspense/.gitignore: -------------------------------------------------------------------------------- 1 | js/ 2 | -------------------------------------------------------------------------------- /examples/suspense/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | suspense test 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /examples/todomvc-split/.gitignore: -------------------------------------------------------------------------------- 1 | /js 2 | -------------------------------------------------------------------------------- /examples/todomvc-split/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 14 | 15 | split • TodoMVC 16 | 17 | 18 | 19 | 20 |
21 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | -------------------------------------------------------------------------------- /examples/todomvc-split/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-app-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "todomvc-app-css@^2.0.0", 3 | "_id": "todomvc-app-css@2.2.0", 4 | "_inBundle": false, 5 | "_integrity": "sha512-H03oc3QOxiGXv+MqnotcduZIwoGX8A8QbSx9J4U2Z5R96LrK+dvQmRDTgeCc0nlkPBhd3nUL4EbfS7l0TccM5g==", 6 | "_location": "/todomvc-app-css", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "todomvc-app-css@^2.0.0", 12 | "name": "todomvc-app-css", 13 | "escapedName": "todomvc-app-css", 14 | "rawSpec": "^2.0.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^2.0.0" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.2.0.tgz", 22 | "_shasum": "6f84b15d9d4e0cf186679489fb236050c822de11", 23 | "_spec": "todomvc-app-css@^2.0.0", 24 | "_where": "/mnt/c/Users/thheller/code/tmp/todomvc-app-template", 25 | "author": { 26 | "name": "Sindre Sorhus", 27 | "email": "sindresorhus@gmail.com", 28 | "url": "sindresorhus.com" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/tastejs/todomvc-app-css/issues" 32 | }, 33 | "bundleDependencies": false, 34 | "deprecated": false, 35 | "description": "CSS for TodoMVC apps", 36 | "files": [ 37 | "index.css" 38 | ], 39 | "homepage": "https://github.com/tastejs/todomvc-app-css#readme", 40 | "keywords": [ 41 | "todomvc", 42 | "tastejs", 43 | "app", 44 | "todo", 45 | "template", 46 | "css", 47 | "style", 48 | "stylesheet" 49 | ], 50 | "license": "CC-BY-4.0", 51 | "name": "todomvc-app-css", 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/tastejs/todomvc-app-css.git" 55 | }, 56 | "style": "index.css", 57 | "version": "2.2.0" 58 | } 59 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-app-css/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-app-css 2 | 3 | > CSS for TodoMVC apps 4 | 5 | ![](screenshot.png) 6 | 7 | 8 | ## Install 9 | 10 | 11 | ``` 12 | $ npm install todomvc-app-css 13 | ``` 14 | 15 | 16 | ## Getting started 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template). 23 | 24 | 25 | ## License 26 | 27 | CC-BY-4.0 © [Sindre Sorhus](https://sindresorhus.com) 28 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-common/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) TasteJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "todomvc-common@^1.0.0", 3 | "_id": "todomvc-common@1.0.5", 4 | "_inBundle": false, 5 | "_integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ==", 6 | "_location": "/todomvc-common", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "todomvc-common@^1.0.0", 12 | "name": "todomvc-common", 13 | "escapedName": "todomvc-common", 14 | "rawSpec": "^1.0.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^1.0.0" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz", 22 | "_shasum": "8c3e799ac9f1fc1573e0c204f984510826914730", 23 | "_spec": "todomvc-common@^1.0.0", 24 | "_where": "/mnt/c/Users/thheller/code/tmp/todomvc-app-template", 25 | "author": { 26 | "name": "TasteJS" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/tastejs/todomvc-common/issues" 30 | }, 31 | "bundleDependencies": false, 32 | "deprecated": false, 33 | "description": "Common TodoMVC utilities used by our apps", 34 | "files": [ 35 | "base.js", 36 | "base.css" 37 | ], 38 | "homepage": "https://github.com/tastejs/todomvc-common#readme", 39 | "keywords": [ 40 | "todomvc", 41 | "tastejs", 42 | "util", 43 | "utilities" 44 | ], 45 | "license": "MIT", 46 | "main": "base.js", 47 | "name": "todomvc-common", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/tastejs/todomvc-common.git" 51 | }, 52 | "style": "base.css", 53 | "version": "1.0.5" 54 | } 55 | -------------------------------------------------------------------------------- /examples/todomvc-split/vendor/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Common TodoMVC utilities used by our apps 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install todomvc-common 10 | ``` 11 | 12 | 13 | ## License 14 | 15 | MIT © [TasteJS](http://tastejs.com) 16 | -------------------------------------------------------------------------------- /examples/todomvc/.gitignore: -------------------------------------------------------------------------------- 1 | /js 2 | -------------------------------------------------------------------------------- /examples/todomvc/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | simple • TodoMVC 7 | 8 | 9 | 10 | 11 | 27 |
28 | 33 | 34 | 35 | 36 | 37 | -------------------------------------------------------------------------------- /examples/todomvc/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-app-css/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "todomvc-app-css@^2.0.0", 3 | "_id": "todomvc-app-css@2.2.0", 4 | "_inBundle": false, 5 | "_integrity": "sha512-H03oc3QOxiGXv+MqnotcduZIwoGX8A8QbSx9J4U2Z5R96LrK+dvQmRDTgeCc0nlkPBhd3nUL4EbfS7l0TccM5g==", 6 | "_location": "/todomvc-app-css", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "todomvc-app-css@^2.0.0", 12 | "name": "todomvc-app-css", 13 | "escapedName": "todomvc-app-css", 14 | "rawSpec": "^2.0.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^2.0.0" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/todomvc-app-css/-/todomvc-app-css-2.2.0.tgz", 22 | "_shasum": "6f84b15d9d4e0cf186679489fb236050c822de11", 23 | "_spec": "todomvc-app-css@^2.0.0", 24 | "_where": "/mnt/c/Users/thheller/code/tmp/todomvc-app-template", 25 | "author": { 26 | "name": "Sindre Sorhus", 27 | "email": "sindresorhus@gmail.com", 28 | "url": "sindresorhus.com" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/tastejs/todomvc-app-css/issues" 32 | }, 33 | "bundleDependencies": false, 34 | "deprecated": false, 35 | "description": "CSS for TodoMVC apps", 36 | "files": [ 37 | "index.css" 38 | ], 39 | "homepage": "https://github.com/tastejs/todomvc-app-css#readme", 40 | "keywords": [ 41 | "todomvc", 42 | "tastejs", 43 | "app", 44 | "todo", 45 | "template", 46 | "css", 47 | "style", 48 | "stylesheet" 49 | ], 50 | "license": "CC-BY-4.0", 51 | "name": "todomvc-app-css", 52 | "repository": { 53 | "type": "git", 54 | "url": "git+https://github.com/tastejs/todomvc-app-css.git" 55 | }, 56 | "style": "index.css", 57 | "version": "2.2.0" 58 | } 59 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-app-css/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-app-css 2 | 3 | > CSS for TodoMVC apps 4 | 5 | ![](screenshot.png) 6 | 7 | 8 | ## Install 9 | 10 | 11 | ``` 12 | $ npm install todomvc-app-css 13 | ``` 14 | 15 | 16 | ## Getting started 17 | 18 | ```html 19 | 20 | ``` 21 | 22 | See the [TodoMVC app template](https://github.com/tastejs/todomvc-app-template). 23 | 24 | 25 | ## License 26 | 27 | CC-BY-4.0 © [Sindre Sorhus](https://sindresorhus.com) 28 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-common/base.css: -------------------------------------------------------------------------------- 1 | hr { 2 | margin: 20px 0; 3 | border: 0; 4 | border-top: 1px dashed #c5c5c5; 5 | border-bottom: 1px dashed #f7f7f7; 6 | } 7 | 8 | .learn a { 9 | font-weight: normal; 10 | text-decoration: none; 11 | color: #b83f45; 12 | } 13 | 14 | .learn a:hover { 15 | text-decoration: underline; 16 | color: #787e7e; 17 | } 18 | 19 | .learn h3, 20 | .learn h4, 21 | .learn h5 { 22 | margin: 10px 0; 23 | font-weight: 500; 24 | line-height: 1.2; 25 | color: #000; 26 | } 27 | 28 | .learn h3 { 29 | font-size: 24px; 30 | } 31 | 32 | .learn h4 { 33 | font-size: 18px; 34 | } 35 | 36 | .learn h5 { 37 | margin-bottom: 0; 38 | font-size: 14px; 39 | } 40 | 41 | .learn ul { 42 | padding: 0; 43 | margin: 0 0 30px 25px; 44 | } 45 | 46 | .learn li { 47 | line-height: 20px; 48 | } 49 | 50 | .learn p { 51 | font-size: 15px; 52 | font-weight: 300; 53 | line-height: 1.3; 54 | margin-top: 0; 55 | margin-bottom: 0; 56 | } 57 | 58 | #issue-count { 59 | display: none; 60 | } 61 | 62 | .quote { 63 | border: none; 64 | margin: 20px 0 60px 0; 65 | } 66 | 67 | .quote p { 68 | font-style: italic; 69 | } 70 | 71 | .quote p:before { 72 | content: '“'; 73 | font-size: 50px; 74 | opacity: .15; 75 | position: absolute; 76 | top: -20px; 77 | left: 3px; 78 | } 79 | 80 | .quote p:after { 81 | content: '”'; 82 | font-size: 50px; 83 | opacity: .15; 84 | position: absolute; 85 | bottom: -42px; 86 | right: 3px; 87 | } 88 | 89 | .quote footer { 90 | position: absolute; 91 | bottom: -40px; 92 | right: 0; 93 | } 94 | 95 | .quote footer img { 96 | border-radius: 3px; 97 | } 98 | 99 | .quote footer a { 100 | margin-left: 5px; 101 | vertical-align: middle; 102 | } 103 | 104 | .speech-bubble { 105 | position: relative; 106 | padding: 10px; 107 | background: rgba(0, 0, 0, .04); 108 | border-radius: 5px; 109 | } 110 | 111 | .speech-bubble:after { 112 | content: ''; 113 | position: absolute; 114 | top: 100%; 115 | right: 30px; 116 | border: 13px solid transparent; 117 | border-top-color: rgba(0, 0, 0, .04); 118 | } 119 | 120 | .learn-bar > .learn { 121 | position: absolute; 122 | width: 272px; 123 | top: 8px; 124 | left: -300px; 125 | padding: 10px; 126 | border-radius: 5px; 127 | background-color: rgba(255, 255, 255, .6); 128 | transition-property: left; 129 | transition-duration: 500ms; 130 | } 131 | 132 | @media (min-width: 899px) { 133 | .learn-bar { 134 | width: auto; 135 | padding-left: 300px; 136 | } 137 | 138 | .learn-bar > .learn { 139 | left: 8px; 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-common/license: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) TasteJS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "_from": "todomvc-common@^1.0.0", 3 | "_id": "todomvc-common@1.0.5", 4 | "_inBundle": false, 5 | "_integrity": "sha512-D8kEJmxVMQIWwztEdH+WeiAfXRbbSCpgXq4NkYi+gduJ2tr8CNq7sYLfJvjpQ10KD9QxJwig57rvMbV2QAESwQ==", 6 | "_location": "/todomvc-common", 7 | "_phantomChildren": {}, 8 | "_requested": { 9 | "type": "range", 10 | "registry": true, 11 | "raw": "todomvc-common@^1.0.0", 12 | "name": "todomvc-common", 13 | "escapedName": "todomvc-common", 14 | "rawSpec": "^1.0.0", 15 | "saveSpec": null, 16 | "fetchSpec": "^1.0.0" 17 | }, 18 | "_requiredBy": [ 19 | "/" 20 | ], 21 | "_resolved": "https://registry.npmjs.org/todomvc-common/-/todomvc-common-1.0.5.tgz", 22 | "_shasum": "8c3e799ac9f1fc1573e0c204f984510826914730", 23 | "_spec": "todomvc-common@^1.0.0", 24 | "_where": "/mnt/c/Users/thheller/code/tmp/todomvc-app-template", 25 | "author": { 26 | "name": "TasteJS" 27 | }, 28 | "bugs": { 29 | "url": "https://github.com/tastejs/todomvc-common/issues" 30 | }, 31 | "bundleDependencies": false, 32 | "deprecated": false, 33 | "description": "Common TodoMVC utilities used by our apps", 34 | "files": [ 35 | "base.js", 36 | "base.css" 37 | ], 38 | "homepage": "https://github.com/tastejs/todomvc-common#readme", 39 | "keywords": [ 40 | "todomvc", 41 | "tastejs", 42 | "util", 43 | "utilities" 44 | ], 45 | "license": "MIT", 46 | "main": "base.js", 47 | "name": "todomvc-common", 48 | "repository": { 49 | "type": "git", 50 | "url": "git+https://github.com/tastejs/todomvc-common.git" 51 | }, 52 | "style": "base.css", 53 | "version": "1.0.5" 54 | } 55 | -------------------------------------------------------------------------------- /examples/todomvc/vendor/todomvc-common/readme.md: -------------------------------------------------------------------------------- 1 | # todomvc-common 2 | 3 | > Common TodoMVC utilities used by our apps 4 | 5 | 6 | ## Install 7 | 8 | ``` 9 | $ npm install todomvc-common 10 | ``` 11 | 12 | 13 | ## License 14 | 15 | MIT © [TasteJS](http://tastejs.com) 16 | -------------------------------------------------------------------------------- /examples/ui/.gitignore: -------------------------------------------------------------------------------- 1 | js/ 2 | -------------------------------------------------------------------------------- /examples/ui/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | ui test 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/website/dev.edn: -------------------------------------------------------------------------------- 1 | {:app/ns dummy.website 2 | :app/root "examples/website" 3 | :http/roots ["public"] 4 | :http/port 8090} -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "shadow-experiments", 3 | "version": "0.0.1", 4 | "private": true, 5 | "devDependencies": { 6 | "shadow-cljs": "2.11.21" 7 | }, 8 | "dependencies": { 9 | "benchmark": "^2.1.4", 10 | "microtime": "^3.0.0", 11 | "react": "^16.12.0", 12 | "react-dom": "^16.12.0" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/examples/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "lockfileVersion": 2, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "version": "1.0.0", 9 | "license": "ISC", 10 | "dependencies": { 11 | "codemirror": "^5.59.4", 12 | "parinfer-codemirror": "^1.4.2", 13 | "prettier": "^2.2.1" 14 | } 15 | }, 16 | "node_modules/codemirror": { 17 | "version": "5.59.4", 18 | "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz", 19 | "integrity": "sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg==" 20 | }, 21 | "node_modules/parinfer": { 22 | "version": "3.12.0", 23 | "resolved": "https://registry.npmjs.org/parinfer/-/parinfer-3.12.0.tgz", 24 | "integrity": "sha512-iViQ8vtJ6CEa9x0SxcQCtsOYSCVVu7ILnuUJfKHPEGjxM0Z6R1EdAJX3kWyqZDrszvQyYWl/XpD9pN2IFgSF/g==" 25 | }, 26 | "node_modules/parinfer-codemirror": { 27 | "version": "1.4.2", 28 | "resolved": "https://registry.npmjs.org/parinfer-codemirror/-/parinfer-codemirror-1.4.2.tgz", 29 | "integrity": "sha1-K5t/J69lvxQoIDShdzHUqCNn0EY=", 30 | "dependencies": { 31 | "parinfer": "^3.11.0" 32 | } 33 | }, 34 | "node_modules/prettier": { 35 | "version": "2.2.1", 36 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", 37 | "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==", 38 | "bin": { 39 | "prettier": "bin-prettier.js" 40 | }, 41 | "engines": { 42 | "node": ">=10.13.0" 43 | } 44 | } 45 | }, 46 | "dependencies": { 47 | "codemirror": { 48 | "version": "5.59.4", 49 | "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.59.4.tgz", 50 | "integrity": "sha512-achw5JBgx8QPcACDDn+EUUXmCYzx/zxEtOGXyjvLEvYY8GleUrnfm5D+Zb+UjShHggXKDT9AXrbkBZX6a0YSQg==" 51 | }, 52 | "parinfer": { 53 | "version": "3.12.0", 54 | "resolved": "https://registry.npmjs.org/parinfer/-/parinfer-3.12.0.tgz", 55 | "integrity": "sha512-iViQ8vtJ6CEa9x0SxcQCtsOYSCVVu7ILnuUJfKHPEGjxM0Z6R1EdAJX3kWyqZDrszvQyYWl/XpD9pN2IFgSF/g==" 56 | }, 57 | "parinfer-codemirror": { 58 | "version": "1.4.2", 59 | "resolved": "https://registry.npmjs.org/parinfer-codemirror/-/parinfer-codemirror-1.4.2.tgz", 60 | "integrity": "sha1-K5t/J69lvxQoIDShdzHUqCNn0EY=", 61 | "requires": { 62 | "parinfer": "^3.11.0" 63 | } 64 | }, 65 | "prettier": { 66 | "version": "2.2.1", 67 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-2.2.1.tgz", 68 | "integrity": "sha512-PqyhM2yCjg/oKkFPtTGUojv7gnZAoG80ttl45O6x2Ug/rMJw4wcc9k6aaf2hibP7BGVCCM33gZoGjyvt9mm16Q==" 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/examples/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "examples", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "keywords": [], 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "codemirror": "^5.59.4", 14 | "parinfer-codemirror": "^1.4.2", 15 | "prettier": "^2.2.1" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject thheller/shadow-experiments "0.0.7" 2 | :description "WARNING: Experimental Code! Changing completely without notice! Do not use for anything but other experiments!" 3 | :url "http://github.com/thheller/shadow-experiments" 4 | :license {:name "Eclipse Public License" 5 | :url "http://www.eclipse.org/legal/epl-v10.html"} 6 | 7 | :repositories 8 | {"clojars" {:url "https://clojars.org/repo" 9 | :sign-releases false}} 10 | 11 | :dependencies 12 | [[org.clojure/clojure "1.10.1" :scope "provided"] 13 | [org.clojure/clojurescript "1.10.773" :scope "provided"]] 14 | 15 | :resource-paths 16 | ["src/resources"] 17 | 18 | :source-paths 19 | ["src/main"] 20 | 21 | :test-paths 22 | ["src/test"] 23 | 24 | :profiles 25 | {:dev 26 | {:source-paths ["src/dev"] 27 | :dependencies 28 | [[hiccup "1.0.5"] 29 | [metosin/reitit-core "0.5.12"] 30 | [thheller/shadow-undertow "0.1.0"]]} 31 | :cljs-dev ;; using this in shadow-cljs so can't use dev profile (Cursive complains about circular dep) 32 | {:dependencies 33 | [[thheller/shadow-cljs "2.11.22"] 34 | ]}}) 35 | 36 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | sync test 5 | 6 | 7 |
8 | 9 | 10 | -------------------------------------------------------------------------------- /shadow-cljs.edn: -------------------------------------------------------------------------------- 1 | ;; shadow-cljs configuration 2 | {:source-paths 3 | ["src/dev" 4 | "src/main" 5 | "src/test"] 6 | 7 | :jvm-opts 8 | ["-XX:-OmitStackTraceInFastThrow"] 9 | 10 | :version 11 | "2.18.0" 12 | 13 | :dependencies 14 | [[metosin/reitit-core "0.5.12"] 15 | [com.wsscode/pathom "2.2.28"] 16 | ;; for benchmark purposes, not actually a dependency 17 | [reagent "0.9.0-rc3"]] 18 | 19 | :dev-http 20 | {3005 "public" 21 | 3006 "examples/todomvc" 22 | 3007 "examples/todomvc-split" 23 | 3008 "examples/suspense" 24 | 3009 "examples/bench-fragment" 25 | 3010 "out/browser-test" 26 | 3011 "examples/app/public" 27 | 3013 "examples/ui"} 28 | 29 | :build-defaults 30 | {:compiler-options 31 | {:fn-invoke-direct true 32 | :shadow-tweaks true 33 | :shadow-keywords true}} 34 | 35 | :builds 36 | {:ui ;; FIXME: should at some point has some kind of workspaces like UI 37 | {:target :browser 38 | :output-dir "examples/ui/js" 39 | :modules {:main {:init-fn dummy.ui/init}}} 40 | 41 | :examples-bootstrap 42 | {:target :bootstrap 43 | :output-dir "examples/app/public/bootstrap" 44 | :exclude #{cljs.js} 45 | :entries [shadow.experiments.grove 46 | shadow.experiments.grove.runtime 47 | shadow.experiments.grove.db 48 | shadow.experiments.grove.eql-query 49 | shadow.experiments.grove.events 50 | shadow.experiments.grove.local 51 | shadow.experiments.grove.ui.testing] 52 | :compiler-options {:output-feature-set :es6} 53 | :macros []} 54 | 55 | :examples-host 56 | {:target :browser 57 | :asset-path "js" 58 | :output-dir "examples/app/public/js" 59 | 60 | :modules {:main {:init-fn shadow.experiments.grove.examples.app/init}} 61 | 62 | :compiler-options 63 | {:optimizations :simple 64 | :output-wrapper false 65 | :output-feature-set :es6} 66 | 67 | :js-options 68 | {:js-package-dirs ["packages/examples/node_modules"]}} 69 | 70 | :test-dom 71 | {:target :browser 72 | :modules {:main {:init-fn shadow.experiments.grove.test-app.dom/init}}} 73 | 74 | :test 75 | {:target :browser-test 76 | :test-dir "out/browser-test" 77 | :compiler-options {:output-feature-set :es-next}} 78 | 79 | :todomvc-simple 80 | {:target :browser 81 | :output-dir "examples/todomvc/js" 82 | :modules {:main {:init-fn todomvc.simple/init 83 | :preloads [shadow.experiments.grove.dev-support]}}} 84 | 85 | :todomvc-single 86 | {:target :browser 87 | :output-dir "examples/todomvc/js" 88 | :modules {:main {:init-fn todomvc.split.single/init 89 | :preloads [shadow.experiments.grove.dev-support]}}} 90 | 91 | :suspense 92 | {:target :browser 93 | :output-dir "examples/suspense/js" 94 | :modules {:main {:init-fn dummy.suspense/init 95 | :preloads [shadow.experiments.grove.dev-support]}}} 96 | 97 | :todomvc-split 98 | {:target :browser 99 | :output-dir "examples/todomvc-split/js" 100 | :modules 101 | {:shared 102 | {:entries []} 103 | :main 104 | {:init-fn todomvc.split.main/init 105 | :preloads [shadow.experiments.grove.dev-support] 106 | :depends-on #{:shared}} 107 | :worker 108 | {:init-fn todomvc.split.worker/init 109 | :web-worker true 110 | :depends-on #{:shared}}}} 111 | 112 | :bench-db 113 | {:target :node-script 114 | :output-to "out/bench-db.js" 115 | :main shadow.experiments.grove.bench-db/main 116 | :devtools {:enabled false}} 117 | 118 | :bench-fragment 119 | {:target :node-script 120 | :compiler-options {:optimizations :simple} 121 | :output-to "out/bench-fragment.js" 122 | :main shadow.experiments.grove.bench-fragment/main 123 | :devtools {:enabled false}} 124 | 125 | :bench-fragment-browser 126 | {:target :browser 127 | :output-dir "examples/bench-fragment/js" 128 | :modules {:main {:init-fn shadow.experiments.grove.bench-fragment/init}} 129 | :devtools {:enabled false}} 130 | }} 131 | -------------------------------------------------------------------------------- /src/dev/dummy/services/foo.clj: -------------------------------------------------------------------------------- 1 | (ns dummy.services.foo) 2 | 3 | (defn start [deps] 4 | ::foo) 5 | 6 | (defn stop [instance]) 7 | -------------------------------------------------------------------------------- /src/dev/dummy/suspense.cljs: -------------------------------------------------------------------------------- 1 | (ns dummy.suspense 2 | (:require 3 | [shadow.experiments.grove :as sg :refer (defc <<)] 4 | [shadow.experiments.grove.ui.testing :as t] 5 | [shadow.experiments.grove.runtime :as rt])) 6 | 7 | (defonce root-el (js/document.getElementById "app")) 8 | 9 | (defc nested [x] 10 | (bind _ (t/rand-delay 250)) 11 | 12 | (render 13 | (<< [:div.border.shadow.my-4.p-4 "nested:" x]))) 14 | 15 | (defn content [] 16 | (<< (nested 1) 17 | (nested 2) 18 | (nested 3) 19 | (nested 4) 20 | (nested 5))) 21 | 22 | (defn page-a [] 23 | (<< [:div "page a"] 24 | (content))) 25 | 26 | (defn page-b [] 27 | (<< [:div "page b"] 28 | (content))) 29 | 30 | (defn page-c [] 31 | (<< [:div "page c"] 32 | (content))) 33 | 34 | (defc ui-root [] 35 | (bind {:keys [page]} 36 | (sg/env-watch :data-ref)) 37 | 38 | (event ::switch-page! [{:keys [data-ref] :as env} {:keys [page]} e] 39 | (swap! data-ref assoc :page page)) 40 | 41 | (render 42 | (let [body 43 | (case page 44 | :a (page-a) 45 | :b (page-b) 46 | :c (page-c))] 47 | 48 | (<< [:div.p-4 49 | [:a.underline {:href "#" :on-click {:e ::switch-page! :page :a}} "page a"] 50 | " " 51 | [:a.underline {:href "#" :on-click {:e ::switch-page! :page :b}} "page b"] 52 | " " 53 | [:a.underline {:href "#" :on-click {:e ::switch-page! :page :c}} "page c"]] 54 | 55 | [:div.flex {:style "width: 500px;"} 56 | [:div.flex-1.p-4 57 | [:div "with suspense"] 58 | (sg/suspense 59 | {:fallback (<< [:div.pl-4 "Loading ..."]) 60 | :timeout 500} 61 | body)] 62 | 63 | [:div.flex-1.p-4 64 | [:div "without"] 65 | body]])))) 66 | 67 | ;; grove runtime 68 | (defonce rt-ref 69 | (rt/prepare {} 70 | (atom {}) ;; grove state which we don't use here 71 | ::ui)) 72 | 73 | (defn ^:dev/after-load start [] 74 | (sg/render rt-ref root-el (ui-root))) 75 | 76 | ;; adding this to the env under :data-ref, env makes it available to the component tree 77 | ;; never using this directly otherwise to avoid global state 78 | (defonce data-ref (atom {:page :a})) 79 | 80 | (defn init [] 81 | ;; functions in env-init are called once on first render for a new root 82 | ;; we use this to provide data-ref access via component env 83 | (swap! rt-ref update ::rt/env-init conj #(assoc % :data-ref data-ref)) 84 | (start)) 85 | -------------------------------------------------------------------------------- /src/dev/dummy/system.clj: -------------------------------------------------------------------------------- 1 | (ns dummy.system 2 | (:require [dummy.services.foo :as foo])) 3 | 4 | (defn foo-start [{:keys [bar]}] 5 | [:foo bar]) 6 | 7 | (defn foo-stop [x]) 8 | 9 | (defn bar-start [] 10 | :bar) 11 | 12 | (defn bar-stop [x]) 13 | 14 | (def services 15 | {:foo 16 | {:depends-on {:bar [:bar]} 17 | :start foo-start 18 | :stop foo-stop} 19 | :bar 20 | {:start bar-start 21 | :stop bar-stop}}) 22 | 23 | (comment 24 | (require '[shadow.experiments.system.runtime :as rt]) 25 | (set! *print-namespace-maps* false) 26 | (let [started 27 | (-> {} 28 | (rt/init services) 29 | (rt/start-all)) 30 | 31 | stopped 32 | (rt/stop-all started)] 33 | 34 | (tap> {:started started 35 | :stopped stopped}) 36 | )) 37 | -------------------------------------------------------------------------------- /src/dev/dummy/ui.cljs: -------------------------------------------------------------------------------- 1 | (ns dummy.ui 2 | (:require 3 | [shadow.experiments.grove.local :as local-eng] 4 | [shadow.experiments.grove.ui.dnd-sortable :as dnd] 5 | [shadow.experiments.grove.runtime :as rt] 6 | [shadow.experiments.grove :as sg :refer (defc <<)] 7 | )) 8 | 9 | (defc ui-dnd [] 10 | (bind items-ref 11 | (atom 12 | [{:id 1 :text "foo1"} 13 | {:id 2 :text "foo2"} 14 | {:id 3 :text "foo3"} 15 | {:id 4 :text "foo4"}])) 16 | 17 | (bind items 18 | (sg/watch items-ref)) 19 | 20 | (event ::dnd/sorted! [env ev e] 21 | (reset! items-ref (:items-after ev)) 22 | (js/console.log "sorted" ev)) 23 | 24 | (render 25 | (<< [:div {:style "font-size: 30px;"} 26 | (dnd/keyed-seq 27 | items 28 | :id 29 | (fn [item {::dnd/keys [dragging hovering] :as opts}] 30 | (<< [:div {:style {:border (str "1px solid" (if hovering " blue" " green")) 31 | :margin-top "10px" 32 | :padding "9px" 33 | :opacity (when dragging "1" "0.3")} 34 | ::dnd/target opts} 35 | [:span {:style {:cursor "move"} ::dnd/draggable opts} "drag-me"] 36 | (:text item) 37 | [:div 38 | (pr-str opts)]]) 39 | ))]))) 40 | 41 | (defonce root-el 42 | (js/document.getElementById "root")) 43 | 44 | (defn ^:dev/after-load start [] 45 | (sg/start ::ui (ui-dnd))) 46 | 47 | (defonce data-ref 48 | (-> {} 49 | (atom))) 50 | 51 | (defonce rt-ref 52 | (rt/prepare {} data-ref ::rt)) 53 | 54 | (defn init [] 55 | (local-eng/init! rt-ref) 56 | (sg/init-root rt-ref ::ui root-el {}) 57 | (start)) -------------------------------------------------------------------------------- /src/dev/dummy/website.clj: -------------------------------------------------------------------------------- 1 | (ns dummy.website 2 | (:require [hiccup.page :refer (html5)])) 3 | 4 | (defn adjust-config [config] 5 | (-> config 6 | (assoc :FOO (System/getenv "FOO")))) 7 | 8 | (defn services [config] 9 | {}) 10 | 11 | (defn web-index 12 | {:http/handle [:GET "/"]} 13 | [env] 14 | {:status 200 15 | :body "Hello World!"}) 16 | 17 | (defn web-product-page 18 | {:http/handle [:GET "/product/{product-id}"]} 19 | [env {:keys [product-id]}] 20 | {:status 200 21 | :body 22 | (html5 {:lang "de"} 23 | [:head 24 | [:title "Hello World!"]] 25 | [:body 26 | [:div "Product: " product-id]])}) 27 | 28 | (defn admin-only 29 | [env] 30 | :ok) 31 | 32 | (defn web-admin-index 33 | {:http/handle [:GET "/admin"] 34 | :http/intercept [::admin-only]} 35 | [env] 36 | {:status 200 37 | :body "Hello World!"}) 38 | -------------------------------------------------------------------------------- /src/dev/shadow/experiments/grove/test_app/dom.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.test-app.dom 2 | (:require 3 | [shadow.experiments.arborist :as sa :refer (<<)] 4 | [shadow.experiments.grove.components :as sac :refer (defc)] 5 | [clojure.string :as str] 6 | [shadow.experiments.arborist.protocols :as p])) 7 | 8 | (def items 9 | (into [] (for [i (range 100)] 10 | {:id i :test (str "foo" i)}))) 11 | 12 | (defc nested 13 | [{:keys [id x class] :as props}] 14 | [] 15 | (<< "foo" 16 | [:div {:class class} 17 | [:h2 "nested instance" id "/" x]] 18 | "bar")) 19 | 20 | (defc component 21 | [{:keys [num class] :as props}] 22 | [children (sac/slot)] 23 | ;; (js/console.log "component render" props children) 24 | (<< [:div {:class ["hello" "world" class]} 25 | [:div "component instance" num] 26 | children] 27 | [:div 28 | (nested props)])) 29 | 30 | (defc root 31 | {:init-state {:num 0}} 32 | [props {:keys [num] :as state}] 33 | [other 34 | (<< [:h1 "@" num]) 35 | 36 | items 37 | (->> items 38 | (shuffle) 39 | (take 75)) 40 | 41 | item-row 42 | (fn [{:keys [id test]}] 43 | ;; closed over state 44 | (nested {:x num :id id :class test})) 45 | 46 | ::inc 47 | (fn [env e] 48 | ;; (js/console.log ::inc env e) 49 | (sac/swap-state! env update :num inc))] 50 | 51 | (<< [:div "before"] 52 | [:div.card {:on-click [::inc]} 53 | 54 | [:div.card-header {:style {:color "red"}} other] 55 | [:div.card-body {:data-x num} 56 | [:h2 "props"] 57 | [:div (pr-str props)] 58 | [:h2 "state"] 59 | [:div (pr-str state)]] 60 | 61 | [component {:x num :class (str "class" num)} 62 | [:div "a" num] 63 | [:div "b" num] 64 | [:div "c" num]] 65 | 66 | [:div (->> items (map :id) pr-str)] 67 | 68 | (sa/render-seq items :id item-row) 69 | 70 | [:div.card-footer 71 | [:button {:type "button" :on-click [::foo 1 2 3]} "OK"]]] 72 | [:div "after"])) 73 | 74 | (defonce root-ref (atom nil)) 75 | 76 | (defn ^:dev/after-load start [] 77 | (let [container 78 | (js/document.getElementById "app") 79 | 80 | dom-root 81 | (sa/dom-root container {})] 82 | 83 | (sa/update! dom-root (root {:props "yo"})) 84 | 85 | (reset! root-ref dom-root))) 86 | 87 | (defn ^:dev/before-load reset-root! [] 88 | (when-let [root @root-ref] 89 | (sa/destroy! root) 90 | (reset! root-ref nil))) 91 | 92 | (defn init [] 93 | (js/setTimeout start 0)) 94 | 95 | (defn bar [x] (js/console.log (x 1))) 96 | 97 | (defn foo [x] 98 | (when (pos? x) 99 | (bar (fn [i] (+ i x))) 100 | (recur (dec x)))) -------------------------------------------------------------------------------- /src/dev/todomvc/model.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.model 2 | "just for keyword alias purposes") 3 | 4 | ;; main and worker live in separate namespaces so :: doesn't work 5 | 6 | (def schema 7 | {::todo 8 | {:type :entity 9 | :primary-key ::todo-id 10 | :attrs {} 11 | }}) 12 | -------------------------------------------------------------------------------- /src/dev/todomvc/split/db.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.db 2 | (:require 3 | [shadow.experiments.grove.eql-query :as eql] 4 | [shadow.experiments.grove.events :as ev] 5 | [shadow.experiments.grove.db :as db] 6 | [todomvc.model :as m] 7 | [todomvc.split.env :as env])) 8 | 9 | ;; FIXME: counting lazy seq .. 10 | (defmethod eql/attr ::m/num-active 11 | [env db current _ params] 12 | (->> (db/all-of db ::m/todo) 13 | (remove ::m/completed?) 14 | (count))) 15 | 16 | (defmethod eql/attr ::m/num-completed 17 | [env db current _ params] 18 | (->> (db/all-of db ::m/todo) 19 | (filter ::m/completed?) 20 | (count))) 21 | 22 | (defmethod eql/attr ::m/num-total 23 | [env db current _ params] 24 | (count (db/all-of db ::m/todo))) 25 | 26 | (defmethod eql/attr ::m/editing? 27 | [env db current _ params] 28 | (= (::m/editing db) (:db/ident current))) 29 | 30 | (defmethod eql/attr ::m/filtered-todos 31 | [env {::m/keys [current-filter] :as db} current _ params] 32 | (let [filter-fn 33 | (case current-filter 34 | :all 35 | (fn [x] true) 36 | :active 37 | #(not (::m/completed? %)) 38 | :completed 39 | #(true? (::m/completed? %)))] 40 | 41 | (->> (db/all-of db ::m/todo) 42 | (filter filter-fn) 43 | (map :db/ident) 44 | (sort) 45 | (vec)))) 46 | 47 | (defn without [items del] 48 | (into [] (remove #{del}) items)) 49 | 50 | (defn r-> [init rfn coll] 51 | (reduce rfn init coll)) 52 | 53 | (ev/reg-event env/rt-ref ::m/create-new! 54 | (fn [{:keys [db]} {::m/keys [todo-text]}] 55 | (let [{::m/keys [id-seq]} db] 56 | {:db 57 | (let [new-todo {::m/todo-id id-seq ::m/todo-text todo-text}] 58 | (-> db 59 | (update ::m/id-seq inc) 60 | (db/add ::m/todo new-todo [::m/todos])))}))) 61 | 62 | (ev/reg-event env/rt-ref ::m/delete! 63 | (fn [{:keys [db] :as env} {:keys [todo]}] 64 | {:db (-> db 65 | (dissoc todo) 66 | (update ::m/todos without todo))})) 67 | 68 | (ev/reg-event env/rt-ref ::m/set-filter! 69 | (fn [{:keys [db] :as env} {:keys [filter]}] 70 | {:db 71 | (assoc db ::m/current-filter filter)})) 72 | 73 | (ev/reg-event env/rt-ref ::m/toggle-completed! 74 | (fn [{:keys [db] :as env} {:keys [todo]}] 75 | {:db 76 | (update-in db [todo ::m/completed?] not)})) 77 | 78 | (ev/reg-event env/rt-ref ::m/edit-start! 79 | (fn [{:keys [db] :as env} {:keys [todo]}] 80 | {:db 81 | (assoc db ::m/editing todo)})) 82 | 83 | (ev/reg-event env/rt-ref ::m/edit-save! 84 | (fn [{:keys [db] :as env} {:keys [todo text]}] 85 | {:db 86 | (-> db 87 | (assoc-in [todo ::m/todo-text] text) 88 | (assoc ::m/editing nil))})) 89 | 90 | (ev/reg-event env/rt-ref ::m/edit-cancel! 91 | (fn [{:keys [db] :as env} {:keys [todo]}] 92 | {:db 93 | (assoc db ::m/editing nil)})) 94 | 95 | (ev/reg-event env/rt-ref ::m/clear-completed! 96 | (fn [{:keys [db] :as env} _] 97 | {:db (-> db 98 | (r-> 99 | (fn [db {::m/keys [completed?] :as todo}] 100 | (if-not completed? 101 | db 102 | (db/remove db todo))) 103 | (db/all-of db ::m/todo)) 104 | (update ::m/todos (fn [current] 105 | (into [] (remove #(get-in db [% ::m/completed?])) current)))) 106 | })) 107 | 108 | (ev/reg-event env/rt-ref ::m/toggle-all! 109 | (fn [{:keys [db] :as env} {:keys [completed?]}] 110 | {:db 111 | (reduce 112 | (fn [db ident] 113 | (assoc-in db [ident ::m/completed?] completed?)) 114 | db 115 | (db/all-idents-of db ::m/todo))})) 116 | 117 | -------------------------------------------------------------------------------- /src/dev/todomvc/split/env.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.env 2 | (:require 3 | [shadow.experiments.grove.db :as db] 4 | [shadow.experiments.grove.runtime :as rt] 5 | [todomvc.model :as m])) 6 | 7 | (defonce data-ref 8 | (-> {::m/id-seq 101 9 | ::m/editing nil 10 | ::m/current-filter :all} 11 | (db/configure m/schema) 12 | (atom))) 13 | 14 | (defonce rt-ref 15 | (-> {} 16 | (rt/prepare data-ref ::db))) 17 | 18 | -------------------------------------------------------------------------------- /src/dev/todomvc/split/main.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.main 2 | (:require 3 | [shadow.experiments.grove :as sg] 4 | [shadow.experiments.grove.worker-engine :as worker] 5 | [todomvc.split.views :as views])) 6 | 7 | ;; this is running in the main thread 8 | 9 | (defonce root-el (js/document.getElementById "app")) 10 | 11 | (defn ^:dev/after-load start [] 12 | (sg/start ::ui root-el (views/ui-root))) 13 | 14 | (defn init [] 15 | (sg/init ::ui 16 | {} 17 | ;; SHADOW_WORKER is created in the .html file to be started as early as possible 18 | ;; could be started here but this is saving a couple ms when doing it in the HTML 19 | ;; see examples/todomvc-split/index.html 20 | [(worker/init js/SHADOW_WORKER)]) 21 | (start)) -------------------------------------------------------------------------------- /src/dev/todomvc/split/single.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.single 2 | (:require 3 | [shadow.experiments.grove :as sg] 4 | [shadow.experiments.grove.local :as local] 5 | [todomvc.split.env :as env] 6 | [todomvc.split.views :as views] 7 | [todomvc.split.db] 8 | [shadow.experiments.grove.local :as local-eng])) 9 | 10 | ;; this is only using the main thread (no worker) 11 | ;; but the logic is still separated 12 | ;; views and db logic are identical to the split worker version 13 | ;; only the initialization changes a tiny bit 14 | 15 | (defonce root-el (js/document.getElementById "app")) 16 | 17 | (defn ^:dev/after-load start [] 18 | (sg/render env/rt-ref root-el (views/ui-root))) 19 | 20 | (defn init [] 21 | (local/init! env/rt-ref) 22 | (start)) -------------------------------------------------------------------------------- /src/dev/todomvc/split/views.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.views 2 | (:require 3 | [shadow.experiments.grove :as sg :refer (<< defc)] 4 | [todomvc.model :as m])) 5 | 6 | (defc todo-item [todo] 7 | (event ::m/edit-update! [env {:keys [todo]} e] 8 | (case (.-which e) 9 | 13 ;; enter 10 | (.. e -target (blur)) 11 | 27 ;; escape 12 | (sg/run-tx env {:e ::m/edit-cancel! :todo todo}) 13 | ;; default do nothing 14 | nil)) 15 | 16 | (event ::m/edit-complete! [env {:keys [todo]} e] 17 | (sg/run-tx env {:e ::m/edit-save! :todo todo :text (.. e -target -value)})) 18 | 19 | (bind {::m/keys [completed? editing? todo-text] :as data} 20 | (sg/query-ident todo 21 | [::m/todo-text 22 | ::m/editing? 23 | ::m/completed?])) 24 | 25 | (render 26 | (<< [:li {:class {:completed completed? 27 | :editing editing?}} 28 | [:div.view 29 | [:input.toggle {:type "checkbox" 30 | :checked completed? 31 | :on-change {:e ::m/toggle-completed! :todo todo}}] 32 | [:label {:on-dblclick {:e ::m/edit-start! :todo todo}} todo-text] 33 | [:button.destroy {:on-click {:e ::m/delete! :todo todo}}]] 34 | 35 | (when editing? 36 | (<< [:input#edit.edit {:autofocus true 37 | :on-keydown {:e ::m/edit-update! :todo todo} 38 | :on-blur {:e ::m/edit-complete! :todo todo} 39 | :value todo-text}]))]))) 40 | 41 | (defc ui-filter-select [] 42 | (bind {::m/keys [current-filter]} 43 | (sg/query-root 44 | [::m/current-filter])) 45 | 46 | (bind 47 | filter-options 48 | [{:label "All" :value :all} 49 | {:label "Active" :value :active} 50 | {:label "Completed" :value :completed}]) 51 | 52 | (render 53 | (<< [:ul.filters 54 | (sg/keyed-seq filter-options :value 55 | (fn [{:keys [label value]}] 56 | (<< [:li [:a 57 | {:class {:selected (= current-filter value)} 58 | :href "#" 59 | :on-click {:e ::m/set-filter! :filter value}} 60 | label]])))]))) 61 | 62 | (defc ui-todo-list [] 63 | (bind {::m/keys [filtered-todos] :as query} 64 | (sg/query-root 65 | [::m/filtered-todos])) 66 | 67 | (render 68 | (<< [:ul.todo-list (sg/keyed-seq filtered-todos identity todo-item)]))) 69 | 70 | (defc ui-root [] 71 | (event ::m/create-new! [env _ ^js e] 72 | (when (= 13 (.-keyCode e)) 73 | (let [input (.-target e) 74 | text (.-value input)] 75 | 76 | (when (seq text) 77 | (set! input -value "") ;; FIXME: this triggers a paint so should probably be delayed? 78 | (sg/run-tx env {:e ::m/create-new! ::m/todo-text text}))))) 79 | 80 | (event ::m/toggle-all! [env _ e] 81 | (sg/run-tx env {:e ::m/toggle-all! :completed? (-> e .-target .-checked)})) 82 | 83 | (bind {::m/keys [num-total num-active num-completed] :as query} 84 | (sg/query-root 85 | [::m/editing 86 | ::m/num-total 87 | ::m/num-active 88 | ::m/num-completed])) 89 | 90 | (render 91 | (<< [:header.header 92 | [:h1 "todos"] 93 | [:input.new-todo {:on-keydown {:e ::m/create-new!} 94 | :placeholder "What needs to be done?" 95 | :autofocus true}]] 96 | 97 | (when (pos? num-total) 98 | (<< [:section.main 99 | [:input#toggle-all.toggle-all 100 | {:type "checkbox" 101 | :on-change {:e ::m/toggle-all!} 102 | :checked false}] 103 | [:label {:for "toggle-all"} "Mark all as complete"] 104 | 105 | (ui-todo-list) 106 | 107 | [:footer.footer 108 | [:span.todo-count 109 | [:strong num-active] (if (= num-active 1) " item" " items") " left"] 110 | 111 | (ui-filter-select) 112 | 113 | (when (pos? num-completed) 114 | (<< [:button.clear-completed {:on-click {:e ::m/clear-completed!}} "Clear completed"]))]]))))) -------------------------------------------------------------------------------- /src/dev/todomvc/split/worker.cljs: -------------------------------------------------------------------------------- 1 | (ns todomvc.split.worker 2 | (:require 3 | [shadow.experiments.grove.worker :as sw] 4 | [todomvc.split.env :as env] 5 | [todomvc.split.db])) 6 | 7 | ;; this is running in the worker thread 8 | 9 | (defn ^:dev/after-load after-load [] 10 | (sw/refresh-all-queries! env/rt-ref)) 11 | 12 | (defn init [] 13 | (sw/init! env/rt-ref)) -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist) 2 | 3 | (defmacro << [& body] 4 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 5 | 6 | ;; I prefer << but <> looks more familiar to reagent :<> 7 | ;; costs nothing to have both, let the user decide 8 | (defmacro <> [& body] 9 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 10 | 11 | (defmacro fragment [& body] 12 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 13 | 14 | (defmacro html [& body] 15 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 16 | 17 | (defmacro svg [& body] 18 | (with-meta `(shadow.experiments.arborist.fragments/svg ~@body) (meta &form))) 19 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist 2 | {:doc "Arborists generally focus on the health and safety of individual plants and trees." 3 | :definition "https://en.wikipedia.org/wiki/Arborist"} 4 | (:require-macros 5 | [shadow.experiments.arborist]) 6 | (:require 7 | [shadow.experiments.arborist.protocols :as p] 8 | [shadow.experiments.arborist.fragments :as frag] 9 | [shadow.experiments.arborist.attributes :as attr] 10 | [shadow.experiments.arborist.common :as common] 11 | [shadow.experiments.arborist.collections :as coll] 12 | [goog.async.nextTick])) 13 | 14 | (set! *warn-on-infer* false) 15 | 16 | (deftype TreeRoot [container ^:mutable env ^:mutable root] 17 | p/IDirectUpdate 18 | (update! [this next] 19 | (if root 20 | (p/update! root next) 21 | (let [new-root (common/managed-root env)] 22 | (set! root new-root) 23 | (p/update! root next) 24 | (p/dom-insert root container nil) 25 | (p/dom-entered! root) 26 | ))) 27 | 28 | Object 29 | (destroy! [this ^boolean dom-remove?] 30 | (when root 31 | (p/destroy! root dom-remove?)))) 32 | 33 | (defn dom-root 34 | ([container env] 35 | {:pre [(common/in-document? container)]} 36 | (let [root (TreeRoot. container nil nil) 37 | root-env (assoc env ::root root :dom/element-fn frag/dom-element-fn)] 38 | (set! (.-env root) root-env) 39 | root)) 40 | ([container env init] 41 | (doto (dom-root container env) 42 | (p/update! init)))) 43 | 44 | (defn << [& body] 45 | (throw (ex-info "<< can only be used a macro" {}))) 46 | 47 | (defn <> [& body] 48 | (throw (ex-info "<> can only be used a macro" {}))) 49 | 50 | (defn fragment [& body] 51 | (throw (ex-info "fragment can only be used a macro" {}))) 52 | 53 | (defn simple-seq [coll render-fn] 54 | (coll/simple-seq coll render-fn)) 55 | 56 | (defn render-seq [coll key-fn render-fn] 57 | (coll/keyed-seq coll key-fn render-fn)) 58 | 59 | (defn keyed-seq [coll key-fn render-fn] 60 | (coll/keyed-seq coll key-fn render-fn)) 61 | 62 | (defn update! [x next] 63 | (p/update! x next)) 64 | 65 | (defn destroy! [root] 66 | (p/destroy! root true)) 67 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/collections.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.collections 2 | (:require [clojure.spec.alpha :as s])) 3 | 4 | ;; FIXME: unused for now but at some point fragments should have native support for collections 5 | ;; render-seq is fine but inefficient due to the often inline render-fn creation which means 6 | ;; it can never short circuit rendering because it may have captured some other locals that are 7 | ;; relevant to render besides the actual coll item 8 | 9 | (comment 10 | 11 | (s/def ::coll-args 12 | (s/cat 13 | :items any? 14 | :key-fn any? 15 | :bindings vector? ;; FIXME: core.specs for destructure help 16 | :body any?)) 17 | 18 | (s/fdef defc :coll ::defc-args) 19 | 20 | (defmacro coll [& args] 21 | (let [{:keys [items key-fn bindings body]} 22 | (s/conform ::coll-args args)] 23 | 24 | `(node 25 | ~items 26 | ~key-fn 27 | ;; FIXME: is it necessary 28 | (fn ~bindings 29 | ~body))))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/common.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.common 2 | (:require 3 | [goog.dom :as gdom] 4 | [shadow.experiments.arborist.protocols :as p])) 5 | 6 | ;; helper functions that lets us bypass the common CLJS ifn dispatch check 7 | ;; helpful in hot loops or places where the same function (or ifn) is called multiple times 8 | ;; and we just want to avoid the extra generated code 9 | 10 | ;; in certain places we care about calling instead a function directly 11 | ;; x(y) 12 | 13 | ;; instead of the inline check 14 | ;; x.cljs$core$IFn$_invoke$arity$1 ? x.cljs$core$IFn$_invoke$arity$1(y) : x.call(y) 15 | 16 | ;; I'm sure JS engines are smart enough to skip the check after a while but best not to rely on it 17 | ;; also generates less code which is always good 18 | 19 | ;; never call these in a hot loop, better to leave the check for those cases 20 | 21 | (defn ifn1-wrap ^function [x] 22 | (if ^boolean (.-cljs$core$IFn$_invoke$arity$1 x) 23 | (fn [a] 24 | (.cljs$core$IFn$_invoke$arity$1 x a)) 25 | x)) 26 | 27 | (defn ifn2-wrap ^function [x] 28 | (if ^boolean (.-cljs$core$IFn$_invoke$arity$2 x) 29 | (fn [a b c] 30 | (.cljs$core$IFn$_invoke$arity$2 x a b)) 31 | x)) 32 | 33 | (defn ifn3-wrap ^function [x] 34 | (if ^boolean (.-cljs$core$IFn$_invoke$arity$3 x) 35 | (fn [a b c] 36 | (.cljs$core$IFn$_invoke$arity$3 x a b c)) 37 | x)) 38 | 39 | (defn dom-marker 40 | ([env] 41 | (js/document.createTextNode "")) 42 | ([env label] 43 | (if ^boolean js/goog.DEBUG 44 | (js/document.createComment label) 45 | (js/document.createTextNode "")))) 46 | 47 | (defn in-document? [el] 48 | (gdom/isInDocument el)) 49 | 50 | (defn fragment-replace [old-managed new-managed] 51 | (let [first-node (p/dom-first old-managed) 52 | _ (assert (some? first-node) "fragment replacing a node that isn't in the DOM") 53 | parent (.-parentNode first-node)] 54 | 55 | (p/dom-insert new-managed parent first-node) 56 | (p/destroy! old-managed true) 57 | new-managed)) 58 | 59 | (defn replace-managed ^not-native [env old nval] 60 | (let [new (p/as-managed nval env)] 61 | (fragment-replace old new))) 62 | 63 | ;; swappable root 64 | (deftype ManagedRoot 65 | [env 66 | ^boolean ^:mutable dom-entered? 67 | ^:mutable marker 68 | ^:mutable 69 | ^not-native node 70 | ^:mutable val] 71 | 72 | p/IManaged 73 | (dom-first [this] marker) 74 | 75 | (dom-insert [this parent anchor] 76 | (.insertBefore parent marker anchor) 77 | (when node 78 | (p/dom-insert node parent anchor) 79 | )) 80 | 81 | (dom-entered! [this] 82 | (set! dom-entered? true) 83 | (when node 84 | (p/dom-entered! node))) 85 | 86 | (supports? [this next] 87 | (throw (ex-info "invalid use, don't sync roots?" {:this this :next next}))) 88 | 89 | (dom-sync! [this next] 90 | (throw (ex-info "invalid use, don't sync roots?" {:this this :next next}))) 91 | 92 | (destroy! [this ^boolean dom-remove?] 93 | (when dom-remove? 94 | (.remove marker)) 95 | (when node 96 | (p/destroy! node dom-remove?))) 97 | 98 | p/IDirectUpdate 99 | (update! [this next] 100 | (set! val next) 101 | (cond 102 | (not node) 103 | (let [el (p/as-managed val env)] 104 | (set! node el) 105 | ;; root was already inserted to dom but no node was available at the time 106 | (when-some [parent (.-parentElement marker)] 107 | (p/dom-insert node parent (.-nextSibling marker)) 108 | ;; root might not be in document yet 109 | (when dom-entered? 110 | (p/dom-entered! node)))) 111 | 112 | (p/supports? node next) 113 | (p/dom-sync! node next) 114 | 115 | :else 116 | (let [new (replace-managed env node next)] 117 | (set! node new) 118 | (when dom-entered? 119 | (p/dom-entered! new) 120 | ))))) 121 | 122 | (defn managed-root [env] 123 | (ManagedRoot. env false (dom-marker env) nil nil)) 124 | 125 | (deftype ManagedText [env ^:mutable val node] 126 | p/IManaged 127 | (dom-first [this] node) 128 | 129 | (dom-insert [this parent anchor] 130 | (.insertBefore parent node anchor)) 131 | 132 | (dom-entered! [this]) 133 | 134 | (supports? [this next] 135 | ;; FIXME: anything else? 136 | (or (string? next) 137 | (number? next) 138 | (nil? next))) 139 | 140 | (dom-sync! [this next] 141 | (when (not= val next) 142 | (set! val next) 143 | ;; https://twitter.com/_developit/status/1129093390883315712 144 | (set! node -data (str next))) 145 | :synced) 146 | 147 | (destroy! [this ^boolean dom-remove?] 148 | (when dom-remove? 149 | (.remove node)))) 150 | 151 | (defn managed-text [env val] 152 | (ManagedText. env val (js/document.createTextNode (str val)))) 153 | 154 | (extend-protocol p/IConstruct 155 | string 156 | (as-managed [this env] 157 | (managed-text env this)) 158 | 159 | number 160 | (as-managed [this env] 161 | (managed-text env this)) 162 | 163 | ;; as a placeholder for (when condition (<< [:deep [:tree]])) 164 | nil 165 | (as-managed [this env] 166 | (managed-text env this))) 167 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/dom_scheduler.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.dom-scheduler 2 | (:refer-clojure :exclude #{read})) 3 | 4 | (defmacro read! [& body] 5 | `(read!! 6 | (fn [] 7 | ~@body))) 8 | 9 | (defmacro write! [& body] 10 | `(write!! 11 | (fn [] 12 | ~@body))) 13 | 14 | (defmacro after! [& body] 15 | `(after!! 16 | (fn [] 17 | ~@body))) 18 | 19 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/dom_scheduler.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.dom-scheduler 2 | (:require-macros [shadow.experiments.arborist.dom-scheduler])) 3 | 4 | ;; microtask based queue, maybe rAf? 5 | (def task-queue (js/Promise.resolve)) 6 | 7 | (def scheduled? false) 8 | (def flushing? false) 9 | 10 | (def read-tasks #js []) 11 | (def write-tasks #js []) 12 | (def update-tasks #js []) 13 | 14 | (defn run-tasks! [^js arr] 15 | ;; (js/console.log "run-tasks!" (into [] arr)) 16 | (loop [] 17 | (when (pos? (alength arr)) 18 | (let [task (.pop arr)] 19 | (task) 20 | (recur))))) 21 | 22 | (defn run-all! [] 23 | (when-not flushing? 24 | (set! scheduled? false) 25 | (set! flushing? true) 26 | (run-tasks! read-tasks) 27 | (run-tasks! write-tasks) 28 | (run-tasks! update-tasks) 29 | (set! flushing? false))) 30 | 31 | (defn maybe-schedule! [] 32 | (when-not scheduled? 33 | (set! scheduled? true) 34 | (.then task-queue run-all!)) 35 | 36 | ;; return task-queue so callers can .then additional stuff after their task? 37 | ;; not sure this will see too much use? 38 | task-queue) 39 | 40 | (defn read!! [cb] 41 | (.push read-tasks cb) 42 | (maybe-schedule!)) 43 | 44 | (defn write!! [cb] 45 | (.push write-tasks cb) 46 | (maybe-schedule!)) 47 | 48 | (defn after!! [cb] 49 | (.push update-tasks cb) 50 | (maybe-schedule!)) 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/fragments.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.fragments 2 | (:require-macros [shadow.experiments.arborist.fragments]) 3 | (:require 4 | [shadow.experiments.arborist.protocols :as p] 5 | [shadow.experiments.arborist.attributes :as a] 6 | [shadow.experiments.arborist.common :as common])) 7 | 8 | (defn fragment-id 9 | ;; https://github.com/google/closure-compiler/wiki/Id-Generator-Annotations 10 | {:jsdoc ["@idGenerator {consistent}"]} 11 | [s] 12 | s) 13 | 14 | (defn array-equiv [a b] 15 | (let [al (alength a) 16 | bl (alength b)] 17 | ;; FIXME: identical? wouldn't work in CLJ, but = is slower in CLJS 18 | (when (identical? al bl) 19 | (loop [i 0] 20 | (if (identical? i al) 21 | true 22 | (when (= (aget a i) (aget b i)) 23 | (recur (inc i)))))))) 24 | 25 | (def svg-ns "http://www.w3.org/2000/svg") 26 | 27 | ;; FIXME: maybe take document from env, easier to mock out later 28 | (defn svg-element-fn [^Keyword type] 29 | (js/document.createElementNS svg-ns (.-name type))) 30 | 31 | (defn dom-element-fn [^Keyword type] 32 | (js/document.createElement (.-name type))) 33 | 34 | (defn get-element-fn [env element-ns] 35 | (if (identical? element-ns svg-ns) 36 | svg-element-fn 37 | dom-element-fn)) 38 | 39 | (deftype FragmentCode [create-fn mount-fn update-fn destroy-fn]) 40 | 41 | (declare ^{:arglists '([thing])} fragment-init?) 42 | 43 | (deftype ManagedFragment 44 | [env 45 | ^FragmentCode code 46 | ^:mutable vals 47 | marker 48 | exports 49 | ^boolean ^:mutable dom-entered?] 50 | 51 | p/IManaged 52 | (dom-first [this] marker) 53 | 54 | (dom-insert [this parent anchor] 55 | (.insertBefore parent marker anchor) 56 | (. code (mount-fn exports parent anchor))) 57 | 58 | (dom-entered! [this] 59 | ;; FIXME: maybe create fn in macro that saves traversing exports 60 | ;; exports may contain many regular dom nodes and those don't need this 61 | ;; but this is called once in the entire lifecycle so this should be fine 62 | (set! dom-entered? true) 63 | (.forEach exports 64 | (fn [item] 65 | (when (implements? p/IManaged item) 66 | (p/dom-entered! item) 67 | )))) 68 | 69 | (supports? [this ^FragmentInit next] 70 | (and (fragment-init? next) 71 | (identical? code (.-code next)))) 72 | 73 | (dom-sync! [this ^FragmentInit next] 74 | (let [nvals (.-vals next)] 75 | (.. code (update-fn this env exports vals nvals)) 76 | (set! vals nvals)) 77 | :synced) 78 | 79 | (destroy! [this ^boolean dom-remove?] 80 | (when dom-remove? 81 | (.remove marker)) 82 | 83 | (. code (destroy-fn env exports vals dom-remove?)))) 84 | 85 | (deftype FragmentInit [vals element-ns ^FragmentCode code] 86 | p/IConstruct 87 | (as-managed [_ env] 88 | (let [element-fn (if (nil? element-ns) (:dom/element-fn env) (get-element-fn env element-ns)) 89 | env (cond-> env (some? element-ns) (assoc :dom/element-fn element-fn :dom/svg true)) 90 | ;; create-fn creates all necessary nodes but only exports those that will be accessed later in an array 91 | ;; this might be faster if create-fn just closed over locals and returns the callbacks to be used later 92 | ;; svelte does this but CLJS doesn't allow to set! locals so it would require ugly js* code to make it work 93 | ;; didn't benchmark but the array variant shouldn't be that much slower. maybe even faster since 94 | ;; the functions don't need to be recreated for each fragment instance 95 | exports (.. code (create-fn env vals element-fn))] 96 | (ManagedFragment. env code vals (common/dom-marker env) exports false))) 97 | 98 | IEquiv 99 | (-equiv [this ^FragmentInit other] 100 | (and (instance? FragmentInit other) 101 | (identical? code (. other -code)) 102 | (array-equiv vals (.-vals other))))) 103 | 104 | (defn fragment-init? [thing] 105 | (instance? FragmentInit thing)) 106 | 107 | (defn has-no-lazy-seqs? [vals] 108 | (every? #(not (instance? cljs.core/LazySeq %)) vals)) 109 | 110 | (defn fragment-init [vals element-ns code] 111 | (assert (has-no-lazy-seqs? vals) "no lazy seqs allowed in fragments") 112 | (FragmentInit. vals element-ns code)) 113 | 114 | ;; for fallback code, relying on registry 115 | (def ^{:jsdoc ["@dict"]} known-fragments #js {}) 116 | 117 | (defn reset-known-fragments! [] 118 | (set! known-fragments #js {})) 119 | 120 | (defn create-text 121 | ;; {:jsdoc ["@noinline"]} 122 | [env text] 123 | (js/document.createTextNode text)) 124 | 125 | (defn set-attr [env node key oval nval] 126 | (a/set-attr env node key oval nval)) 127 | 128 | 129 | (defn append-child 130 | ;; {:jsdoc ["@noinline"]} 131 | [parent child] 132 | (.appendChild parent child)) 133 | 134 | (defn managed-create [env other] 135 | ;; FIXME: validate that return value implements the proper protocols 136 | (p/as-managed other env)) 137 | 138 | ;; called by macro generated code 139 | (defn managed-append [parent other] 140 | (when-not (satisfies? p/IManaged other) 141 | (throw (ex-info "cannot append-managed" {:parent parent :other other}))) 142 | (p/dom-insert other parent nil)) 143 | 144 | (defn managed-insert [component parent anchor] 145 | (p/dom-insert component parent anchor)) 146 | 147 | (defn managed-remove [component dom-remove?] 148 | (p/destroy! component dom-remove?)) 149 | 150 | ;; called by macro generated code 151 | (defn update-managed [^ManagedFragment fragment env nodes idx oval nval] 152 | ;; not comparing oval/nval because impls can do that if needed 153 | (let [^not-native el (aget nodes idx)] 154 | (if ^boolean (p/supports? el nval) 155 | (p/dom-sync! el nval) 156 | (let [next (common/replace-managed env el nval)] 157 | (aset nodes idx next) 158 | (when ^boolean (.-dom-entered? fragment) 159 | (p/dom-entered! next)))))) 160 | 161 | ;; called by macro generated code 162 | (defn update-attr [env nodes idx ^not-native attr oval nval] 163 | ;; FIXME: should maybe move the comparisons to the actual impls? 164 | (when (not= oval nval) 165 | (let [el (aget nodes idx)] 166 | (set-attr env el attr oval nval)))) 167 | 168 | (defn clear-attr [env nodes idx attr oval] 169 | (let [node (aget nodes idx)] 170 | (a/set-attr env node attr oval nil))) 171 | 172 | ;; just so the macro doesn't have to use dot interop 173 | ;; will likely be inlined by closure anyways 174 | (defn dom-insert-before [^js parent node anchor] 175 | (.insertBefore parent node anchor)) 176 | 177 | (defn dom-remove [^js node] 178 | (.remove node)) 179 | 180 | (defn css-join [from-el from-attrs] 181 | [from-el from-attrs]) 182 | 183 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/arborist/protocols.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.protocols 2 | (:refer-clojure :exclude #{swap!})) 3 | 4 | (defprotocol IManaged 5 | (^boolean supports? [this next]) 6 | (dom-sync! [this next]) 7 | 8 | (dom-insert [this parent anchor]) 9 | (dom-first [this]) 10 | 11 | ;; called after all nodes managed by this have been added to the actual document 12 | ;; might be immediately after dom-insert but may be delayed when tree is constructed 13 | ;; offscreen by something like suspense 14 | ;; implementations must properly propagate this to children if needed 15 | (dom-entered! [this]) 16 | 17 | ;; if parent node was already removed from DOM the children 18 | ;; don't need to bother removing themselves again 19 | (destroy! [this ^boolean dom-remove?])) 20 | 21 | ;; root user api 22 | (defprotocol IDirectUpdate 23 | (update! [this next])) 24 | 25 | (defprotocol IConstruct 26 | :extend-via-metadata true 27 | (as-managed [this env])) 28 | 29 | (defn identical-creator? [a b] 30 | (let [am (get (meta a) `as-managed) 31 | bm (get (meta b) `as-managed)] 32 | (and am bm (identical? am bm)))) 33 | 34 | (defprotocol IHandleDOMEvents 35 | (validate-dom-event-value! [this env event value]) 36 | (handle-dom-event! [this env event ev-value dom-event])) -------------------------------------------------------------------------------- /src/main/shadow/experiments/archetype/website.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.archetype.website 2 | (:require 3 | [clojure.edn :as edn] 4 | [clojure.java.io :as io] 5 | [clojure.string :as str] 6 | [reitit.core :as rc] 7 | [shadow.experiments.system.runtime :as rt] 8 | [shadow.undertow :as undertow] 9 | )) 10 | 11 | (defonce runtime-ref (atom nil)) 12 | 13 | (defn find-loaded-namespaces [ns-root] 14 | (let [ns-root-str (name ns-root)] 15 | (->> (all-ns) 16 | (filter #(str/starts-with? (name (ns-name %)) ns-root-str)) 17 | ))) 18 | 19 | (defn find-web-routes [ns-root] 20 | (for [ns (find-loaded-namespaces ns-root) 21 | var (vals (ns-publics ns)) 22 | :when (contains? (meta var) :http/handle)] 23 | var)) 24 | 25 | (defn build-routing-data [vars] 26 | (reduce 27 | (fn [tbl var] 28 | (let [{:http/keys [handle] :as m} (meta var) 29 | [method pattern] handle] 30 | 31 | (assoc-in tbl [pattern method] 32 | {:handler var 33 | :fqn (symbol (str (:ns m)) (str (:name m)))}))) 34 | {} 35 | vars)) 36 | 37 | (comment 38 | (-> (find-web-routes 'dummy.website) 39 | (build-routing-data) 40 | (rc/router) 41 | (rc/match-by-path "/product/1") 42 | )) 43 | 44 | (def resp-404 45 | {:status 404 46 | :headers {"content-type" "text/plain"} 47 | :body "Not found."}) 48 | 49 | ;; old habits die hard, I prefer upper case 50 | (def uc-method 51 | {:get :GET 52 | :head :HEAD 53 | :post :POST 54 | :put :PUT 55 | :delete :DELETE 56 | :connect :CONNECT 57 | :options :OPTIONS 58 | :trace :TRACE 59 | :patch :PATCH}) 60 | 61 | (defn -main [path-to-config & args] 62 | (let [time-start 63 | (System/currentTimeMillis) 64 | 65 | config 66 | (-> (io/file path-to-config) 67 | (slurp) 68 | (edn/read-string)) 69 | 70 | app-ns 71 | (get config :app/ns) 72 | 73 | ns-count-before 74 | (count (all-ns))] 75 | 76 | (require app-ns) 77 | 78 | (let [ns-count-after 79 | (count (all-ns)) 80 | 81 | time-after-load 82 | (System/currentTimeMillis) 83 | 84 | app-root 85 | (-> (get config :app/root ".") 86 | (io/file) 87 | (.getAbsoluteFile)) 88 | 89 | adjust-config 90 | (ns-resolve app-ns 'adjust-config) 91 | 92 | config 93 | (-> config 94 | (cond-> 95 | adjust-config 96 | (adjust-config))) 97 | 98 | 99 | services-fn 100 | (ns-resolve app-ns 'services) 101 | 102 | router 103 | (-> (find-web-routes app-ns) 104 | (build-routing-data) 105 | (rc/router)) 106 | 107 | base-app 108 | {::time-start time-start 109 | ::time-after-load time-after-load 110 | ::ns-count-before ns-count-before 111 | ::ns-count-after ns-count-after 112 | ::shutdown! false 113 | :config config 114 | ::router router 115 | :fs-root app-root} 116 | 117 | services 118 | (services-fn config) 119 | 120 | runtime 121 | (-> base-app 122 | (rt/init services) 123 | (rt/start-all)) 124 | 125 | 126 | ring-fn 127 | (fn [{:keys [request-method uri] :as req}] 128 | (let [match (rc/match-by-path router uri)] 129 | (if-not match 130 | resp-404 131 | (let [data (:data match) 132 | 133 | req-config 134 | (or (get data (get uc-method request-method)) 135 | (get data request-method) 136 | (get data :ANY))] 137 | 138 | ;; FIXME: handle exceptions 139 | (if-not req-config 140 | resp-404 141 | (let [handler (:handler req-config) 142 | path-params (:path-params match) 143 | req-env (assoc @runtime-ref :http/request req)] 144 | (if-not (seq path-params) 145 | (handler req-env) 146 | ;; FIXME: handle other params, use reitit stuff more 147 | ;; FIXME: handle /path/{id:int} notation 148 | (handler req-env path-params) 149 | ))))))) 150 | 151 | ;; basic inner handler setup 152 | http-handler 153 | [::undertow/ws-upgrade 154 | [::undertow/ws-ring {:handler-fn ring-fn}] 155 | [::undertow/blocking 156 | [::undertow/ring {:handler-fn ring-fn}]]] 157 | 158 | ;; static from classpath or files 159 | http-handler 160 | (reduce 161 | (fn [http-handler path] 162 | (if (str/starts-with? path "classpath:") 163 | [::undertow/classpath {:root (subs path 10)} http-handler] 164 | [::undertow/file {:root-dir (io/file app-root path)} http-handler])) 165 | http-handler 166 | (:http/roots config)) 167 | 168 | ;; final handler, compress 169 | http-handler 170 | [::undertow/compress {} http-handler] 171 | 172 | http-server 173 | (undertow/start 174 | {:host "0.0.0.0" 175 | :port 8090} 176 | http-handler)] 177 | 178 | (prn :started) 179 | (prn runtime) 180 | 181 | (reset! runtime-ref (assoc runtime :http/server http-server 182 | :http/handler http-handler)) 183 | 184 | (while (not @(::shutdown! @runtime-ref)) 185 | (Thread/sleep 2500)) 186 | 187 | (undertow/stop http-server) 188 | 189 | (rt/stop-all @runtime-ref) 190 | (prn :done)))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove) 2 | 3 | ;; just for convenience, less imports for the user 4 | 5 | (defmacro defc [& args] 6 | `(shadow.experiments.grove.components/defc ~@args)) 7 | 8 | (defmacro << [& body] 9 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 10 | 11 | ;; I prefer << but <> looks more familiar to reagent :<> 12 | ;; costs nothing to have both, let the user decide 13 | (defmacro <> [& body] 14 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 15 | 16 | (defmacro fragment [& body] 17 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 18 | 19 | (defmacro html [& body] 20 | (with-meta `(shadow.experiments.arborist.fragments/html ~@body) (meta &form))) 21 | 22 | (defmacro svg [& body] 23 | (with-meta `(shadow.experiments.arborist.fragments/svg ~@body) (meta &form))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/cards/env.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.cards.env) 2 | 3 | (defonce cards-ref (atom {})) 4 | 5 | (defn register-card [id opts rendered] 6 | (swap! cards-ref update id merge {:id id 7 | :opts opts 8 | :dirty? true 9 | :rendered rendered})) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/cards/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.cards.runner 2 | (:require 3 | [shadow.experiments.grove.cards.env :as gce] 4 | [shadow.experiments.arborist.protocols :as ap] 5 | [shadow.experiments.grove :as sg :refer (<<)] 6 | [shadow.experiments.grove.db :as db] 7 | [shadow.experiments.arborist.common :as common] 8 | [shadow.experiments.grove.protocols :as gp] 9 | [shadow.experiments.arborist.attributes :as attr] 10 | [shadow.experiments.arborist.fragments :as frag])) 11 | 12 | (deftype LocalQueryEngine [db-data stream-data] 13 | gp/IQueryEngine 14 | (query-init [this key query config callback] 15 | (let [result (db/query {} db-data query)] 16 | (js/console.log "query-init" key query config result) 17 | ;; mimic async queries for now? 18 | (js/setTimeout #(callback result) 0))) 19 | (query-destroy [this key]) 20 | (transact! [this tx] 21 | (js/console.log "tx" tx))) 22 | 23 | (defn test-query-engine [schema static-db static-streams] 24 | (let [db-data (db/configure static-db schema)] 25 | (fn [env] 26 | (assoc env ::gp/query-engine (LocalQueryEngine. db-data static-streams))))) 27 | 28 | (defn make-card-env [{:keys [id opts] :as card}] 29 | (let [{:keys [schema db streams]} opts] 30 | (-> (sg/init* id 31 | {:dom/element-fn frag/dom-element-fn} 32 | [(test-query-engine schema db streams)])))) 33 | 34 | (defn start [] 35 | (let [cards 36 | (->> @gce/cards-ref 37 | (vals) 38 | (sort-by :id))] 39 | 40 | (doseq [{:keys [id rendered root managed dirty?] :as card} cards 41 | ;; :when dirty? 42 | ] 43 | (let [env (make-card-env card) 44 | wrapper (<< [:div.p-4 45 | [:div.border.shadow 46 | [:div.p-1.bg-gray-100 (str id)] 47 | [:div.border-t {:style {:height "300px"}} 48 | rendered]]]) 49 | new (ap/as-managed wrapper env) 50 | root (or root (js/document.createElement "div"))] 51 | 52 | (if managed 53 | (do (common/fragment-replace managed new)) 54 | (do (js/document.body.appendChild root) 55 | (ap/dom-insert new root nil))) 56 | 57 | (ap/dom-entered! new) 58 | (swap! gce/cards-ref update id merge {:root root :managed new :env env :dirty? false}) 59 | )))) 60 | 61 | (defn stop [done] 62 | (done)) 63 | 64 | (defn ^:export init [] 65 | (js/console.log "init") 66 | (start)) 67 | 68 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/css_transition.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.css-transition 2 | (:require 3 | [shadow.experiments.arborist.attributes :as sa] 4 | [shadow.experiments.arborist.dom-scheduler :as ds] 5 | [shadow.experiments.grove.protocols :as gp] 6 | [goog.reflect :as gr])) 7 | 8 | 9 | ;; extremely simplistic css class based transitions 10 | ;; instead of living in the virtual DOM tree it requires calling 11 | ;; the trigger-out! manually (usually from an event) 12 | ;; it takes a callback which will be called after the transition ends 13 | ;; and should result in removing the actual node from the tree or so 14 | ;; otherwise it'll revert to the state it is in without transition classes 15 | 16 | ;; will use the usual 17 | ;; -enter 18 | ;; -enter-active 19 | ;; -exit 20 | ;; -exit-active 21 | 22 | ;; FIXME: appear support? 23 | 24 | (defprotocol DomTransition 25 | (trigger-out! [this callback]) 26 | (set-node! [this node])) 27 | 28 | (deftype ClassTransition [class ^:mutable ^js element ^:mutable stage component-handle] 29 | gp/IBuildHook 30 | (hook-build [this ch] 31 | (ClassTransition. class element stage ch)) 32 | 33 | gp/IHook 34 | (hook-init! [this]) 35 | (hook-ready? [this] true) 36 | (hook-value [this] this) 37 | (hook-update! [this] false) 38 | (hook-deps-update! [this next] false) 39 | (hook-destroy! [this]) 40 | 41 | gp/IHookDomEffect 42 | (hook-did-update! [this ^boolean did-render?] 43 | (when (and did-render? (= stage :mount)) 44 | (set! stage :active) 45 | 46 | (let [class-enter (str class "-enter") 47 | class-enter-active (str class "-enter-active")] 48 | 49 | ;; FIXME: this isn't reliable if there are :hover transitions or so 50 | (.addEventListener element "transitionend" 51 | (fn [e] 52 | (.. element -classList (remove class-enter-active))) 53 | #js {:once true}) 54 | 55 | (ds/write! 56 | (.. element -classList (add class-enter)) 57 | 58 | (ds/read! 59 | 60 | ;; CSS trigger 61 | (gr/sinkValue (.-scrollTop element)) 62 | 63 | (ds/write! 64 | (.. element -classList (add class-enter-active)) 65 | (.. element -classList (remove class-enter)))))))) 66 | 67 | DomTransition 68 | (trigger-out! [this callback] 69 | 70 | (let [class-exit (str class "-exit") 71 | class-exit-active (str class "-exit-active")] 72 | 73 | ;; FIXME: this isn't reliable if there are :hover transitions or so 74 | (.addEventListener element "transitionend" 75 | (fn [e] 76 | (ds/write! 77 | (.. element -classList (remove class-exit-active)) 78 | (callback))) 79 | #js {:once true}) 80 | 81 | (ds/write! 82 | (.. element -classList (add class-exit)) 83 | 84 | (ds/read! 85 | 86 | ;; CSS trigger 87 | (gr/sinkValue (.-scrollTop element)) 88 | 89 | (ds/write! 90 | (.. element -classList (add class-exit-active)) 91 | (.. element -classList (remove class-exit))))))) 92 | 93 | (set-node! [this new] 94 | (when element 95 | (throw (ex-info "already have an element?" {:new new :this this}))) 96 | 97 | (set! element new))) 98 | 99 | (defn class-transition [class] 100 | (ClassTransition. class nil :mount nil nil)) 101 | 102 | (sa/add-attr ::ref 103 | (fn [env node oval nval] 104 | (when nval 105 | (set-node! nval node)))) 106 | 107 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/edn.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.edn 2 | (:require 3 | [cljs.reader :as reader] 4 | [shadow.experiments.grove.runtime :as rt])) 5 | 6 | (defn init! [rt-ref opts] 7 | (let [edn-read 8 | (fn edn-read [data] 9 | (reader/read-string opts data))] 10 | 11 | (swap! rt-ref assoc 12 | ::rt/edn-read edn-read 13 | ::rt/edn-str pr-str))) 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/effects.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.effects 2 | (:require 3 | [shadow.experiments.arborist.attributes :as saa] 4 | [shadow.experiments.grove.components :as comp] 5 | [shadow.experiments.grove.protocols :as gp])) 6 | 7 | ;; FIXME: make this actually useful, not just a dummy effect 8 | 9 | (deftype EffectHook [^:mutable dom-node component-handle] 10 | gp/IBuildHook 11 | (hook-build [this ch] 12 | (EffectHook. dom-node ch)) 13 | 14 | gp/IHook 15 | (hook-init! [this]) 16 | (hook-ready? [this] true) 17 | (hook-value [this] this) 18 | (hook-update! [this] 19 | (throw (ex-info "effect hook update, TBD" {}))) 20 | (hook-deps-update! [this new-val] 21 | (throw (ex-info "effect hook update, TBD" {}))) 22 | (hook-destroy! [this] 23 | ;; track if running and maybe do early cleanup 24 | ) 25 | 26 | IFn 27 | (-invoke [this] 28 | (.trigger! this)) 29 | (-invoke [this after] 30 | (.trigger! this) 31 | (js/setTimeout #(gp/run-now! (gp/get-scheduler component-handle) after ::effect-hook) 200)) 32 | 33 | Object 34 | (trigger! [this after] 35 | (set! (.. dom-node -style -transition) "opacity 200ms ease-out, transform 200ms ease-out") 36 | ;; (set! (.. dom-node -style -transform) "scale(1)") 37 | ;; css trigger 38 | ;; (js/goog.reflect.sinkValue (.. dom-node -offsetWidth)) 39 | (set! (.. dom-node -style -opacity) 0) 40 | (set! (.. dom-node -style -transform) "scale(0)")) 41 | 42 | (set-node! [this node] 43 | (when (and dom-node (not (identical? node dom-node))) 44 | (throw (ex-info "already had a node" {:node node :dom-node dom-node}))) 45 | (set! dom-node node))) 46 | 47 | (defn make-test-effect [ignored] 48 | (EffectHook. nil nil nil)) 49 | 50 | (saa/add-attr ::effect 51 | (fn [env ^js node oval ^EffectHook nval] 52 | {:pre [(instance? EffectHook nval)]} 53 | (.set-node! nval node))) 54 | 55 | ;; FIXME: figure out how to make custom fx without getting too OOP-ish 56 | (def fade-out 57 | {:init (fn [args] (js/console.log "fade-out-init") {}) 58 | :start (fn [args] {}) 59 | :stop (fn [args] {})}) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/events.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.events 2 | (:require 3 | [cljs.env :as env] 4 | [cljs.analyzer :as ana] 5 | [clojure.string :as str] 6 | [shadow.lazy :as sl])) 7 | 8 | (defn module-for-ns [compiler-env ns] 9 | (get-in compiler-env [::sl/ns->mod ns] ::no-mods)) 10 | 11 | ;; the point of this is getting the side-effect of registering the event handler to a controlled place 12 | ;; instead of having them happen at namespace loading time 13 | 14 | ;; and also having actual defns that can be tinkered with from the REPL 15 | ;; having the user write this by hand kinda sucks 16 | 17 | ;; (defn my-ev-handler [env ev] ...) 18 | ;; (ev/reg-event env/rt-ref ::my-ev my-ev-handler) 19 | 20 | ;; also gets rid of the constant references to rt-ref which would maybe allow isolating that more 21 | 22 | ;; could instead create a defevent macro but that would require taking the rt-ref argument 23 | ;; and still execute the register side-effect on-load 24 | ;; and maybe not register with Cursive etc for find-usages 25 | 26 | ;; anonymous functions suck because you can never reference them directly anywhere 27 | ;; (ev/reg-event env/rt-ref ::my-ev (fn [env ev] ...)) 28 | 29 | ;; with defn they can easily compose and one event handler can easily directly call another 30 | 31 | (defmacro register-events! [rt-ref-sym] 32 | (let [env 33 | &env 34 | 35 | current-ns 36 | (ana/gets env :ns :name) 37 | 38 | ns-str 39 | (str current-ns) 40 | 41 | ;; FIXME: should this really be enforced here? 42 | ;; only allowing "magic" registering of events that belong to the actual project 43 | ;; libraries should just call reg-event directly when initializing 44 | ns-prefix 45 | (subs ns-str 0 (inc (str/last-index-of ns-str "."))) 46 | 47 | compiler-env 48 | @env/*compiler* 49 | 50 | our-mod 51 | (module-for-ns compiler-env current-ns)] 52 | 53 | ;; make it a hard error when the namespace calling this isn't marked with :dev/always 54 | ;; otherwise it'll have unpredictable caching issues 55 | 56 | (when-not (get-in compiler-env [::ana/namespaces current-ns :meta :dev/always]) 57 | (throw (ana/error env "Namespace using register-events! must use :dev/always meta"))) 58 | 59 | `(do 60 | ~@(for [{:keys [name defs]} (vals (::ana/namespaces compiler-env)) 61 | :when (let [ns-mod (module-for-ns compiler-env name)] 62 | (and (not= name current-ns) 63 | (= our-mod ns-mod) 64 | (str/starts-with? (str name) ns-prefix))) 65 | 66 | {fq-name :name def-meta :meta :as def} (vals defs) 67 | 68 | :let [ev-handle (get def-meta ::handle)] 69 | :when ev-handle 70 | ev-id (cond 71 | ;; {::ev/handle ::some/event} specific event to handle 72 | (keyword? ev-handle) 73 | [ev-handle] 74 | 75 | ;; {::ev/handle true} equals event name matches defn name 76 | (true? ev-handle) 77 | [(keyword (namespace ev-handle) (name ev-handle))] 78 | 79 | ;; multiple events 80 | ;; {::ev/handle [::some/event ::other/event]} 81 | ;; FIXME: validate seq of actual keywords 82 | (seq ev-handle) 83 | ev-handle 84 | 85 | ;; FIXME: this can't throw for the other ns since that is already compiled 86 | ;; but can we make it look it came from there? might require mods in shadow-cljs 87 | :else 88 | (throw (ana/error env (str "event handler used invalid handles value: " ev-handle))))] 89 | 90 | ;; add a reg-event call for every event handler found so user can skip it 91 | ;; FIXME: should maybe have a secondary reg-event in dev that takes additional data 92 | ;; so that runtime errors can accurately point to the proper defn? 93 | ;; FIXME: could maybe do additional validation here 94 | ;; could also use some additional metadata maybe 95 | `(reg-event ~rt-ref-sym ~ev-id ~fq-name) 96 | )))) 97 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/examples/cljs_editor.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.examples.cljs-editor 2 | (:require 3 | ["codemirror" :as cm] 4 | ["codemirror/mode/clojure/clojure"] 5 | ["parinfer-codemirror" :as par-cm] 6 | [clojure.string :as str] 7 | [shadow.experiments.arborist.protocols :as ap] 8 | [shadow.experiments.arborist.common :as common] 9 | [shadow.experiments.arborist.dom-scheduler :as ds] 10 | [shadow.experiments.grove :as sg] 11 | [shadow.experiments.grove.components :as comp] 12 | [shadow.experiments.grove.protocols :as gp])) 13 | 14 | (deftype EditorRoot 15 | [env 16 | marker 17 | ^:mutable opts 18 | ^:mutable editor 19 | ^:mutable editor-el] 20 | 21 | ap/IManaged 22 | (supports? [this next] 23 | (ap/identical-creator? opts next)) 24 | 25 | (dom-sync! [this next-opts] 26 | (let [{:keys [value cm-opts]} next-opts] 27 | 28 | (when (and editor (seq value) (not= value (.getValue editor))) 29 | (.setValue editor value)) 30 | 31 | (reduce-kv 32 | (fn [_ key val] 33 | (.setOption editor (name key) val)) 34 | nil 35 | cm-opts) 36 | 37 | (set! opts next-opts) 38 | )) 39 | 40 | (dom-insert [this parent anchor] 41 | (.insertBefore parent marker anchor)) 42 | 43 | (dom-first [this] 44 | (or editor-el marker)) 45 | 46 | ;; codemirror doesn't render correctly if added to an element 47 | ;; that isn't actually in the dcoument, so we delay construction until actually entered 48 | ;; codemirror also does a bunch of force layouts/render when mounting 49 | ;; which kill performance quite badly 50 | (dom-entered! [this] 51 | (ds/write! 52 | (let [{:keys [editor-ref value cm-opts clojure]} 53 | opts 54 | 55 | ;; FIXME: this config stuff needs to be cleaned up, this is horrible 56 | cm-opts 57 | (js/Object.assign 58 | #js {:lineNumbers true 59 | :theme "github"} 60 | (when cm-opts (clj->js cm-opts)) 61 | (when-not (false? clojure) 62 | #js {:mode "clojure" 63 | :matchBrackets true}) 64 | (when (seq value) 65 | #js {:value value})) 66 | 67 | ed 68 | (cm. 69 | (fn [el] 70 | (set! editor-el el) 71 | (.insertBefore (.-parentElement marker) el marker)) 72 | cm-opts) 73 | 74 | ;; FIXME: this sucks 75 | submit-fn 76 | (fn [e] 77 | (let [val (str/trim (.getValue ed))] 78 | (when (seq val) 79 | (let [comp (comp/get-component env)] 80 | (gp/handle-event! comp (assoc (:submit-event opts) :code val) e env)))))] 81 | 82 | (set! editor ed) 83 | 84 | (when editor-ref 85 | (vreset! editor-ref ed)) 86 | 87 | ;; FIXME: this sucks, find a better way to handle configuration like this 88 | (when (:submit-event opts) 89 | (.setOption ed "extraKeys" 90 | #js {"Ctrl-Enter" submit-fn 91 | "Shift-Enter" submit-fn})) 92 | 93 | (when-not (false? clojure) 94 | (par-cm/init ed))))) 95 | 96 | (destroy! [this dom-remove?] 97 | 98 | (when-some [editor-ref (:editor-ref opts)] 99 | (vreset! editor-ref nil)) 100 | 101 | (when dom-remove? 102 | (when editor-el 103 | ;; FIXME: can't find a dispose method on codemirror? 104 | (.remove editor-el)) 105 | (.remove marker)))) 106 | 107 | (defn make-editor [opts env] 108 | (EditorRoot. env (common/dom-marker env) opts nil nil)) 109 | 110 | (defn editor [opts] 111 | (with-meta opts {`ap/as-managed make-editor})) 112 | 113 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/examples/env.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.examples.env 2 | (:require 3 | [cljs.env :as cljs-env] 4 | [shadow.experiments.grove.runtime :as gr] 5 | [shadow.experiments.grove.db :as db] 6 | [shadow.experiments.grove.examples.model :as m])) 7 | 8 | (defonce data-ref 9 | (-> {::m/example-tab :result 10 | ::m/example-result "No Result yet."} 11 | (db/configure m/schema) 12 | (atom))) 13 | 14 | (defonce rt-ref 15 | (-> {::m/compile-state-ref (cljs-env/default-compiler-env)} 16 | (gr/prepare data-ref ::app))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/examples/js_editor.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.examples.js-editor 2 | (:require 3 | ["codemirror" :as cm] 4 | ["codemirror/mode/javascript/javascript"] 5 | [clojure.string :as str] 6 | [shadow.experiments.arborist.protocols :as ap] 7 | [shadow.experiments.arborist.common :as common] 8 | [shadow.experiments.arborist.dom-scheduler :as ds] 9 | [shadow.experiments.grove :as sg] 10 | [shadow.experiments.grove.components :as comp] 11 | [shadow.experiments.grove.protocols :as gp])) 12 | 13 | (deftype EditorRoot 14 | [env 15 | marker 16 | ^:mutable opts 17 | ^:mutable editor 18 | ^:mutable editor-el] 19 | 20 | ap/IManaged 21 | (supports? [this next] 22 | (ap/identical-creator? opts next)) 23 | 24 | (dom-sync! [this next-opts] 25 | (let [{:keys [value cm-opts]} next-opts] 26 | 27 | (when (and editor (seq value)) 28 | (.setValue editor value)) 29 | 30 | (reduce-kv 31 | (fn [_ key val] 32 | (.setOption editor (name key) val)) 33 | nil 34 | cm-opts) 35 | 36 | (set! opts next-opts) 37 | )) 38 | 39 | (dom-insert [this parent anchor] 40 | (.insertBefore parent marker anchor)) 41 | 42 | (dom-first [this] 43 | (or editor-el marker)) 44 | 45 | ;; codemirror doesn't render correctly if added to an element 46 | ;; that isn't actually in the dcoument, so we delay construction until actually entered 47 | ;; codemirror also does a bunch of force layouts/render when mounting 48 | ;; which kill performance quite badly 49 | (dom-entered! [this] 50 | (ds/write! 51 | (let [{:keys [value]} 52 | opts 53 | 54 | ;; FIXME: this config stuff needs to be cleaned up, this is horrible 55 | cm-opts 56 | (js/Object.assign 57 | #js {:lineNumbers true 58 | :theme "github" 59 | :mode "javascript" 60 | :matchBrackets true 61 | :readOnly true 62 | :autofocus false} 63 | (when (seq value) 64 | #js {:value value})) 65 | 66 | ed 67 | (cm. 68 | (fn [el] 69 | (set! editor-el el) 70 | (.insertBefore (.-parentElement marker) el marker)) 71 | cm-opts)] 72 | 73 | (set! editor ed)))) 74 | 75 | (destroy! [this dom-remove?] 76 | (when dom-remove? 77 | (when editor-el 78 | ;; FIXME: can't find a dispose method on codemirror? 79 | (.remove editor-el)) 80 | (.remove marker)))) 81 | 82 | (defn make-editor [opts env] 83 | (EditorRoot. env (common/dom-marker env) opts nil nil)) 84 | 85 | (defn editor [opts] 86 | (with-meta opts {`ap/as-managed make-editor})) 87 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/examples/model.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.examples.model) 2 | 3 | (def schema 4 | { ;; ::m/runtime 5 | ;; {:type :entity :attrs {:runtime-id [:primary-key number?]}} 6 | }) 7 | 8 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/history.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.history 2 | (:require 3 | [clojure.string :as str] 4 | [shadow.experiments.grove :as sg] 5 | [shadow.experiments.grove.runtime :as rt] 6 | [shadow.experiments.grove.events :as ev] 7 | [shadow.experiments.arborist.attributes :as attr])) 8 | 9 | (defn init! 10 | [rt-ref 11 | {:keys [start-token path-prefix use-fragment root-el] 12 | :or {start-token "/" 13 | path-prefix "" 14 | use-fragment false} 15 | :as config}] 16 | 17 | {:pre [(or (= "" path-prefix) 18 | (and (string? path-prefix) 19 | (str/starts-with? path-prefix "/") 20 | (not (str/ends-with? path-prefix "/")))) 21 | 22 | (or (= "/" start-token) 23 | (and (str/starts-with? start-token "/") 24 | (not (str/ends-with? start-token "/")))) 25 | ]} 26 | 27 | (let [get-token 28 | (fn [] 29 | (if-not use-fragment 30 | (let [path js/window.location.pathname] 31 | (cond 32 | (= path path-prefix) 33 | "/" 34 | 35 | (str/starts-with? path path-prefix) 36 | (subs path (count path-prefix)) 37 | 38 | :else 39 | (throw (ex-info "path did not match path prefix" {:path path :path-prefix path-prefix})) 40 | )) 41 | (let [hash js/window.location.hash] 42 | ;; always start everything with a / even when using hash 43 | ;; is "" when url doesn't have a hash, otherwise #foo 44 | (if (= hash "") 45 | "/" 46 | (subs js/window.location.hash (+ 1 (count path-prefix))))))) 47 | 48 | trigger-route! 49 | (fn [] 50 | ;; token must start with /, strip it to get tokens vector 51 | (let [token (get-token) 52 | tokens (str/split (subs token 1) #"/")] 53 | 54 | (sg/run-tx! rt-ref {:e :ui/route! :token token :tokens tokens}))) 55 | 56 | first-token 57 | (get-token)] 58 | 59 | (attr/add-attr :ui/href 60 | (fn [env node oval nval] 61 | (when nval 62 | (when-not (str/starts-with? nval "/") 63 | (throw (ex-info (str ":ui/href must start with / got " nval) 64 | {:val nval}))) 65 | 66 | (set! node -href 67 | (if use-fragment 68 | (str "#" path-prefix nval) 69 | (str path-prefix 70 | (if-not (str/ends-with? path-prefix "/") 71 | nval 72 | (subs nval 1)))))))) 73 | 74 | (ev/reg-fx rt-ref :ui/redirect! 75 | (fn [{:keys [transact!] :as env} {:keys [token title]}] 76 | {:pre [(str/starts-with? token "/")]} 77 | 78 | (js/window.history.pushState 79 | nil 80 | (or title js/document.title) 81 | (str path-prefix token)) 82 | 83 | (let [tokens (str/split (subs token 1) #"/")] 84 | ;; FIXME: there needs to be cleaner way to start another tx from fx 85 | ;; currently forcing them to be async so the initial tx can conclude 86 | (js/setTimeout #(transact! {:e :ui/route! :token token :tokens tokens}) 0) 87 | ))) 88 | 89 | ;; immediately trigger initial route when this is initialized 90 | ;; don't wait for first env-init, thats problematic with multiple roots 91 | (trigger-route!) 92 | 93 | (swap! rt-ref 94 | (fn [rt] 95 | (-> rt 96 | (assoc ::config config) 97 | (update ::rt/env-init conj 98 | (fn [env] 99 | ;; fragment uses hashchange event so we can skip checking clicks 100 | (when-not use-fragment 101 | (.addEventListener (or root-el js/document.body) "click" 102 | (fn [^js e] 103 | (when (and (zero? (.-button e)) 104 | (not (or (.-shiftKey e) (.-metaKey e) (.-ctrlKey e) (.-altKey e)))) 105 | (when-let [a (some-> e .-target (.closest "a"))] 106 | 107 | (let [href (.getAttribute a "href") 108 | a-target (.getAttribute a "target")] 109 | 110 | (when (and href (seq href) (str/starts-with? href path-prefix) (nil? a-target)) 111 | (.preventDefault e) 112 | 113 | (js/window.history.pushState nil js/document.title href) 114 | 115 | (trigger-route!) 116 | ))))))) 117 | 118 | (when (and (= "/" first-token) (seq start-token)) 119 | (js/window.history.replaceState 120 | nil 121 | js/document.title 122 | (str (when use-fragment "#") path-prefix start-token))) 123 | 124 | (js/window.addEventListener "popstate" 125 | (fn [e] 126 | (trigger-route!))) 127 | 128 | (when use-fragment 129 | (js/window.addEventListener "hashchange" 130 | (fn [e] 131 | (trigger-route!)))) 132 | 133 | env))))))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/keyboard.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.keyboard 2 | (:require 3 | [goog.events :as gev] 4 | [clojure.string :as str] 5 | [shadow.experiments.arborist.attributes :as sa] 6 | [shadow.experiments.grove.ui.util :as util] 7 | [shadow.experiments.grove.protocols :as gp] 8 | [shadow.experiments.grove.components :as comp] 9 | ) 10 | (:import [goog.events KeyHandler EventType])) 11 | 12 | (util/assert-not-in-worker!) 13 | 14 | ;; FIXME: this produces alt+alt, ctrl+ctrl for blank alt/control presses 15 | ;; not interested in those for now 16 | (defn str-key [^goog e] 17 | (->> [(and (.-ctrlKey e) "ctrl") 18 | (and (.-altKey e) "alt") 19 | (and (.-metaKey e) "meta") 20 | (and (.-shiftKey e) "shift") 21 | (str/lower-case (.-key e))] 22 | (filter identity) 23 | (str/join "+"))) 24 | 25 | (def this-ns (namespace ::listen)) 26 | 27 | (sa/add-attr ::listen 28 | (fn [env ^js node oval nval] 29 | ;; FIXME: should throw when used without component 30 | ;; FIXME: should dispose key-handler when node/fragment is unmounted but there is no way to hook into that yet 31 | (when-some [comp (comp/get-component env)] 32 | 33 | (cond 34 | ;; off->on 35 | (and (not oval) nval) 36 | (let [key-handler (KeyHandler. node)] 37 | (set! node -shadow$key-handler key-handler) 38 | (.listen key-handler "key" 39 | (fn [^goog e] 40 | (let [event-id (keyword this-ns (str-key e))] 41 | ;; (js/console.log "checking event" event-id comp env) 42 | (when-some [handler (get (comp/get-events comp) event-id)] 43 | ;; FIXME: needs fixing when using to event maps 44 | (handler env [event-id] e)))))) 45 | 46 | ;; on->off 47 | (and (not nval) oval) 48 | (when-some [key-handler (.-shadow$key-handler node)] 49 | (.dispose key-handler)) 50 | 51 | ;; on->on 52 | :else 53 | nil)))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/preload.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.preload 2 | (:require 3 | [shadow.remote.runtime.api :as p] 4 | [shadow.remote.runtime.shared :as shared] 5 | [shadow.cljs.devtools.client.shared :as cljs-shared] 6 | [shadow.experiments.grove.db :as db] 7 | [shadow.experiments.grove.runtime :as rt] 8 | [clojure.core.protocols :as cp])) 9 | 10 | (defn get-databases [{:keys [runtime]} msg] 11 | (shared/reply runtime msg 12 | {:op :db/list-databases 13 | ;; just keywords or more details? don't actually have any? 14 | :databases 15 | (-> (keys @rt/known-runtimes-ref) 16 | (set))})) 17 | 18 | (defn get-tables [{:keys [runtime]} {:keys [db] :as msg}] 19 | (let [env-ref (get @rt/known-runtimes-ref db) 20 | data-ref (get @env-ref ::rt/data-ref) 21 | data @data-ref 22 | {::db/keys [ident-types]} (meta data)] 23 | 24 | (shared/reply runtime msg 25 | {:op :db/list-tables 26 | ;; just keywords or more details? don't actually have any? 27 | :tables (conj ident-types :db/globals)}))) 28 | 29 | (defn get-table-columns [{:keys [runtime]} {:keys [db table] :as msg}] 30 | (let [env-ref (get @rt/known-runtimes-ref db) 31 | data-ref (get @env-ref ::rt/data-ref) 32 | db @data-ref 33 | 34 | ;; FIXME: likely doesn't need all rows, should just take a random sample 35 | known-keys-of-table 36 | (->> (db/all-of db table) 37 | (mapcat keys) 38 | (set))] 39 | 40 | (shared/reply runtime msg 41 | {:op :db/list-table-columns 42 | :columns known-keys-of-table}))) 43 | 44 | (defn get-rows [{:keys [runtime]} {:keys [db table offset count] :as msg}] 45 | (let [env-ref (get @rt/known-runtimes-ref db) 46 | data-ref (get @env-ref ::rt/data-ref) 47 | data @data-ref 48 | rows 49 | (->> (db/all-of data table) 50 | (sort-by :db/ident) 51 | (vec))] 52 | 53 | ;; FIXME: slice data, don't send everything 54 | 55 | (shared/reply runtime msg 56 | {:op :db/list-rows 57 | :rows rows}))) 58 | 59 | (defn get-entry 60 | [{:keys [runtime]} 61 | {:keys [db table row] :as msg}] 62 | (let [env-ref (get @rt/known-runtimes-ref db) 63 | data-ref (get @env-ref ::rt/data-ref) 64 | db @data-ref 65 | ident (if (= table :db/globals) row (db/make-ident table row)) 66 | val (get db ident)] 67 | 68 | (shared/reply runtime msg 69 | {:op :db/entry :row val}))) 70 | 71 | (cljs-shared/add-plugin! ::db-explorer #{} 72 | (fn [{:keys [runtime] :as env}] 73 | (let [svc 74 | {:runtime runtime}] 75 | 76 | ;; maybe just return the ops? 77 | ;; dunno if this extra layer is needed 78 | (p/add-extension runtime 79 | ::db-explorer 80 | {:ops 81 | {:db/get-databases #(get-databases svc %) 82 | :db/get-tables #(get-tables svc %) 83 | :db/get-table-columns #(get-table-columns svc %) 84 | :db/get-rows #(get-rows svc %) 85 | :db/get-entry #(get-entry svc %)} 86 | ;; :on-tool-disconnect #(tool-disconnect svc %) 87 | }) 88 | svc)) 89 | (fn [{:keys [runtime] :as svc}] 90 | (p/del-extension runtime ::db-explorer))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/protocols.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.protocols) 2 | 3 | 4 | ;; not using record since they shouldn't act as maps 5 | ;; also does a bunch of other stuff I don't want 6 | (deftype Ident [entity-type id ^:mutable _hash] 7 | ILookup 8 | (-lookup [this key] 9 | (case key 10 | :entity-type entity-type 11 | :id id 12 | nil)) 13 | 14 | IHash 15 | (-hash [this] 16 | (if (some? _hash) 17 | _hash 18 | (let [x (bit-or 123 (hash id) (hash id))] 19 | (set! _hash x) 20 | x))) 21 | IEquiv 22 | (-equiv [this ^Ident other] 23 | (and (instance? Ident other) 24 | (keyword-identical? entity-type (.-entity-type other)) 25 | (= id (.-id other))))) 26 | 27 | (defprotocol IWork 28 | (work! [this])) 29 | 30 | (defprotocol IHandleEvents 31 | ;; e and origin can be considered optional and will be ignored by most actual handlers 32 | (handle-event! [this ev-map e origin])) 33 | 34 | 35 | (defprotocol IHook 36 | (hook-init! [this]) 37 | (hook-ready? [this]) 38 | (hook-value [this]) 39 | ;; true-ish return if component needs further updating 40 | (hook-deps-update! [this val]) 41 | (hook-update! [this]) 42 | (hook-destroy! [this])) 43 | 44 | (defprotocol IHookDomEffect 45 | (hook-did-update! [this did-render?])) 46 | 47 | (defprotocol IBuildHook 48 | (hook-build [this component-handle])) 49 | 50 | (defprotocol IComponentHookHandle 51 | (hook-invalidate! [this] "called when a hook wants the component to refresh")) 52 | 53 | (defprotocol IEnvSource 54 | (get-component-env [this])) 55 | 56 | (defprotocol ISchedulerSource 57 | (get-scheduler [this])) 58 | 59 | ;; just here so that working on components file doesn't cause hot-reload issues 60 | ;; with already constructed components 61 | (deftype ComponentConfig 62 | [component-name 63 | hooks 64 | opts 65 | check-args-fn 66 | render-deps 67 | render-fn 68 | events]) 69 | 70 | (defprotocol IQueryEngine 71 | ;; each engine may have different requirements regarding interop with the components 72 | ;; websocket engine can only do async queries 73 | ;; local engine can do sync queries but might have async results 74 | ;; instead of trying to write a generic one they should be able to specialize 75 | (query-hook-build [this env component-handle ident query config]) 76 | 77 | ;; hooks may use these but they may also interface with the engine directly 78 | (query-init [this key query config callback]) 79 | (query-destroy [this key]) 80 | 81 | ;; FIXME: one shot query that can't be updated later? 82 | ;; can be done by helper method over init/destroy but engine 83 | ;; would still do a bunch of needless work 84 | ;; only had one case where this might have been useful, maybe it isn't worth adding? 85 | ;; (query-once [this query config callback]) 86 | 87 | ;; returns a promise, tx might need to go async 88 | (transact! [this tx origin])) 89 | 90 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/runtime.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.runtime) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/runtime.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.runtime 2 | (:require 3 | [goog.async.nextTick] 4 | [shadow.experiments.grove.protocols :as gp])) 5 | 6 | ;; code in here is shared between the worker and local runtime 7 | ;; don't put things here that should only be in one runtime 8 | 9 | ;; this is mostly for devtools so they can access the environments 10 | ;; actual code shouldn't use this anywhere 11 | (defonce known-runtimes-ref (atom {})) 12 | 13 | (defn ref? [x] 14 | (and (atom x) 15 | (::rt @x))) 16 | 17 | (defprotocol IScheduleWork 18 | (schedule-work! [this task trigger]) 19 | (unschedule! [this task]) 20 | (run-now! [this action trigger]) 21 | 22 | ;; FIXME: this is purely a UI concern, should most likely be a separate interface 23 | ;; this ns is meant to be usable in a worker environment which is not concerned with suspense 24 | ;; for now suspense is a hack anyways so need to sort that out more 25 | (did-suspend! [this target]) 26 | (did-finish! [this target]) 27 | 28 | ;; need actual scheduler support in browser for these 29 | ;; (run-asap! [this action]) 30 | ;; (run-whenever! [this action]) 31 | ) 32 | 33 | (defn next-tick [callback] 34 | ;; FIXME: should be smarter about when/where to schedule 35 | (js/goog.async.nextTick callback)) 36 | 37 | (deftype RootScheduler [^:mutable update-pending? work-set] 38 | IScheduleWork 39 | (schedule-work! [this work-task trigger] 40 | (.add work-set work-task) 41 | 42 | (when-not update-pending? 43 | (set! update-pending? true) 44 | (next-tick #(.process-work! this)))) 45 | 46 | (unschedule! [this work-task] 47 | (.delete work-set work-task)) 48 | 49 | (did-suspend! [this target]) 50 | (did-finish! [this target]) 51 | 52 | (run-now! [this action trigger] 53 | (set! update-pending? true) 54 | (action) 55 | ;; work must happen immediately since (action) may need the DOM event that triggered it 56 | ;; any delaying the work here may result in additional paint calls (making things slower overall) 57 | ;; if things could have been async the work should have been queued as such and not ended up here 58 | (.process-work! this)) 59 | 60 | Object 61 | (process-work! [this] 62 | (try 63 | (let [iter (.values work-set)] 64 | (loop [] 65 | (let [current (.next iter)] 66 | (when (not ^boolean (.-done current)) 67 | (gp/work! ^not-native (.-value current)) 68 | 69 | ;; should time slice later and only continue work 70 | ;; until a given time budget is consumed 71 | (recur))))) 72 | 73 | (finally 74 | (set! update-pending? false))))) 75 | 76 | ;; FIXME: make this delegate to the above, don't duplicate the code 77 | (deftype TracingRootScheduler [^:mutable update-pending? work-set] 78 | IScheduleWork 79 | (schedule-work! [this work-task trigger] 80 | (.add work-set work-task) 81 | 82 | (when-not update-pending? 83 | (set! update-pending? true) 84 | (next-tick 85 | (fn [] 86 | (js/console.group (str trigger)) 87 | (try 88 | (.process-work! this) 89 | (finally 90 | (js/console.groupEnd))) 91 | )))) 92 | 93 | (unschedule! [this work-task] 94 | (.delete work-set work-task)) 95 | 96 | (did-suspend! [this target]) 97 | (did-finish! [this target]) 98 | 99 | (run-now! [this action trigger] 100 | (js/console.group (str trigger)) 101 | (try 102 | (set! update-pending? true) 103 | (action) 104 | ;; work must happen immediately since (action) may need the DOM event that triggered it 105 | ;; any delaying the work here may result in additional paint calls (making things slower overall) 106 | ;; if things could have been async the work should have been queued as such and not ended up here 107 | (.process-work! this) 108 | 109 | (finally 110 | (js/console.groupEnd)) 111 | )) 112 | 113 | Object 114 | (process-work! [this] 115 | (try 116 | (let [iter (.values work-set)] 117 | (loop [] 118 | (let [current (.next iter)] 119 | (when (not ^boolean (.-done current)) 120 | (gp/work! ^not-native (.-value current)) 121 | 122 | ;; should time slice later and only continue work 123 | ;; until a given time budget is consumed 124 | (recur))))) 125 | 126 | (finally 127 | (set! update-pending? false))))) 128 | 129 | (goog-define TRACE false) 130 | 131 | (defn prepare 132 | ([data-ref runtime-id] 133 | (prepare {} data-ref runtime-id)) 134 | ([init data-ref runtime-id] 135 | (let [root-scheduler 136 | (if ^boolean TRACE 137 | (TracingRootScheduler. false (js/Set.)) 138 | (RootScheduler. false (js/Set.))) 139 | 140 | rt-ref 141 | (-> init 142 | (assoc ::rt true 143 | ::scheduler root-scheduler 144 | ::runtime-id runtime-id 145 | ::data-ref data-ref 146 | ::event-config {} 147 | ::fx-config {} 148 | ::active-queries-map (js/Map.) 149 | ::key-index-seq (atom 0) 150 | ::key-index-ref (atom {}) 151 | ::query-index-map (js/Map.) 152 | ::query-index-ref (atom {}) 153 | ::env-init []) 154 | (atom))] 155 | 156 | (when ^boolean js/goog.DEBUG 157 | (swap! known-runtimes-ref assoc runtime-id rt-ref)) 158 | 159 | rt-ref))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/server.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.server 2 | (:require [clojure.spec.alpha :as s]) 3 | (:import [java.io StringWriter])) 4 | 5 | (s/def ::defc-args 6 | (s/cat 7 | :comp-name simple-symbol? 8 | :docstring (s/? string?) 9 | :opts (s/? map?) 10 | :bindings vector? ;; FIXME: core.specs for destructure help 11 | :hook-bindings vector? 12 | :body (s/* any?))) 13 | 14 | (s/fdef defc :args ::defc-args) 15 | 16 | (defn hook-process [env result] 17 | result) 18 | 19 | (defprotocol ServerRenderable 20 | (html-gen [this env writer])) 21 | 22 | (deftype ServerComponent [render-fn] 23 | ServerRenderable 24 | (html-gen [this env writer] 25 | (let [result (render-fn env)] 26 | (html-gen result env writer)))) 27 | 28 | (extend-protocol ServerRenderable 29 | java.lang.String 30 | (html-gen [this env writer] 31 | ;; FIXME: html encode 32 | (.write writer this)) 33 | 34 | java.lang.Number 35 | (html-gen [this env writer] 36 | (.write writer (str this))) 37 | 38 | nil 39 | (html-gen [this env writer]) 40 | 41 | clojure.lang.PersistentVector 42 | (html-gen [this env writer] 43 | (let [kw (nth this 0) 44 | props (nth this 1) 45 | children (subvec this 2)] 46 | (.write writer (str "<" (name kw))) 47 | (reduce-kv 48 | (fn [_ k v] 49 | ;; FIXME: properly encode k/v 50 | (.write writer (str " " (name k) "=\"" v "\""))) 51 | nil 52 | props) 53 | (.write writer ">") 54 | (run! #(html-gen % env writer) children) 55 | (.write writer (str ""))))) 56 | 57 | (defmacro defc [& args] 58 | (let [{:keys [comp-name bindings hook-bindings opts body]} 59 | (s/conform ::defc-args args) 60 | 61 | env-sym (gensym "env")] 62 | 63 | `(defn ~comp-name ~bindings 64 | (->ServerComponent 65 | (fn [~env-sym] 66 | (let [~@(->> hook-bindings 67 | (partition-all 2) 68 | (mapcat 69 | (fn [[binding init]] 70 | [binding `(hook-process ~env-sym ~init)])))] 71 | ~@body)))))) 72 | 73 | (deftype Fragment [items] 74 | ServerRenderable 75 | (html-gen [this env writer] 76 | (run! #(html-gen % env writer) items))) 77 | 78 | (defn << [& items] 79 | (->Fragment items)) 80 | 81 | (clojure.pprint/pprint 82 | (macroexpand-1 83 | '(defc test-comp [a b] 84 | [c (+ a b)] 85 | :body))) 86 | 87 | (defc nested [thing] 88 | [] 89 | [:div {:class "nested"} thing]) 90 | 91 | (defc dummy [foo bar] 92 | [] 93 | (<< "before" 1 2 3 94 | [:div {:id "some-id"} 95 | [:h1 {} foo] 96 | [:h2 {} bar] 97 | (nested "nested")] 98 | "after")) 99 | 100 | (let [sw (StringWriter.)] 101 | (html-gen (dummy "foo" "bar") {} sw) 102 | (println (str sw))) 103 | 104 | 105 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/timeouts.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.timeouts 2 | (:require 3 | [shadow.experiments.grove :as sg] 4 | [shadow.experiments.grove.runtime :as rt] 5 | [shadow.experiments.grove.events :as ev])) 6 | 7 | ;; FIXME: should this just be available by default? 8 | 9 | (defn init! [rt-ref] 10 | (let [timeouts-ref (atom {})] 11 | 12 | (ev/reg-fx rt-ref :timeout/set 13 | (fn [env {:timeout/keys [id after] :as ev}] 14 | (let [tid (js/setTimeout 15 | (fn [] 16 | (when id 17 | (swap! timeouts-ref dissoc id)) 18 | 19 | (sg/run-tx! rt-ref ev)) 20 | after)] 21 | 22 | (when id 23 | (swap! timeouts-ref assoc id tid))))) 24 | 25 | (ev/reg-fx rt-ref :timeout/clear 26 | (fn [env {:timeout/keys [id]}] 27 | (when-some [tid (get @timeouts-ref id)] 28 | (js/clearTimeout tid) 29 | (swap! timeouts-ref dissoc id) 30 | ))) 31 | 32 | rt-ref)) 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/transit.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.transit 2 | (:require 3 | [cognitect.transit :as transit] 4 | [shadow.experiments.grove.runtime :as rt])) 5 | 6 | ;; FIXME: custom handler config options 7 | 8 | (defn init! [rt-ref] 9 | (let [tr (transit/reader :json) 10 | tw (transit/writer :json) 11 | 12 | transit-read 13 | (fn transit-read [data] 14 | (transit/read tr data)) 15 | 16 | transit-str 17 | (fn transit-str [obj] 18 | (transit/write tw obj))] 19 | 20 | (swap! rt-ref assoc 21 | ::rt/transit-read transit-read 22 | ::rt/transit-str transit-str))) 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/atoms.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.atoms 2 | (:require 3 | [shadow.experiments.grove.components :as comp] 4 | [shadow.experiments.grove.protocols :as gp] 5 | [shadow.experiments.grove.ui.util :as util])) 6 | 7 | (util/assert-not-in-worker!) 8 | 9 | (deftype EnvWatch [key-to-atom path default the-atom ^:mutable val component-handle] 10 | gp/IBuildHook 11 | (hook-build [this ch] 12 | (let [atom (get (gp/get-component-env ch) key-to-atom)] 13 | (when-not atom 14 | (throw (ex-info "no atom found under key" {:key key-to-atom :path path}))) 15 | (EnvWatch. key-to-atom path default atom nil ch))) 16 | 17 | gp/IHook 18 | (hook-init! [this] 19 | (set! val (get-in @the-atom path default)) 20 | (add-watch the-atom this 21 | (fn [_ _ _ new-value] 22 | ;; check immediately and only invalidate if actually changed 23 | ;; avoids kicking off too much work 24 | (let [next-val (get-in new-value path default)] 25 | (when (not= val next-val) 26 | (set! val next-val) 27 | (gp/hook-invalidate! component-handle)))))) 28 | 29 | (hook-ready? [this] true) ;; born ready 30 | (hook-value [this] val) 31 | (hook-update! [this] 32 | ;; only gets here if val actually changed 33 | true) 34 | 35 | (hook-deps-update! [this new-val] 36 | (throw (ex-info "shouldn't have changing deps?" {}))) 37 | (hook-destroy! [this] 38 | (remove-watch the-atom this))) 39 | 40 | (deftype AtomWatch [the-atom access-fn ^:mutable val component-handle] 41 | gp/IBuildHook 42 | (hook-build [this ch] 43 | (AtomWatch. the-atom access-fn nil ch)) 44 | 45 | gp/IHook 46 | (hook-init! [this] 47 | (set! val (access-fn nil @the-atom)) 48 | (add-watch the-atom this 49 | (fn [_ _ old new] 50 | ;; check immediately and only invalidate if actually changed 51 | ;; avoids kicking off too much work 52 | ;; FIXME: maybe shouldn't check equiv? only identical? 53 | ;; pretty likely that something changed after all 54 | (let [next-val (access-fn old new)] 55 | (when (not= val next-val) 56 | (set! val next-val) 57 | (gp/hook-invalidate! component-handle)))))) 58 | 59 | (hook-ready? [this] true) ;; born ready 60 | (hook-value [this] val) 61 | (hook-update! [this] 62 | ;; only gets here if value changed 63 | true) 64 | (hook-deps-update! [this new-val] 65 | ;; FIXME: its ok to change the access-fn 66 | (throw (ex-info "shouldn't have changing deps?" {}))) 67 | (hook-destroy! [this] 68 | (remove-watch the-atom this))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/data.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.data 2 | (:require 3 | [shadow.experiments.grove :as sg :refer (defc <<)])) 4 | 5 | (defn render* [obj] 6 | (cond 7 | (nil? obj) 8 | (<< [:div.text-gray-300 "nil"]) 9 | 10 | (boolean? obj) 11 | (<< [:div (str obj)]) 12 | 13 | (number? obj) 14 | (<< [:div (str obj)]) 15 | 16 | (keyword? obj) 17 | (<< [:div.text-indigo-600.whitespace-nowrap (str obj)]) 18 | 19 | (symbol? obj) 20 | (<< [:div (str obj)]) 21 | 22 | (string? obj) 23 | (<< [:div (pr-str obj)]) 24 | 25 | (vector? obj) 26 | (<< [:div.flex 27 | [:div "["] 28 | [:div 29 | (sg/simple-seq obj 30 | (fn [val idx] 31 | (render* val)))] 32 | [:div.self-end "]"]]) 33 | 34 | (map? obj) 35 | (<< [:div.flex 36 | [:div "{"] 37 | [:div.flex-1 38 | (sg/simple-seq (try (sort (keys obj)) (catch :default e (keys obj))) 39 | (fn [key idx] 40 | (<< [:div {:class (when (pos? idx) "pt-2")} (render* key)] 41 | [:div (render* (get obj key))]) 42 | ))] 43 | [:div.self-end "}"]]) 44 | 45 | :else 46 | (<< [:div (pr-str obj)]))) 47 | 48 | (defn render [obj] 49 | (<< [:div.font-mono.overflow-auto.text-sm 50 | (render* obj)])) 51 | 52 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/grid.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.grid) 2 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/loadable.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.loadable) 2 | 3 | (defmacro refer-lazy 4 | ([lazy-name] 5 | `(refer-lazy ~lazy-name ~(symbol (name lazy-name)))) 6 | ([lazy-name local-name] 7 | `(def ~(with-meta local-name {:tag 'function}) 8 | (wrap-loadable 9 | (shadow.lazy/loadable ~lazy-name))))) -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/loadable.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.loadable 2 | (:require-macros [shadow.experiments.grove.ui.loadable]) 3 | (:require 4 | [shadow.lazy :as lazy] 5 | [shadow.experiments.arborist.protocols :as ap] 6 | [shadow.experiments.arborist.common :as common] 7 | [shadow.experiments.grove.runtime :as rt] 8 | [shadow.experiments.grove.components :as comp] 9 | )) 10 | 11 | ;; FIXME: shadow.lazy is only available with shadow-cljs since it requires compiler support 12 | ;; must not use this namespace directly in the framework elsewhere since that would 13 | ;; make everything shadow-cljs only. not that important but also not necessary to do that. 14 | 15 | (declare LoadableInit) 16 | 17 | (deftype LoadableRoot 18 | [env 19 | scheduler 20 | loadable 21 | marker 22 | ^not-native ^:mutable managed 23 | ^:mutable opts 24 | ^:mutable dom-entered?] 25 | 26 | ap/IManaged 27 | (supports? [this ^LoadableInit next] 28 | (and (instance? LoadableInit next) 29 | (identical? loadable (.-loadable next)))) 30 | 31 | (dom-sync! [this ^LoadableInit next] 32 | (set! opts (.-opts next)) 33 | 34 | (when managed 35 | (let [renderable @loadable 36 | rendered (apply renderable opts)] 37 | (if (ap/supports? managed rendered) 38 | (ap/dom-sync! managed rendered) 39 | (let [new (common/replace-managed env managed rendered)] 40 | (set! managed new) 41 | (when dom-entered? 42 | (ap/dom-entered! new)) 43 | ))))) 44 | 45 | (dom-insert [this parent anchor] 46 | ;; (js/console.log ::dom-insert this) 47 | (.insertBefore parent marker anchor) 48 | (when managed 49 | (ap/dom-insert managed parent anchor))) 50 | 51 | (dom-first [this] 52 | marker) 53 | 54 | (dom-entered! [this] 55 | ;; (js/console.log ::dom-entered! this) 56 | (set! dom-entered? true) 57 | (when managed 58 | (ap/dom-entered! managed))) 59 | 60 | (destroy! [this ^boolean dom-remove?] 61 | ;; (js/console.log ::destroy! this) 62 | (when dom-remove? 63 | (.remove marker)) 64 | (when managed 65 | (ap/destroy! managed dom-remove?))) 66 | 67 | Object 68 | (init! [this] 69 | ;; (js/console.log ::init! (lazy/ready? loadable)) 70 | (if (lazy/ready? loadable) 71 | (.render! this) 72 | (do (rt/did-suspend! scheduler this) 73 | ;; (js/console.log ::did-suspend! this) 74 | (lazy/load loadable 75 | (fn [] 76 | ;; (js/console.log ::loaded this) 77 | (.render! this) 78 | 79 | ;; FIXME: dom-insert should have happened by now but might not be because of suspense 80 | (when-some [parent-el (.-parentElement marker)] 81 | (ap/dom-insert managed parent-el marker) 82 | (when dom-entered? 83 | (ap/dom-entered! managed))) 84 | 85 | (rt/did-finish! scheduler this)) 86 | (fn [err] 87 | (js/console.warn "lazy loading failed" this err) 88 | ))))) 89 | 90 | (render! [this] 91 | ;; (js/console.log ::render! this (lazy/ready? loadable)) 92 | (let [renderable @loadable 93 | rendered (apply renderable opts) 94 | new (ap/as-managed rendered env)] 95 | (set! managed new)))) 96 | 97 | (deftype LoadableInit [loadable opts] 98 | ap/IConstruct 99 | (as-managed [this env] 100 | ;; (js/console.log ::as-managed this env) 101 | (doto (->LoadableRoot env (::comp/scheduler env) loadable (common/dom-marker env) nil opts false) 102 | (.init!)))) 103 | 104 | (defn wrap-loadable [loadable] 105 | (fn [& opts] 106 | ;; this should NOT check if loadable is ready 107 | ;; otherwise may lead to situation where it is not ready at first 108 | ;; and rendering the loadable 109 | ;; then on re-render it would be ready but replace the content 110 | ;; since the new rendered is not compatible with the managed loadable 111 | ;; so the impl takes care of rendering immediately if available 112 | (LoadableInit. loadable opts))) 113 | 114 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/portal.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.portal 2 | (:require 3 | [shadow.experiments.arborist.protocols :as ap] 4 | [shadow.experiments.arborist.common :as common])) 5 | 6 | (declare PortalSeed) 7 | 8 | (deftype PortalNode [env ref-node root] 9 | ap/IManaged 10 | (supports? [this ^PortalSeed next] 11 | (and (instance? PortalSeed next) 12 | (identical? ref-node (.-ref-node next)))) 13 | 14 | (dom-sync! [this ^PortalSeed next] 15 | (ap/update! root (.-body next))) 16 | 17 | (dom-insert [this parent anchor] 18 | (ap/dom-insert root ref-node nil)) 19 | 20 | (dom-first [this] 21 | (ap/dom-first root)) 22 | 23 | (dom-entered! [this] 24 | (ap/dom-entered! root)) 25 | 26 | (destroy! [this ^boolean dom-remove?] 27 | ;; always dom-remove? true since the root is a child of the ref-node not the parent 28 | (ap/destroy! root true))) 29 | 30 | (deftype PortalSeed [ref-node body] 31 | ap/IConstruct 32 | (as-managed [this env] 33 | (PortalNode. 34 | env 35 | ref-node 36 | (doto (common/managed-root env) 37 | (ap/update! body))))) 38 | 39 | (defn portal [ref-node body] 40 | (PortalSeed. ref-node body)) 41 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/testing.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.testing 2 | (:require 3 | [shadow.experiments.grove.protocols :as gp] 4 | [shadow.experiments.grove.components :as comp])) 5 | 6 | (set! *warn-on-infer* false) 7 | 8 | (deftype DelayHook [component-handle max ^:mutable timeout] 9 | gp/IHook 10 | (hook-init! [this] 11 | (let [timeout-ms (rand-int max)] 12 | (set! timeout (js/setTimeout #(.on-timeout! this) timeout-ms)))) 13 | (hook-ready? [this] 14 | (nil? timeout)) 15 | 16 | (hook-value [this] ::timeout) 17 | (hook-deps-update! [this val]) 18 | (hook-update! [this]) 19 | (hook-destroy! [this] 20 | (when timeout 21 | (js/clearTimeout timeout) 22 | (set! timeout nil))) 23 | 24 | Object 25 | (on-timeout! [this] 26 | (set! timeout nil) 27 | (gp/hook-invalidate! component-handle))) 28 | 29 | (deftype DelayInit [max] 30 | gp/IBuildHook 31 | (hook-build [this ch] 32 | (DelayHook. ch max nil))) 33 | 34 | (defn rand-delay [max] 35 | (DelayInit. max)) 36 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/ui/util.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.ui.util 2 | ) 3 | 4 | (defn assert-not-in-worker! [] 5 | (assert js/goog.global.document "this can only be used inside the main parts of your app, not in a worker")) 6 | 7 | ;; FIXME: build should enforce this too 8 | (assert-not-in-worker!) 9 | 10 | (defonce id-seq (atom 0)) 11 | 12 | (defn next-id [] 13 | (swap! id-seq inc)) 14 | 15 | 16 | (def now 17 | (if (exists? js/performance) 18 | #(js/performance.now) 19 | #(js/Date.now))) 20 | 21 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/grove/websocket_engine.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.websocket-engine 2 | (:require 3 | [shadow.experiments.grove.protocols :as gp] 4 | [shadow.experiments.grove.components :as comp] 5 | [cognitect.transit :as transit])) 6 | 7 | (defn send-to-ws [ws ^function transit-str msg] 8 | ;; (js/console.log "worker-write" env msg) 9 | (.send ws (transit-str msg))) 10 | 11 | (deftype Engine 12 | [websocket active-queries-ref transit-str] 13 | gp/IQueryEngine 14 | (query-init [this env query-id query config callback] 15 | (swap! active-queries-ref assoc query-id callback) 16 | (send-to-ws websocket transit-str [:query-init query-id query])) 17 | 18 | (query-destroy [this query-id] 19 | (send-to-ws websocket transit-str [:query-destroy query-id])) 20 | 21 | (transact! [this tx] 22 | (send-to-ws websocket transit-str [:tx tx]))) 23 | 24 | (defn init 25 | ([env ws] 26 | (init env ws ::gp/query-engine)) 27 | ([env websocket engine-key] 28 | (let [tr (transit/reader :json) 29 | tw (transit/writer :json) 30 | 31 | transit-read 32 | (fn transit-read [data] 33 | (transit/read tr data)) 34 | 35 | transit-str 36 | (fn transit-str [obj] 37 | (transit/write tw obj)) 38 | 39 | active-queries-ref 40 | (atom {}) 41 | 42 | env 43 | (assoc env 44 | ::websocket websocket 45 | ::active-queries-ref active-queries-ref 46 | engine-key (->Engine websocket active-queries-ref transit-str) 47 | ::transit-read transit-read 48 | ::transit-str transit-str)] 49 | env))) 50 | 51 | (defn on-open [{::keys [websocket transit-read active-queries-ref] :as env}] 52 | (.addEventListener websocket "message" 53 | (fn [e] 54 | (let [msg (transit-read (.-data e))] 55 | (let [[op & args] msg] 56 | 57 | ;; (js/console.log "main read took" (- t start)) 58 | (case op 59 | :worker-ready 60 | (js/console.log "worker is ready") 61 | 62 | :query-result 63 | (let [[query-id result] args 64 | ^function callback (get @active-queries-ref query-id)] 65 | (when (some? callback) 66 | (callback result))) 67 | 68 | (js/console.warn "unhandled main msg" op msg)))))) 69 | ) -------------------------------------------------------------------------------- /src/main/shadow/experiments/system.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.system) 2 | 3 | (defn -main [& args]) 4 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/system/runtime.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.system.runtime 2 | (:require [clojure.string :as str])) 3 | 4 | (defn- rt-state? [x] 5 | (and (map? x) (map? (::app x)))) 6 | 7 | (defn topo-sort-services 8 | [{:keys [services deps visited] :as state} 9 | service-key] 10 | (let [{:keys [depends-on] :as svc-def} 11 | (get services service-key) 12 | 13 | dep-keys 14 | (->> (vals depends-on) 15 | (map (fn [dep-spec] 16 | (cond 17 | (vector? dep-spec) 18 | (first dep-spec) 19 | (keyword? dep-spec) 20 | dep-spec 21 | :else 22 | (throw (ex-info "invalid :depends-on value" {:dep service-key :entry dep-spec :all svc-def}))))) 23 | (vec))] 24 | 25 | (cond 26 | ;; undefined service dependency is ok, assuming it is provided 27 | (nil? svc-def) 28 | state 29 | 30 | (contains? deps service-key) 31 | (throw (ex-info "service circular dependeny" {:deps deps :name service-key})) 32 | 33 | (contains? visited service-key) 34 | state 35 | 36 | :else 37 | (-> state 38 | (update :visited conj service-key) 39 | (update :deps conj service-key) 40 | (as-> state 41 | (reduce topo-sort-services state dep-keys)) 42 | (update :deps disj service-key) 43 | (update :order conj service-key))))) 44 | 45 | (defn setup [services] 46 | (-> (reduce 47 | topo-sort-services 48 | {:deps #{} 49 | :visited #{} 50 | :order [] 51 | :services services} 52 | (keys services)) 53 | (dissoc :visited :deps))) 54 | 55 | (defn init [state services] 56 | {:pre [(map? state) 57 | (map? services)]} 58 | (assoc state ::app (setup services))) 59 | 60 | ;; stopping 61 | 62 | (defn- stop-service 63 | [{::keys [app] :as state} service-id] 64 | (if-let [service (get state service-id)] 65 | (if-let [{:keys [stop] :as service-def} 66 | (get-in app [:services service-id])] 67 | (do (when stop 68 | (stop service)) 69 | (dissoc state service-id)) 70 | ;; not defined, do nothing 71 | state) 72 | ;; not present, do nothing 73 | state)) 74 | 75 | (defn stop-all 76 | [{::keys [app] :as state}] 77 | {:pre [(rt-state? state)]} 78 | (let [stop-order (reverse (:order app))] 79 | (reduce stop-service state stop-order) 80 | )) 81 | 82 | (defn stop-single 83 | [state service] 84 | {:pre [(rt-state? state)]} 85 | (stop-service state service)) 86 | 87 | (defn stop-many 88 | [state services] 89 | (reduce stop-single state services)) 90 | 91 | ;; starting 92 | 93 | (defn- start-one 94 | [{::keys [app] :as state} service-id] 95 | ;; already present, assume its started 96 | (if (contains? state service-id) 97 | state 98 | ;; lookup up definition, get deps (assumes deps are already started), start 99 | (if-let [{:keys [depends-on start] :as service-def} 100 | (get-in app [:services service-id])] 101 | (let [deps 102 | (reduce-kv 103 | (fn [deps dep-key dep-spec] 104 | (cond 105 | (keyword? dep-spec) 106 | (assoc deps dep-key (get state dep-spec)) 107 | 108 | (vector? dep-spec) 109 | (assoc deps dep-key (get-in state dep-spec)) 110 | 111 | :else 112 | (throw (ex-info "how did this get here?" {})) 113 | )) 114 | {} 115 | depends-on) 116 | 117 | service-instance 118 | (if (empty? deps) 119 | (start) 120 | (start deps))] 121 | 122 | (assoc state service-id service-instance)) 123 | ;; not defined 124 | (throw (ex-info (format "cannot start/find undefined service %s (%s)" service-id (str/join "," (keys state))) {:service service-id :provided (keys state)})) 125 | ))) 126 | 127 | (defn- start-many 128 | "start services and return updated state 129 | will attempt to stop all if one startup fails" 130 | [state services] 131 | {:keys [(rt-state? state)]} 132 | (loop [state 133 | state 134 | 135 | start 136 | services 137 | 138 | started 139 | []] 140 | 141 | (let [service-id (first start)] 142 | (if (nil? service-id) 143 | ;; nothing left to start 144 | state 145 | 146 | ;; start next service 147 | (let [state 148 | (try 149 | (start-one state service-id) 150 | (catch Exception e 151 | ;; FIXME: ignores an exception if a rollback fails 152 | (try 153 | (stop-many state started) 154 | (catch Exception x 155 | (prn [:failed-to-rollback started x]))) 156 | 157 | (throw (ex-info "failed to start service" {:id service-id} e))))] 158 | (recur state (rest start) (conj started service-id))) 159 | )))) 160 | 161 | (defn start-all 162 | "start all services in dependency order, will attempt to properly shutdown if once service fails to start" 163 | [{::keys [app] :as state}] 164 | {:pre [(rt-state? state)]} 165 | (start-many state (:order app))) 166 | 167 | (defn start-single 168 | "start a single service (and its deps)" 169 | [{::keys [app] :as state} service] 170 | {:pre [(rt-state? state)]} 171 | (let [start-order (topo-sort-services app [service])] 172 | (start-many state start-order))) 173 | 174 | (defn start-services 175 | "start a multiple services (and their deps)" 176 | [state services] 177 | {:pre [(rt-state? state)]} 178 | (reduce start-single state services)) 179 | -------------------------------------------------------------------------------- /src/main/shadow/experiments/system_dev.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.system-dev 2 | (:require 3 | [clojure.java.io :as io] 4 | [shadow.experiments.system.runtime :as rt])) 5 | 6 | (defonce instance-ref (atom nil)) 7 | 8 | (defn start [{:system/keys [main] :as config}] 9 | {:pre [(simple-symbol? main)]} 10 | (assert (nil? @instance-ref) "already started?") 11 | 12 | (require main) 13 | 14 | (let [services-sym (symbol (str main) "services") 15 | services-var (resolve services-sym)] 16 | (assert services-var "services var not found") 17 | 18 | (let [app 19 | (-> {:config config} 20 | (rt/init @services-var) 21 | (rt/start-all))] 22 | 23 | (reset! instance-ref app)))) 24 | 25 | (defn -main [config-path & args] 26 | (let [config-file (io/file config-path) 27 | config (read-string (slurp config-file))] 28 | (start config))) 29 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/arborist/components.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @license 3 | * Copyright 2017 Google Inc. All rights reserved. 4 | * 5 | * Licensed under the Apache License, Version 2.0 (the "License"); 6 | * you may not use this file except in compliance with the License. 7 | * You may obtain a copy of the License at 8 | * 9 | * http://www.apache.org/licenses/LICENSE-2.0 10 | * 11 | * Unless required by applicable law or agreed to in writing, software 12 | * distributed under the License is distributed on an "AS IS" BASIS, 13 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 | * See the License for the specific language governing permissions and 15 | * limitations under the License. 16 | */ 17 | 18 | // these were taken from 19 | // https://github.com/webcomponents/custom-elements-everywhere/tree/master/libraries/__shared__/webcomponents/src 20 | 21 | // copied here since to run the same compatibility tests 22 | // https://github.com/webcomponents/custom-elements-everywhere 23 | 24 | // copied here since the recommended project setup is built for JS projects 25 | // and doesn't fit into how shadow-cljs works. didn't want to work with webpack 26 | // just to get these. 27 | 28 | 29 | class CEWithChildren extends HTMLElement { 30 | constructor() { 31 | super(); 32 | this.attachShadow({mode: 'open'}); 33 | this.shadowRoot.innerHTML = ` 34 |

Test h1

35 |
36 |

Test p

37 |
38 | 39 | `; 40 | } 41 | } 42 | 43 | customElements.define('ce-with-children', CEWithChildren); 44 | 45 | class CEWithEvent extends HTMLElement { 46 | constructor() { 47 | super(); 48 | this.addEventListener('click', this.onClick); 49 | } 50 | onClick() { 51 | this.dispatchEvent(new CustomEvent('lowercaseevent')); 52 | this.dispatchEvent(new CustomEvent('kebab-event')); 53 | this.dispatchEvent(new CustomEvent('camelEvent')); 54 | this.dispatchEvent(new CustomEvent('CAPSevent')); 55 | this.dispatchEvent(new CustomEvent('PascalEvent')); 56 | } 57 | } 58 | 59 | customElements.define('ce-with-event', CEWithEvent); 60 | 61 | class CEWithProperties extends HTMLElement { 62 | set bool(value) { 63 | this._bool = value; 64 | } 65 | get bool() { 66 | return this._bool; 67 | } 68 | set num(value) { 69 | this._num = value; 70 | } 71 | get num() { 72 | return this._num; 73 | } 74 | set str(value) { 75 | this._str = value; 76 | } 77 | get str() { 78 | return this._str; 79 | } 80 | set arr(value) { 81 | this._arr = value; 82 | } 83 | get arr() { 84 | return this._arr; 85 | } 86 | set obj(value) { 87 | this._obj = value; 88 | } 89 | get obj() { 90 | return this._obj; 91 | } 92 | } 93 | 94 | customElements.define('ce-with-properties', CEWithProperties); 95 | 96 | class CEWithoutChildren extends HTMLElement { 97 | constructor() { 98 | super(); 99 | } 100 | } 101 | customElements.define('ce-without-children', CEWithoutChildren); -------------------------------------------------------------------------------- /src/test/shadow/experiments/arborist/keyed_seq_test.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.keyed-seq-test 2 | (:require 3 | [cljs.test :as ct :refer (deftest is)] 4 | [shadow.experiments.grove :as sg :refer (<<)] 5 | [shadow.experiments.arborist :as sa])) 6 | 7 | (defn ^:dev/before-load clear-console [] 8 | (js/console.clear)) 9 | 10 | (defn with-test-node [callback] 11 | (let [node (js/document.createElement "div")] 12 | (js/document.body.append node) 13 | (callback node (sa/dom-root node {})) 14 | (.remove node))) 15 | 16 | (defn item [x] 17 | (<< [:span x])) 18 | 19 | (deftest can-swap-item-positions 20 | (with-test-node 21 | (fn [node root] 22 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 23 | (is (= "123" (.-innerText node))) 24 | 25 | (sa/update! root (sg/keyed-seq [3 2 1] identity item)) 26 | (is (= "321" (.-innerText node))) 27 | ))) 28 | 29 | (deftest can-mix-new-and-old 30 | (with-test-node 31 | (fn [node root] 32 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 33 | (is (= "123" (.-innerText node))) 34 | 35 | (sa/update! root (sg/keyed-seq [3 4 1] identity item)) 36 | (is (= "341" (.-innerText node)))))) 37 | 38 | (deftest can-replace-all 39 | (with-test-node 40 | (fn [node root] 41 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 42 | (is (= "123" (.-innerText node))) 43 | 44 | (sa/update! root (sg/keyed-seq [4 5 6] identity item)) 45 | (is (= "456" (.-innerText node))) 46 | ))) 47 | 48 | (deftest can-remove-all 49 | (with-test-node 50 | (fn [node root] 51 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 52 | (is (= "123" (.-innerText node))) 53 | 54 | (sa/update! root (sg/keyed-seq [] identity item)) 55 | (is (= "" (.-innerText node))) 56 | ))) 57 | 58 | (deftest can-add-item-at-start 59 | (with-test-node 60 | (fn [node root] 61 | (let [root (sa/dom-root node {})] 62 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 63 | (is (= "123" (.-innerText node))) 64 | 65 | (sa/update! root (sg/keyed-seq [0 1 2 3] identity item)) 66 | (is (= "0123" (.-innerText node))) 67 | )))) 68 | 69 | (deftest can-replace-with-fewer-items 70 | (with-test-node 71 | (fn [node root] 72 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 73 | (is (= "123" (.-innerText node))) 74 | 75 | (sa/update! root (sg/keyed-seq [4] identity item)) 76 | (is (= "4" (.-innerText node))) 77 | ))) 78 | 79 | (deftest can-replace-with-more-items 80 | (with-test-node 81 | (fn [node root] 82 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 83 | (is (= "123" (.-innerText node))) 84 | 85 | (sa/update! root (sg/keyed-seq [4 5 6 7] identity item)) 86 | (is (= "4567" (.-innerText node))) 87 | ))) 88 | 89 | (deftest can-add-items-at-end 90 | (with-test-node 91 | (fn [node root] 92 | (sa/update! root (sg/keyed-seq [1 2 3] identity item)) 93 | (is (= "123" (.-innerText node))) 94 | 95 | (sa/update! root (sg/keyed-seq [1 2 3 4] identity item)) 96 | (is (= "1234" (.-innerText node)))))) 97 | 98 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/arborist/wc_test.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist.wc-test 2 | (:require 3 | [goog.object :as gobj] 4 | [cljs.test :as ct :refer (deftest is)] 5 | [clojure.string :as str] 6 | [shadow.experiments.grove :as sg :refer (<<)] 7 | [shadow.experiments.arborist :as sa] 8 | [shadow.experiments.arborist.protocols :as ap] 9 | [shadow.experiments.arborist.attributes :as attr] 10 | ["./components.js"])) 11 | 12 | ;; some tests to check web component compatibility, similar to 13 | ;; https://github.com/webcomponents/custom-elements-everywhere 14 | ;; just written in CLJS 15 | 16 | ;; similar in structure to 17 | ;; https://github.com/webcomponents/custom-elements-everywhere/blob/master/libraries/preact/src/basic-tests.js 18 | ;; https://github.com/webcomponents/custom-elements-everywhere/blob/master/libraries/preact/src/advanced-tests.js 19 | 20 | (defn ^:dev/before-load clear-console [] 21 | (js/console.clear)) 22 | 23 | (defn with-test-node 24 | ([callback] 25 | (with-test-node {} callback)) 26 | ([env callback] 27 | (let [node (js/document.createElement "div")] 28 | (js/document.body.append node) 29 | (callback node (sa/dom-root node env)) 30 | (.remove node)))) 31 | 32 | 33 | ;; FIXME: should these all use :dom/ref instead of looking at lastChild? 34 | 35 | (deftest can-mount-without-children 36 | (with-test-node 37 | (fn [node root] 38 | (sa/update! root (<< [:ce-without-children])) 39 | (let [wc (.-lastChild node)] 40 | (is wc) 41 | )))) 42 | 43 | (defn check-children [^js wc] 44 | (let [shadow-root (.-shadowRoot wc) 45 | h1 (.querySelector shadow-root "h1") 46 | p (.querySelector shadow-root "p")] 47 | 48 | (is h1) 49 | (is (= "Test h1" (.-textContent h1))) 50 | 51 | (is p) 52 | (is (= "Test p" (.-textContent p))))) 53 | 54 | (deftest can-mount-with-children 55 | (with-test-node 56 | (fn [node root] 57 | (sa/update! root (<< [:ce-with-children])) 58 | (let [wc (.-lastChild node)] 59 | (is wc) 60 | (check-children wc) 61 | )))) 62 | 63 | (deftest can-mount-with-children-and-slot 64 | (with-test-node 65 | (fn [node root] 66 | (sa/update! root (<< [:ce-with-children 1])) 67 | (let [wc (.-lastChild node)] 68 | (is wc) 69 | (check-children wc) 70 | 71 | ;; textContent doesn't include shadowRoot 72 | (is (= "1" (.-textContent wc)))) 73 | 74 | (sa/update! root (<< [:ce-with-children 2])) 75 | (let [wc (.-lastChild node)] 76 | (is wc) 77 | (check-children wc) 78 | 79 | ;; textContent doesn't include shadowRoot 80 | (is (= "2" (.-textContent wc))) 81 | )))) 82 | 83 | (deftest can-replace-with-regular-node 84 | (with-test-node 85 | (fn [node root] 86 | (sa/update! root (<< [:ce-with-children])) 87 | (let [wc (.-lastChild node)] 88 | (is wc) 89 | (check-children wc)) 90 | 91 | ;; this test seems kinda pointless, pretty fundamental to replace things 92 | ;; nothing special regarding custom elements in this whatsoever 93 | (sa/update! root (<< [:div "Dummy view"])) 94 | 95 | (let [div (.-lastChild node)] 96 | (is div) 97 | (is (nil? (.-shadowRoot div))) 98 | ;; textContent doesn't include shadowRoot 99 | (is (= "Dummy view" (.-textContent div))) 100 | )))) 101 | 102 | (deftest can-pass-properties 103 | (with-test-node 104 | (fn [node root] 105 | (sa/update! root 106 | (<< [:ce-with-properties 107 | {:bool true 108 | :num 42 109 | :str "shadow" 110 | ;; lol who wants arrays or objects 111 | :arr [1 2 3] 112 | :obj {:foo "bar"}}])) 113 | 114 | (let [^js wc (.-lastChild node)] 115 | (is wc) 116 | 117 | ;; https://github.com/webcomponents/custom-elements-everywhere/blob/83c186bffd3987cfd3442ba4dcfbc104b19f6614/libraries/preact/src/basic-tests.js#L104-L105 118 | ;; why is it ok for tests if these just work through .getAttribute? 119 | ;; seems like a pretty significant problem since .getAttribute/.setAttribute 120 | ;; turns vals into strings? maybe some old outdated stuff? maybe some spec fallback? 121 | (is (true? (.-bool wc))) 122 | (is (identical? 42 (.-num wc))) 123 | (is (identical? "shadow" (.-str wc))) 124 | (is (= [1 2 3] (.-arr wc))) 125 | (is (= {:foo "bar"} (.-obj wc))) 126 | )))) 127 | 128 | (deftest can-handle-events 129 | (let [events-ref (atom #{})] 130 | 131 | (with-test-node 132 | ;; low level custom event handler, would usually use component 133 | ;; but we are testing fragments here not components 134 | {::ap/dom-event-handler 135 | (reify 136 | ap/IHandleDOMEvents 137 | (validate-dom-event-value! [this env event value] 138 | (assert (true? value))) 139 | 140 | (handle-dom-event! [this event-env event value dom-event] 141 | (swap! events-ref conj event)))} 142 | 143 | (fn [node root] 144 | (sa/update! root 145 | (<< [:ce-with-event 146 | {:on-lowercaseevent true 147 | :on-kebab-event true 148 | :on-camelEvent true 149 | :on-CAPSevent true 150 | :on-PascalEvent true}])) 151 | 152 | (let [^js wc (.-lastChild node)] 153 | (is wc) 154 | 155 | (.click wc) 156 | 157 | (is (= #{"lowercaseevent" "kebab-event" "camelEvent" "CAPSevent" "PascalEvent"} 158 | @events-ref)) 159 | ))))) -------------------------------------------------------------------------------- /src/test/shadow/experiments/arborist_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.arborist-test 2 | (:require 3 | [clojure.test :as t :refer (deftest is)] 4 | [clojure.pprint :refer (pprint)] 5 | [clojure.string :as str] 6 | [shadow.experiments.arborist.fragments :as frag])) 7 | 8 | (def test-body 9 | '[[:foo {:i/key :key :bar 1 :foo foo :x nil :bool true}] 10 | "hello" 11 | [:div#id 12 | (dynamic-thing {:x 1}) 13 | (if (even? 1) 14 | (<< [:h1 "even"]) 15 | (<< [:h2 "odd"])) 16 | [:h1 "hello, " title ", " foo] 17 | (let [x 1] 18 | (<< [:h2 x]))] 19 | 1 20 | (some-fn 1 2)]) 21 | 22 | (def test-body 23 | #_'[[:div.card {:style {:color "red"} :foo ["xyz" "foo" "bar"] :attr toggle} 24 | [:div.card-header title] 25 | [:div.card-body {:on-click ^:once [::foo {:bar yo}] :attr "foo"} "Hello"]]] 26 | 27 | '[[:div.card 28 | [:div.card-title title] 29 | [:div.card-body {:foo "bar"} body] 30 | [bar bar-child-can-change] 31 | [foo {:bar 1} 32 | [:foo.child "foo.child"]] 33 | [:div.card-actions 34 | [:button "ok"]]]] 35 | #_[[:div x] 36 | [:> component {:foo "bar"} [:c1 [:c2 {:x x}] y] [:c3]]]) 37 | 38 | 39 | (deftest test-macro-expand 40 | (pprint (frag/make-fragment {} test-body))) 41 | 42 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/bench_db.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.bench-db 2 | (:require 3 | ["benchmark" :as b] 4 | )) 5 | 6 | (defn log-cycle [event] 7 | (println (.toString (.-target event)))) 8 | 9 | (defn log-complete [event] 10 | (this-as this 11 | (js/console.log this))) 12 | 13 | (defn db-flat-read [db idents] 14 | (->> idents 15 | (map #(get db %)) 16 | (into []))) 17 | 18 | (defn db-flat-write [db idents] 19 | (let [val (random-uuid)] 20 | (reduce 21 | (fn [db ident] 22 | (update db ident assoc ::val val)) 23 | db 24 | idents))) 25 | 26 | (defn db-nested-read [db idents] 27 | (->> idents 28 | (map #(get-in db %)) 29 | (into []))) 30 | 31 | (defn db-nested-write [db idents] 32 | (let [val (random-uuid)] 33 | (reduce 34 | (fn [db ident] 35 | (update-in db ident assoc ::val val)) 36 | db 37 | idents))) 38 | 39 | (defn gen-idents [num-types num-items] 40 | (->> (for [key-id (range num-types) 41 | val-id (range num-items)] 42 | [(keyword (str "key" key-id)) val-id]) 43 | (shuffle) 44 | (vec))) 45 | 46 | (defn gen-flat-db [idents] 47 | (reduce 48 | (fn [db ident] 49 | (assoc db ident {::ident ident})) 50 | {} 51 | idents)) 52 | 53 | (defn gen-nested-db [idents] 54 | (reduce 55 | (fn [db ident] 56 | (assoc-in db ident {::ident ident})) 57 | {} 58 | idents)) 59 | 60 | ;; flat wins at pretty much any size 61 | (defn main [& args] 62 | (let [idents 63 | (gen-idents 5 1000) 64 | 65 | flat-db 66 | (gen-flat-db idents) 67 | 68 | nested-db 69 | (gen-nested-db idents) 70 | 71 | read-idents 72 | (->> idents 73 | (shuffle) 74 | (take 50) 75 | (vec)) 76 | 77 | get-ident 78 | (first idents)] 79 | 80 | (prn [:nested (count nested-db)]) 81 | (prn [:flat (count flat-db)]) 82 | 83 | (-> (b/Suite.) 84 | (.add "db-flat-get" #(get flat-db get-ident)) 85 | (.add "db-nested-get" #(get-in nested-db get-ident)) 86 | 87 | (.add "db-flat-read" #(db-flat-read flat-db read-idents)) 88 | (.add "db-nested-read" #(db-nested-read nested-db read-idents)) 89 | 90 | (.add "db-flat-write" #(db-flat-write flat-db read-idents)) 91 | (.add "db-nested-write" #(db-nested-write nested-db read-idents)) 92 | (.on "cycle" log-cycle) 93 | (.run)))) 94 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/bench_fragment.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.bench-fragment 2 | (:require 3 | ["benchmark" :as b] 4 | ["react" :as react :rename {createElement $}] 5 | ["react-dom" :as rdom] 6 | [reagent.core :as reagent] 7 | [goog.reflect :as gr] 8 | [shadow.experiments.arborist :as sa] 9 | [shadow.experiments.arborist.interpreted] 10 | [shadow.experiments.arborist.protocols :as sap])) 11 | 12 | ;; browser version seems to rely on Benchmark global for some reason 13 | (set! js/Benchmark b) 14 | 15 | (defn log-cycle [event] 16 | (println (.toString (.-target event)))) 17 | 18 | (defn log-complete [event] 19 | (this-as this 20 | (js/console.log this))) 21 | 22 | (defn fragment-optimized [v] 23 | (sa/fragment 24 | [:div.card 25 | [:div.card-title "title"] 26 | [:div.card-body v] 27 | (when v 28 | (sa/fragment 29 | [:div.card-footer 30 | [:div.card-actions 31 | [:button "ok" v] 32 | [:button "cancel"]]]))])) 33 | 34 | (defn fragment-fallback [v] 35 | (sa/fragment-fallback 36 | [:div.card 37 | [:div.card-title "title"] 38 | [:div.card-body v] 39 | (when v 40 | (sa/fragment-fallback 41 | [:div.card-footer 42 | [:div.card-actions 43 | [:button "ok" v] 44 | [:button "cancel"]]])) 45 | ])) 46 | 47 | (defn hiccup [v] 48 | [:div.card 49 | [:div.card-title "title"] 50 | [:div.card-body v] 51 | (when v 52 | [:div.card-footer 53 | [:div.card-actions 54 | [:button "ok" v] 55 | [:button "cancel"]]])]) 56 | 57 | (defn react-element [v] 58 | ($ "div" #js {:className "card"} 59 | ($ "div" #js {:className "card-title"} "title") 60 | ($ "div" #js {:className "card-body"} v) 61 | (when v 62 | ($ "div" #js {:className "card-footer"} 63 | ($ "div" #js {:className "card-actions"} 64 | ($ "button" nil "ok" v) 65 | ($ "button" nil "cancel")))))) 66 | 67 | (defn start [] 68 | (let [m-optimized 69 | (sap/as-managed (fragment-optimized (str "dont-inline-this: " (rand))) {}) 70 | 71 | m-fallback 72 | (sap/as-managed (fragment-fallback (str "dont-inline-this: " (rand))) {}) 73 | 74 | m-hiccup 75 | (sap/as-managed (hiccup (str "dont-inline-this: " (rand))) {}) 76 | 77 | react-root 78 | (js/document.createElement "div") 79 | 80 | reagent-root 81 | (js/document.createElement "div")] 82 | 83 | (sap/dom-insert m-optimized js/document.body nil) 84 | (sap/dom-insert m-fallback js/document.body nil) 85 | (sap/dom-insert m-hiccup js/document.body nil) 86 | 87 | (js/document.body.appendChild react-root) 88 | (js/document.body.appendChild reagent-root) 89 | 90 | (rdom/render (react-element (rand)) react-root) 91 | (reagent/render (hiccup (rand)) reagent-root) 92 | 93 | (-> (b/Suite.) 94 | ;; just fragment 95 | (.add "fragment-optimized" #(fragment-optimized (rand))) 96 | ;; construct dom 97 | (.add "managed-optimized" #(sap/as-managed (fragment-optimized (rand)) {})) 98 | ;; update dom 99 | (.add "update-optimized" #(sap/dom-sync! m-optimized (fragment-optimized (rand)))) 100 | 101 | (.add "fragment-fallback" #(fragment-fallback (rand))) 102 | (.add "managed-fallback" #(sap/as-managed (fragment-fallback (rand)) {})) 103 | (.add "update-fallback" #(sap/dom-sync! m-fallback (fragment-fallback (rand)))) 104 | 105 | (.add "hiccup" #(hiccup (rand))) 106 | (.add "managed-hiccup" #(sap/as-managed (hiccup (rand)) {})) 107 | (.add "update-hiccup" #(sap/dom-sync! m-hiccup (hiccup (rand)))) 108 | (.add "reagent" #(reagent/render (hiccup (rand)) reagent-root)) 109 | 110 | (.add "react-element" #(react-element (rand))) 111 | ;; can't test create-only since rdom requires the element to be in the dom 112 | (.add "react-dom" #(rdom/render (react-element (rand)) react-root)) 113 | 114 | (.on "cycle" log-cycle) 115 | (.run #js {:async true})))) 116 | 117 | ;; node 118 | (defn main [] 119 | (start)) 120 | 121 | ;; browser 122 | (defn init [] 123 | (js/setTimeout start 1000)) 124 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/builder_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.builder-test 2 | (:require 3 | [clojure.test :as t :refer (deftest is)] 4 | [clojure.pprint :refer (pprint)] 5 | [shadow.experiments.grove.builder :as b])) 6 | 7 | (def test-body 8 | '[[:foo {:i/key :key :bar 1 :foo foo :x nil :bool true}] 9 | "hello" 10 | [:div#id 11 | (dynamic-thing {:x 1}) 12 | (if (even? 1) 13 | (<< [:h1 "even"]) 14 | (<< [:h2 "odd"])) 15 | [:h1 "hello, " title] 16 | (let [x 1] 17 | (<< [:h2 x]))] 18 | 1 19 | (some-fn 1 2)]) 20 | 21 | (deftest test-macro-expand 22 | (pprint (b/compile 23 | {:skip-check false} 24 | {:line 1 :column 1 :ns {:name 'cljs}} 25 | test-body))) 26 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/collections_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.collections-test 2 | (:require [clojure.test :as ct :refer (deftest is)])) 3 | 4 | ;; original collection vector of items 5 | ;; vector or array of key-fn(item) 6 | 7 | (def old [1 2 3 4 5]) 8 | (def new [1 2 4 5]) ;; remove 3 in middle 9 | 10 | (deftest dummy 11 | "foo") -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/db_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.db-test 2 | (:require 3 | [shadow.experiments.grove.db :as db] 4 | [clojure.pprint :refer (pprint)] 5 | [clojure.test :as t :refer (deftest is)])) 6 | 7 | (def schema 8 | (db/configure 9 | {:a 10 | {:type :entity 11 | :attrs {:a-id [:primary-key number?] 12 | :many [:many :b] 13 | :single [:one :b]}} 14 | 15 | :b 16 | {:type :entity 17 | :attrs {:b-id [:primary-key number?] 18 | :c :c}} 19 | 20 | :c 21 | {:type :entity 22 | :attrs {:c-id [:primary-key number?]}}})) 23 | 24 | (def sample-data 25 | [{:a-id 1 26 | :a-value "a" 27 | :many [{:b-id 1 28 | :b-value "b" 29 | :c {:c-id 1 :c true}} 30 | {:b-id 2 31 | :b-value "c"}] 32 | :single {:b-id 1 33 | :b-value "x"}}]) 34 | 35 | (deftest building-a-db-normalizer 36 | (let [before 37 | (with-meta 38 | {:foo "bar"} 39 | {::db/schema schema}) 40 | 41 | after 42 | (db/merge-seq before :a sample-data [::foo])] 43 | 44 | (pprint after) 45 | 46 | 47 | (pprint (db/query {} after [{:a [:a-value]}])) 48 | )) 49 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/forms_test.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.forms-test 2 | (:require 3 | [cljs.test :as ct :refer (deftest is)] 4 | [shadow.experiments.grove :as sg :refer (defc <<)])) 5 | 6 | (comment 7 | (def form 8 | (-> (form-describe) 9 | (form-add-field :name :string {}) 10 | (form-add-field :flag :boolean {}) 11 | (form-add-field :extra :string {}))) 12 | 13 | ;; forms are boring, nobody wants to code them properly 14 | ;; but a proper form makes all the difference and is one of the most important aspects for user-driven input 15 | ;; doing them properly with all the event wiring and aria-* attributes is very difficult 16 | 17 | ;; can already do everything needed in the "old" style just using an atom 18 | ;; but I want some kind of abstraction that makes it easier to do correct forms 19 | 20 | ;; might be out of scope given how complex it is though 21 | 22 | (defc my-page [] 23 | (bind loaded-data 24 | (sg/query [::my-data])) 25 | 26 | (bind form 27 | ;; must be able to initialize form with data loaded from other sources 28 | ;; if this data changes the form must recognize those changes 29 | ;; it must not dismiss those changes but it must also not mess with data 30 | ;; the user might currently be editing 31 | (form/init form loaded-data)) 32 | 33 | 34 | (render 35 | 36 | (<< [:div 37 | ;; need helpers for other :aria-* related things on labels 38 | ;; directly setting attributes manually is tedious and error prone 39 | ;; footgun potential for conflicting :class etc. 40 | [:label {:form-label-attrs [form :name]} "Name"] 41 | 42 | ;; this would suck because there are a lot of extra attributes you'd need to manage 43 | ;; changing :class depending on field state empty/clean/dirty/invalid/valid 44 | ;; adding :id and other :aria-* related things 45 | ;; somehow needs to be wired up to form, can't rely on "magic" later 46 | [:input {:type "text"}] 47 | 48 | ;; going with the attribute path exposes many footguns where people start 49 | ;; adding custom :on-change or whatever events 50 | ;; conflicting :class/:id handling 51 | ;; must still be possible though if people really want custom things 52 | [:input {:input-attrs [form :name] :class "foo"}] 53 | 54 | ;; premade form fields that generate dom elements, managing all attributes 55 | (form-input form :name {:data-foo "extra-attr"}) 56 | 57 | ;; form errors/validation problems may want to add extra DOM elements 58 | (when (form-has-error? form :name) 59 | (<< [:div "Invalid Name."]))] 60 | 61 | ;; or the above just wrapped in a simple reusable helper 62 | (form-simple-input-with-label form :name "Name") 63 | 64 | ;; some kind of logic that just renders the entire form 65 | ;; when the dev don't care much about how it looks 66 | ;; forms are ultimately boring in most cases (eg. admin stuff) 67 | (form-simple form) 68 | 69 | ;; fields will have widely different DOM structures 70 | ;; must be flexible enough to allow this 71 | [:div 72 | [:label {:form-label-attrs [form :flag]} 73 | (form-checkbox form :flag) 74 | "Optional?"]] 75 | 76 | ;; must be possible to access current form state easily 77 | ;; to hide/show optional/contextual fields 78 | (when (form-get-value form :flag) 79 | (<< [:div 80 | [:label {:form-label-attrs [form :extra]} "Extra Field"] 81 | (form-input form :extra)])))))) 82 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/html_test.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.html-test 2 | (:require 3 | [clojure.test :refer (deftest is)] 4 | [clojure.pprint :refer (pprint)] 5 | [shadow.experiments.grove.html :as html :refer (<<)])) 6 | 7 | (defn dynamic-thing [thing] thing) 8 | 9 | (deftest test-write-str 10 | (let [foo "fo\"XSS\"o" 11 | title "world" 12 | some-fn (fn [& body] body) 13 | test-fn 14 | (fn [attrs] 15 | (<< [:foo {:i/key 1 :bar 1 :foo foo :x nil :bool true}] 16 | "hello" 17 | [:div#id.xxx (dynamic-thing {:foo "bar"}) 18 | [:h1 "hello, " title] 19 | attrs 20 | (when (:nested attrs) 21 | (<< [:h2 "nested fragment"] 22 | "foo" 23 | [:still "nested"]))] 24 | 1 25 | (some-fn 1 2)))] 26 | 27 | (println (test-fn {:yo "attrs"})) 28 | (println) 29 | (println (test-fn {:nested true})) 30 | 31 | (println (html/str [:div#hello.world "yo"])) 32 | )) 33 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/others/builder.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.others.builder 2 | (:require-macros [shadow.experiments.grove.builder]) 3 | (:require 4 | [shadow.experiments.grove.protocols :as p])) 5 | 6 | (def ^:dynamic ^not-native *instance* nil) 7 | 8 | (defn check-instance! [] 9 | (when-not *instance* 10 | (throw (ex-info "no shadow.experiments.grove.builder/*instance* set!" {})))) 11 | 12 | (defn fragment-start [fragment-id node-count] 13 | (p/fragment-start *instance* fragment-id node-count)) 14 | 15 | (defn fragment-end [] 16 | (p/fragment-end *instance*)) 17 | 18 | (defn element-open [type akeys avals attr-offset specials] 19 | (p/element-open *instance* type akeys avals attr-offset specials)) 20 | 21 | (defn element-close [] 22 | (p/element-close *instance*)) 23 | 24 | (defn text [content] 25 | (p/text *instance* content)) 26 | 27 | (defn interpret [thing] 28 | (p/interpret *instance* thing)) -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/others/html.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.others.html 2 | (:refer-clojure :exclude (str)) 3 | (:require 4 | [shadow.experiments.grove.builder :as build] 5 | [shadow.experiments.grove.protocols :as p]) 6 | (:import [java.io Writer StringWriter] 7 | [clojure.lang IDeref] 8 | com.google.common.html.HtmlEscapers)) 9 | 10 | (set! *warn-on-reflection* true) 11 | 12 | (defrecord Element [tag parent]) 13 | 14 | (defn ^String escape-val [thing] 15 | (-> (HtmlEscapers/htmlEscaper) 16 | (.escape (clojure.core/str thing)))) 17 | 18 | (deftype SafeString [actual] 19 | Object 20 | (toString [_] 21 | actual)) 22 | 23 | (defmethod print-method SafeString [^SafeString x ^Writer w] 24 | (.write w (.toString x))) 25 | 26 | (deftype HTMLWriter 27 | [^StringWriter sw 28 | ^:unsynchronized-mutable element] 29 | 30 | IDeref 31 | (deref [_] element) 32 | 33 | p/IBuildTrees 34 | (fragment-start [this fragment-id node-count] 35 | (when element 36 | (throw (ex-info "invalid state, shouldn't have an element" {})))) 37 | 38 | (fragment-end [this] 39 | (SafeString. (.toString sw))) 40 | 41 | ;; 42 | (element-open [_ tag akeys avals sc specials] 43 | (let [el (Element. tag element) 44 | c (count akeys)] 45 | 46 | (set! element el) 47 | 48 | (.write sw "<") 49 | (.write sw (name tag)) 50 | 51 | (dotimes [i c] 52 | (let [key (nth akeys i) 53 | val (nth avals i)] 54 | (when val 55 | (.write sw " ") 56 | (.write sw (name key)) 57 | (when-not (true? val) 58 | (.write sw "=\"") 59 | (if (< i sc) 60 | ;; static attributes can be emitted as is since it is unlikely we will XSS ourselves 61 | (.write sw (clojure.core/str val)) 62 | ;; dynamic attrs must be escaped properly 63 | (.write sw (escape-val val))) 64 | (.write sw "\""))))) 65 | (.write sw ">"))) 66 | 67 | ;; 68 | (element-close [_] 69 | (.write sw " element :tag name)) 71 | (.write sw ">") 72 | (set! element (:parent element))) 73 | 74 | ;; only text is passed through as is 75 | (text [_ val] 76 | (.write sw (clojure.core/str val))) 77 | 78 | ;; everything else must be escaped 79 | (interpret [_ val] 80 | (when (some? val) 81 | (if (instance? SafeString val) 82 | (.write sw (clojure.core/str val)) 83 | (.write sw (escape-val val)))))) 84 | 85 | (defn new-fragment [] 86 | (HTMLWriter. (StringWriter.) nil)) 87 | 88 | (defmacro << [& body] 89 | `(build/with (new-fragment) 90 | ~(build/compile {} &env body))) 91 | 92 | (defmacro str [& body] 93 | `(build/with (new-fragment) 94 | ~(build/compile {} &env body))) -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/others/html.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.others.html 2 | (:require-macros [shadow.experiments.grove.html]) 3 | (:refer-clojure :exclude (str)) 4 | (:require 5 | [shadow.experiments.grove.builder :as build] 6 | [shadow.experiments.grove.protocols :as p] 7 | [goog.string :as gstr])) 8 | 9 | (defrecord Element [tag parent]) 10 | 11 | (defn ^String escape-val [thing] 12 | (gstr/htmlEscape thing)) 13 | 14 | (deftype SafeString [actual] 15 | Object 16 | (toString [_] 17 | actual)) 18 | 19 | (deftype HTMLWriter 20 | [^js sb 21 | ^:mutable element] 22 | 23 | p/IBuildTrees 24 | (fragment-start [this fragment-id node-count] 25 | (when element 26 | (throw (ex-info "invalid state, shouldn't have an element" {})))) 27 | 28 | (fragment-end [this] 29 | (SafeString. (.join sb ""))) 30 | 31 | ;; 32 | (element-open [_ ^not-native tag akeys avals sc specials] 33 | (let [el (Element. tag element) 34 | c (alength akeys)] 35 | 36 | (set! element el) 37 | 38 | (.push sb "<") 39 | (.push sb (-name tag)) 40 | 41 | (dotimes [i c] 42 | (let [^not-native key (aget akeys i) 43 | val (aget avals i)] 44 | (when val 45 | (.push sb " ") 46 | (.push sb (-name key)) 47 | (when-not (true? val) 48 | (.push sb "=\"") 49 | (if (< i sc) 50 | ;; static attributes can be emitted as is since it is unlikely we will XSS ourselves 51 | (.push sb (cljs.core/str val)) 52 | ;; dynamic attrs must be escaped properly 53 | (.push sb (escape-val val))) 54 | (.push sb "\""))))) 55 | (.push sb ">"))) 56 | 57 | ;; 58 | (element-close [_] 59 | (.push sb " element :tag name)) 61 | (.push sb ">") 62 | (set! element (:parent element))) 63 | 64 | ;; only text is passed through as is 65 | (text [_ val] 66 | (.push sb val)) 67 | 68 | ;; everything else must be escaped 69 | (interpret [_ val] 70 | (when (some? val) 71 | (if (instance? SafeString val) 72 | (.push sb (.-actual val)) 73 | (.push sb (escape-val val)))))) 74 | 75 | (defn new-fragment [] 76 | (HTMLWriter. (array) nil)) 77 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/others/protocols.cljc: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.others.protocols) 2 | 3 | ;; protocol is used in both CLJ and CLJS variants 4 | ;; just moved this in to this .cljc file to avoid repeating it 5 | ;; builder is not .cljc because I don't like working in files with lots of conditionals 6 | 7 | (defprotocol IBuildTrees 8 | (fragment-start [this fragment-id node-count]) 9 | (fragment-end [this]) 10 | 11 | (element-open [this tag akeys avals abits specials]) 12 | (element-close [this]) 13 | (text [this val]) 14 | 15 | (interpret [this val])) 16 | 17 | (defrecord Ident [entity-type id]) 18 | -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/others/react.clj: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.others.react 2 | (:require [shadow.experiments.grove.builder :as build]) 3 | (:import [cljs.tagged_literals JSValue])) 4 | 5 | (defmacro << [& body] 6 | `(build/with (shadow.experiments.grove.react/ElementBuilder. nil ~(not= 1 (count body))) 7 | ~(build/compile {:skip-check true} &env body))) 8 | 9 | ;; only a macro to optimize the props handling (avoid allocating clojure maps) 10 | ;; FIXME: should probably validate that attrs are only simple keywords 11 | (defmacro js 12 | ([type {:keys [ref key] :as attrs}] 13 | `(js-el* 14 | ~type 15 | ~(-> attrs 16 | (dissoc :key :ref) 17 | (JSValue.)) 18 | ~key 19 | ~ref)) 20 | ([type {:keys [ref key] :as attrs} child-expr] 21 | (when (vector? child-expr) 22 | (throw (ex-info "invalid child-expr, elements need to be wrapped in $" {:child-expr child-expr}))) 23 | 24 | `(shadow.experiments.grove.react/js-el* 25 | ~type 26 | ~(-> attrs 27 | (dissoc :key :ref) 28 | (assoc :children `(shadow.experiments.grove.react/unwrap-fragment ~child-expr)) 29 | (JSValue.)) 30 | ~key 31 | ~ref))) -------------------------------------------------------------------------------- /src/test/shadow/experiments/grove/react/dump.cljs: -------------------------------------------------------------------------------- 1 | (ns shadow.experiments.grove.react.dump 2 | (:require 3 | [shadow.experiments.grove.react :as r :refer (<<)])) 4 | 5 | ;; FIXME: make these cool ... 6 | (def map-renderer 7 | {:component-id ::map-renderer 8 | :render 9 | (fn [{:keys [value] :as props}] 10 | (let [entries (sort-by first value)] 11 | (<< [:div.edn-dump.text-left 12 | [:table.edn-map 13 | [:caption.font-bold 14 | (pr-str (type value)) 15 | " size: " 16 | (count value)] 17 | [:tbody 18 | (r/render-seq entries first 19 | (fn [[key val]] 20 | (<< [:tr 21 | [:td.edn-mkey (r/as-react-element key)] 22 | [:td.edn-mval (r/as-react-element val)]])))]]])))}) 23 | 24 | (def vec-renderer 25 | {:component-id ::map-renderer 26 | :render 27 | (fn [{:keys [value] :as props}] 28 | (<< [:div "DUMP: " (pr-str value)]))}) 29 | 30 | (extend-protocol r/IConvertToReact 31 | cljs.core/PersistentArrayMap 32 | (as-react-element [m] 33 | (r/render map-renderer {:value m})) 34 | 35 | cljs.core/PersistentHashMap 36 | (as-react-element [m] 37 | (r/render map-renderer {:value m})) 38 | 39 | cljs.core/PersistentVector 40 | (as-react-element [m] 41 | (r/render vec-renderer {:value m})) 42 | 43 | cljs.core/Keyword 44 | (as-react-element [m] 45 | (<< [:span.edn-keyword (str m)])) 46 | 47 | number 48 | (as-react-element [m] 49 | (<< [:span.edn-number (str m)])) 50 | 51 | string 52 | (as-react-element [m] 53 | (<< [:span.edn-string m])) 54 | 55 | nil 56 | (as-react-element [m] 57 | (<< "nil")) 58 | 59 | default 60 | (as-react-element [m] 61 | (<< (pr-str m))) 62 | ) 63 | 64 | --------------------------------------------------------------------------------