Fork me on GitHub
#clojurescript
<
2023-02-28
>
mesota09:02:20

Hello clojurians, since re-com uses bootstrap, will using it in a re-frame project that uses another css framework, say tailwind or bulma, create any issues?

p-himik09:02:53

Depends on whether or not they have intersecting class names.

p-himik09:02:11

And it will invariably increase the total bundle size.

kennytilton21:02:24

So I am getting ready for an official release of my long time personal CLJS web framework, and I would be grateful for the reaction of the uninitiated to this write-up. https://github.com/kennytilton/web-mx/blob/main/src/tiltontec/example/quick_start/README.md I have been coding this way since literally the last century, starting in Common Lisp and Mac Quickdraw desktop GUIs, so I have zero perspective on the beast. Is that ^^^ just a mystery to others? Thx! šŸ™

šŸ‘ 3
seancorfield21:02:43

I think my only concern would be that the conciseness (which is nice) leads to some very cryptic naming. with-cc, fmu, evt-md, cF+ The README uses svg-plus and then speed-plus and doesn't define either -- I assume they are not part of web-mx itself?

seancorfield21:02:29

The automated aspect of data flow driving reactive changes is certainly nice.

kennytilton21:02:35

Thx! I will add my standard mea culpa: I hate typing. And a justification: these little guys get used a lot so they become familiar. I am also adding a glossary, and will include all these. Part of the problem may be that I grew up before code completion was a thing; super-concise may be a vestigial accommodation I need to lose.

šŸ‘ 2
isak21:02:30

Overall it seems very interesting and worth checking out. A few things seem strange (coming from a re-frame background): ā€¢ Multiple attribute maps (I guess one for html, and one for the framework) ā€¢ Cryptic names like Sean said ā€¢ State organized around/inside html elements (kind of), instead of an app-db

isak21:02:11

Also are some of the macros anaphoric? For example I see me, but didn't see where it came from

seancorfield21:02:08

Re: named state -- I assume qualified keywords can be used for those :name fields so you ensure you don't get "conflicts" across multiple items across the codebase?

seancorfield21:02:51

@U08JKUHA9 I kinda like me as a reference to the current HTML element šŸ™‚

šŸ‘ 2
isak22:02:06

Hmm yea it might be good, just not familiar to me

p-himik22:02:56

It'd be helpful to add what each abbreviation stands for. I noticed it only for cI, but maybe I didn't look hard enough. Also, personally I have a rather strong dislike for anaphoric macros. Glad Clojure itself doesn't have them.

kennytilton22:02:37

@U08JKUHA9 re "me", sorry, I hate typing. Yes, Web/MX is all in on anaphors, and lexical capturing as well. eg, fmu given only the name to seek relies on 'me having been anaphorically provided. It is hard to convey how much that improves the coding experience. And if it helps, I think me is the only place I do this. Speaking of this šŸ™‚ , my feeling is that this kind of obscurity is justified when used for these rare, vital, and ubiquitous symbols. As for the multiple attribute maps, yeah, agreed, that is a speed bump. But early on I had just one map and sorted out which was for HTML and which was for the app, and it did not go well. This then is a case where I go the other way and make the dev work a little harder. I guess a small win is having them clearly distinguished in the source. All that said, if the consensus goes the other way on this, I would switch to a flatter syntax. All the properties end up as props of the same map anyway, so I would just need to hardcode which attributes go to HTML, and that is just a copy/paste from the MDN doc. Thx for the input!

šŸ‘ 2
seancorfield22:02:51

I think (tag tag-attributes state content) is fine with tag-attributes and state optional -- well, and content for some tags -- and I found that part intuitive, reading the code.

šŸ‘ 2
isak22:02:51

Not sure if this would be better, but another option would be to use meta: ^{:name speedometer ... } (span {:class ...} ...) (that is how I usually add a react :key in re-frame)

kennytilton22:02:10

Re the :name field, good catch @U04V70XH6. I did not go into it just to keep things concise, and it helps to know you picked up on that. I will add something more about navigation. The fact is, navigation is the only thing that slows me down when coding this way. And there is a lot more to navigation than I let on! To begin with, MX models are like the DOM: every element but the root has one parent and knows that parent as a fixed property, and everyone has zero or more children. So in any nasty organization that should arise, a dev could write their own navigation. Second, the provided fm-navig utility takes a "test" function as its first parameter, which by default tests the :name of every node for a match with the sought name. In an OO world (CLOS) I searched more often for a nearby widget of the same class. So a radio button would just search for the nearest radio group, which offered necessary flexibility on rich radio items. Anyway, ambiguity can prolly be resolved by writing a sufficiently clever test function. FWIW, I have never had to do that. And speaking of "nearby", another thing I decided not to go into: fm-navig by default does a depth-first, left-right search starting at the provided start node, recursing up to the start's parent if necessary. So search for a name benefits from a natural scoping, and duplicate names arising from a row of similar componets works out fine. Lately I have thought about supporting a path of names. That would be easy, but I always wait for a use case before implementing things. I should add that the reason navigation slows me down is that this is one place where I can get into a cycle: if I am computing a list of children, I have to use the option that tells fm-navig to search "up only", because otherwise it will ask for the children I am busy computing. Boom. Thx! I'll add more on navigation.

seancorfield22:02:31

In the main repo's README, there's a link to rxTrak https://github.com/kennytilton/matrix/tree/main/cljs/rxtrak that is a 404

kennytilton22:02:00

I'll also add the source for the "components" -- I think of these as a substitute for Web components: https://developer.mozilla.org/en-US/docs/Web/Web_Components

kennytilton22:02:25

Thx, @U04V70XH6. I recently had the bright idea of fixing a directory misnomer (cljs should have been cljc) and 404'ed a kazillion links. šŸ˜ž I'll track that down.

seancorfield22:02:33

@U0PUGPSFR Do you have any examples that have a Clojure backend so I can see how web/mx deals with server calls and responses?

kennytilton23:02:30

No, @U04V70XH6, but a fullstack example would be a good idea. The "Cat Chat" example demonstrates XHR, if that helps. I also executed the https://github.com/staltz/flux-challenge, if you are familiar with that: https://github.com/kennytilton/matrix/tree/main/cljc/fluxchallenge. Not sure if that has succumbed to bit rot, but I think it ran OK just a few months ago. That uses both Web Sockets and http to get the galactic info it needs.

seancorfield23:02:58

Ah, that looks pretty straightforward... (async cats)

seancorfield23:02:40

I tried to view the cljdoc's for web/mx but the build fails with

Execution error (ExceptionInfo) at cljs.analyzer/error (analyzer.cljc:762).
Invalid :refer, var tiltontec.model.core/fget does not exist in file /tmp/cljdoc-com.tiltontec-web-mx-1.0.0-SNAPSHOT11628004566724414461/src/main/clojure/tiltontec/web_mx/html.cljs
Dunno if you want to follow-up in #C8V0BQ0M6?

seancorfield23:02:15

Ah, it looks like that's something in the 1.0.0-SNAPSHOT on Clojars that has gone away since? Maybe you could deploy an up-to-date build to Clojars so http://cljdoc.org can try to analyze the latest source?

kennytilton23:02:51

New deploy of 1.0.0-SNAPSHOT just made, @U04V70XH6. I'll try the cljdoc mysel, too.

seancorfield23:02:41

I kicked off a cljdoc rebuild of that snapshot (using the almost invisible rebuild button)

kennytilton00:03:19

Awesome. I'll study up on cljdoc to see how I can clean things up and get more docstrings in there.

kennytilton00:03:50

@U2FRKM4TW wrote: "I have a rather strong dislike for anaphoric macros" I thought I could hide behind the self and this precedents. Not working? :)

kennytilton00:03:38

Speaking of doc, in a case like this I get confused. Am I documenting for the Web/MX user, or the Web/MX maintainer? Or should I not care? Also, should I make an effort to use defn-? Will that make the cljdoc-genned doc more approachable?

seancorfield00:03:12

Looking over the docs generated for web/mx, it seems that you need to know a fair bit of matrix too so I just generated the latest version of that https://cljdoc.org/d/com.tiltontec/matrix/4.3.1-SNAPSHOT/doc/readme You probably want a cljdoc.edn file in doc/ and have it link to other .md files through the repo to create a set of articles in the top-right section of the http://cljdoc.org pages.

seancorfield00:03:55

(you'll also want to add docstrings to all the public functions you want users to understand -- and ^:no-doc metadata to the nses you want to hide from cljdoc)

kennytilton00:03:36

Thanks for the running jump on cljdoc!

p-himik00:03:39

@U0PUGPSFR If that's Python's self then it's not like that at all - the argument is explicit, it's just a convention to name it self. And this is a keyword in all the languages I know, you can't willy-nilly use it there. Personally, I'd prefer to see something like the existing this-as or have something like >! from core.async (which is a symbol from that ns but is only used to rewrite the code where it's mentioned by the go macro).

p-himik00:03:43

But it's just that - a preference. AFAIK, anaphorism doesn't mean a lack of hygiene in macros, and it's the latter that I really don't like - it can screw things up big time.

kennytilton00:03:36

No, I was thinking of Smalltalk self. Or C++ this , another precedent.

phill11:03:11

Clojure's "proxy" function involves anaphoric 'this. BTW, I like the concise names because they suggest that there is going to be a limited number of primitives, which, once familiar, will be unintrusive.

p-himik11:03:53

Ah, right, forgot about proxy. Which is actually a problem even without taking potential developer mistakes into account because sometimes you'll have to create ugliness like this: https://github.com/tailrecursion/javastar/blob/master/src/tailrecursion/javastar.clj#L43

phill11:03:53

Back to web-mx. Would it be fair to say: A tree of Hiccup looks like data, such as [:div ...], which can be read from a resource, received from a service, converted from HTML possibly written by Dreamweaver (as Enlive does), constructed or revised progressively as data structures, or inserted into the (v)DOM more than once, like a rubber stamp. Perhaps Hiccup itself can be written or understood by HTML technicians. But, in practice (e.g., in Reagent), strictly speaking, Hiccup's plain-old-data aspect is more convenience than principle, because the general case includes at least references to event handlers, and perhaps complete local state too. In web-mx, there is no pretense of plain-old-data; element descriptions are procedural and evaluate to a stateful, single-parent DOM element and web-mx constructs are programs meant to be programmed by programmers.

kennytilton12:03:58

@U0HG4EHMH wrote: "I like the concise names because they suggest that there is going to be a limited number of primitives, which, once familiar, will be unintrusive." Yes, that is my thinking. I am a fervent advocate of long data names, but when they name things I will be using all over the place, I go the other way: I do not want to be typing even "self" if I can come up with sth shorter; and the frequency will translate to familiarity in short order, as you say.

kennytilton12:03:23

@U0HG4EHMH wrote: "In web-mx, there is no pretense of plain-old-data; element descriptions are procedural and evaluate to a stateful, single-parent DOM element and web-mx constructs are programs meant to be programmed by programmers. " Correct. I have fantasies of a hard-core, JS-savvy graphic designer tweaking a Web/MX app at 3am when a production emergency arises, because it is pretty easy to reshuffle chunks of DOM, but ... actually, I have not dwelt on this in the quick-start, but the CSS is also formulaic/dynamic, potentially down to individual style properties, if we want to leverage the make-css-style mechanism. Sometimes I think CSS might be better off co-located with the DOM construction, and class-juggling replaced by functional composition, which similarly would move CSS back to the programmer. :thinking_face:

kennytilton12:03:44

One thing about the short names: a Web/MX user can always write their own wrappers if cI or cF+ drive them crazy. They are just functions/macros. I did have someone do that back in the Common Lisp world when they were just starting to use https://github.com/kennytilton/qooxlisp.

kennytilton15:03:54

And of course then folks can hack away at the examples. I'll be happy to onboard anyone who gets stuck, btw.

kennytilton16:03:44

@U08JKUHA9 wrote: "State organized around/inside html elements (kind of), instead of an app-db" Yes, that is definitely swimming against the Flux tide, and to me it is a feature, not a bug. šŸ™‚ I see a separate store in a GUI app as one example of where separation of concerns is a mistake. It was intended by FB engineers to make state change manageable, and the separate store is indeed sth we can manage reliably (yay!) but...boilerplate. Aside from the boilerplate nuisance, it leaves substantial terrain uncovered by the reactive system: with a separate store, the dataflow stops at the view function boundary: I look at a view function and see several subscriptions. Now the view function has the data it needs, and it uses it here and there while building the DOM and deciding the DOM attributes. Not bad, but not very granular. Performance-wise we get a small degradation from unnecessary notification, but code-wise there is no clear code connection between, say, a widget's style and the to-do state. I can usually follow it, but on big hairy components the disconnect between store and final widget rendering softens the declarative win, and we always have the work of first boilerplate and then connecting subscribed data to the component details. It is as if the subscribed data is reliably being delivered to a warehouse outside the city and a wodge of code is delivering it as needed to different final destinations within the city. With Web/MX each formula for each property decides what state it needs. A large container, say a to-do list, might pull in all the to-dos, but then the list components have formulas that select from that blob. And every todo is its own little reactive model. If the "completed" widget of a to-do list item is clicked, it just toggles the completed property of the to-do, and that state change propagates all the way to the affected properties. No new code needs to be written; the formula just runs again and produces a new property value. And no explicit subscribe is necessary, because a little clever dynamic variable and macrology and MX internals can see the dependency access and record it. If it is any consolation, we still have the one-way DAG of Flux/Redux/app-db, but it is an "in place" DAG. The property does not need to travel to the DAG, the DAG travels to the property. Stop me before I metaphor again. šŸ™€

isak16:03:17

@U0PUGPSFRI agree the increased granularity you have is beneficial, but I don't see why it would need to be tied to local rather than global state. If the data was dereferenced exactly where it was needed (e.g., the :width inside of a :style on a span), it seems like the same granular re-rendering would still be possible. No?

āž• 1
isak16:03:14

A lot of the pros/cons are probably pretty subjective, but I think there is one that more people can agree on, which is more shared code between multiple platforms

isak16:03:12

For example, some people say they are able to use React like technologies and reuse 80% of their codebase between iOS and Android. It is hard to imagine that being possible if the state was tied to components (since they would be different for different platforms)

kennytilton17:03:52

"why it would need to be tied to local rather than global state" Well, we do go "global" in the sense that I can navigate the app widget tree to find specific state. And each little bit of local state is in fact part of the global state, if anyone cares to navigate to my local node. But more grandly, the simple fact is that a separate store is an artificial Balkanization (here I go again) of state, taking it away from its natural location in our web app component tree, the thing we are trying to build for our user. Something else I have failed to harp on is my disagreement with the popular view-model bifurcation, where view is a second-class citizen. FB famously declared View=f(state) . Yes, but View is state, too, the state of most value to the user. So let's not sunder them, is my thinking. "It is hard to imagine that being possible if the state was tied to components (since they would be different for different platforms)" OK, another thing I did not belabor in the quick-start was that web/mx code does not yield DOM, it yields proxy objects from which we generate the DOM. We can take a Web/MX app and point it at React or ReactNative and it will look the same. The only difference is that Web/MX uses direct DOM manipulation to relay proxy state changes to the corresponding DOM elements/attributes with fine granularity. My nascent React/MX and RN/MX stuff is stuck with setState. Further along is my https://github.com/kennytilton/flutter-mx wrapper. There, too, we play nice and just use their setState. Otherwise, the beef of an application lives at the proxy level; Dart/Flutter are just backends. And, again, the beef defines its own "in place" DAG, which would translate unchanged if we change backends. So we do have everything a separate store offers, we just do not require the definition of the store or the piping of subscriptions to component substructure by view functions. Thanks btw for all the feedback!

šŸ‘ 1
kennytilton17:03:42

Fun follow-up idea: Flutter is super-ambitious and detailed, but I miss the insane regularity of HTML. I am sorely tempted to add an HTML-workalike layer of macrology to Flutter/MX. That would definitely make sharing easier.

isak18:03:15

"taking it away from its natural location in our web app component tree" I think some data may be feel natural to organize in a component, but there are important exceptions, e.g., current-user, language, display settings, etc. For any of those, trying to cram them into a component would seem unnatural to me. Though maybe if there were a component for the app-root that had all such data that you could reference anywhere you said it may be fine. "OK, another thing I did not belabor in the quick-start was that web/mx code does not yield DOM, it yields proxy objects from which we generate the DOM. We can take a Web/MX app and point it at React or ReactNative and it will look the same" I was thinking not just the components would be different, but the whole structure may be different too (e.g., some things that are attributes for one platform require component nesting in others ( like scrolling)). So my guess is because of that, tying state to these proxy elements will still require more changes. But not sure, you may be right here.

kennytilton18:03:24

"I think some data may be feel natural to organize in a component, but there are important exceptions, e.g., current-user, language, display settings, etc." Absolutely. As app architects, we decide where state belongs, and things required everywhere end up very high in the hierarchy. For example, the root MX object in the todo-mvc example looks like this, with routing and existing todos right at the top.

(md/make ::md/todoApp
      :route (cI "All")
      :route-starter (r/start! (r/router [["/" :All]
                                          ["/active" :Active]
                                          ["/completed" :Completed]])
                       {:default     :ignore
                        :on-navigate (fn [route params query]
                                       (when-let [mtx @md/matrix]
                                         (mset! mtx :route (name route))))})
      :todos (todo/todo-list)
      :mx-dom (cFonce
                (with-par me
                  (section {:class "todoapp" :style "padding:24px"}
                    ;(webco/wall-clock :date 60000 0 15)
                    ;(webco/wall-clock :time 1000 0 8)
                    (header {:class "header"}
                      (h1 "todos")
                      (todo-entry-field))
                    (todo-items-list)
                    (todo-items-dashboard)
                    (webco/app-credits mxtodo-credits)))))
Note that this is a little inflexible: if I wanted multiple to-do lists, we would need to refactor, creating a to-do-list component where to-dos from a specific list would be kept. We could name each of them :to-do-list, because the existing navigation utilities work inside out from me. So any sub-component that says (fmu :to-do-list) will find the right list component. "I was thinking not just the components would be different, but the whole structure may be different too (e.g., some things that are attributes for one platform require component nesting in others ( like scrolling))." Sure, but the proxy app we build one place will stay the same if we are just switching target OSes. If there is a radical OS shortcoming, or superior U/X mechanism available, we would conform to that, but it would be relatively modest restructuring -- Web/MX code chunks survive large scale reshuffling quite well because of the navigation flexibility: no matter what we do to the U/X of a todo list, it is bound to have a :to-do-list at the top, and find it OK without us tweaking the navigation.

šŸ‘ 1
isak21:03:04

Good points šŸ‘

kennytilton10:03:43

The runnable quick-start now has a super-basic "glossary" button on each example. Shows/hides this:

kennytilton10:03:01

I think I want to get the whole thing running as a GitHub page next rather than rebuild the readme with all the newly commented examples. Will try that when I am awake. šŸ›ļø