Fork me on GitHub
#hoplon
<
2016-05-31
>
lewix02:05:35

Where can i find examples of components talking to components - in another word , a large app example

mynomoto03:05:28

@lewix: not sure if this is big enough for what you want but there is https://github.com/staltz/flux-challenge/tree/master/submissions/mynomoto that I did sometime ago.

lewix03:05:11

i played with hoplon...still cannot make a choice between hoplon and re-frame 😕

dm305:05:21

@piotrek: that's what I meant 🙂

onetom06:05:19

we would like to deploy a standalone hoplon frontend to S3 but the source files are also dumped into the target/ dir. what's the idiomatic way to exclude them? (since we are not putting the source maps into production either, there is no use for the source files. or should we just deploy the source proudly? :)

dm306:05:58

(sift-jar :exclude ...) in the boot task

dm306:05:25

source files appearing in sources means they're actually set as resources

micha12:05:45

@lewix: if you want to see the my latest thinking with respect to organization of large applications i have some examples

lewix12:05:12

micha: sure

micha12:05:42

the workflow/form-machine on L32 is a state machine that mediates interaction with the database backend

micha12:05:21

the thing to notice is how we use extremely flexible reusable components there

micha12:05:31

no boilerplate really

micha12:05:53

and it does all the complex behavior that a real application will need

micha12:05:20

like validation (backend only for now), displaying contextual error messages, things like that

micha12:05:56

also the "i forgot my password" thing works the right way, and doesn't make you retype your email address 🙂

micha12:05:13

for the overall organization of the application we use composable "contexts"

micha12:05:59

that is like mutable state (in cells) that is shared among ui components

micha13:05:40

we use macros to set dynamic vars, and those vars become the refault values for custom element (that's the hoplon way to say "component") attributes

micha13:05:23

for example form/with-form is a macro that sets up the form context

micha13:05:41

form context contains dynamic bindings for the form machine etc

micha13:05:16

custom elements from the form namespace are aware of these dynamic vars and use them if present to configure themselves

micha13:05:11

the form/with-field macro makes bindings for cells associated with a specific field in the form

micha13:05:46

then for example the custom element form/validation-message, which is nested in both the form and the form field contexts

micha13:05:04

that custom element uses context bindings from both of those contexts to configure itself

micha13:05:11

so you don't need any boilerplate glue

micha13:05:36

but you also don't need to explicitly pass contexts through all the intermediate elements

micha13:05:16

so i could wrap that validation-message custom element in any number of other elements that have no need to know or care about the bindings related to the form stuff

micha13:05:38

also that example is made from all reusable components

micha13:05:59

it looks like it couldn't be, becaus ethere is basically no configuration in there, so how does it know?

micha13:05:12

but it is, all forms in the application look like that, pretty much

micha13:05:34

with some individual things here and there, like the logic you can see in that example around the "i forgot my password" state

micha13:05:58

that's something that's unique to the login form, that specific workflow

micha13:05:53

(i'm referring there to lines 50, 56, 61, 71, and 72)

micha13:05:44

the idea is that you can abstract away the reusable aspects while still exposing all the knobs you need to customize for a specific implementation

micha13:05:21

and the default configuration is derived from contexts which are set at a higher level than the UI elements

micha13:05:10

using dynamic extent instead of passing context in a map through the component tree also has the advantage of separation of concerns

micha13:05:34

the concerns of configuration context and location in the dom are orthogonal

micha13:05:21

that's why we have dynamic vars, right?

micha13:05:57

and good factoring and separation of concerns allows you to build more powerful abstractions and eliminate boilerplate and hard to follow logic

micha13:05:13

especially when you're working in an asynchronous environment like in a browser

micha13:05:17

the deeper layers of the application get progressively less elegant, but that's fine because they will all be factored out into separate libraries and require very little maintenance

micha13:05:51

the part that really needs to be elegant and easy to understand is the application layer, where we assemble the actual appliaction

micha13:05:14

that's the part we work on to add features and new workflows

micha13:05:48

using all the general reusable components developed in the lower level libraries and namespaces

micha13:05:36

we're pretty much at the point with that application now that adding new features hardly ever involves mucking around in lower level code

micha13:05:55

we just assemble a new screen and workflow from existing components

micha13:05:29

or the submit-primary element, which knows when a request is in flight to the backend and shows Working... and disables itself until the request completes

micha13:05:32

the cool thing is that it manages its own state, and it doesn't need to ahev everything passed to it via arguments

micha13:05:53

so that element can be created by another piece of code that doesn't know about anything

micha13:05:22

when you need to pass info directly down as parameters it becomes difficult to make higher-order components

micha13:05:39

because you need to tell the higher order thing how to pass stuff down to the things it creates

micha13:05:19

"don't know, don't care" is i think the key to separation of concerns, which is key to developing general-purpose higher-order functions, which is the key to making maintainable applications 🙂

flyboarder15:05:44

@micha Ln 233 has attr before you merge with attr

flyboarder15:05:24

@micha how are you binding the dynamic vars? with-bindings? I was looking into this last week and got rather turned around by the different macros for binding dynamic vars

micha15:05:54

@flyboarder: the macros are just wrappers for the binding cljs macro

micha15:05:05

totally just an aesthetic move really

micha15:05:12

probably should have just used binding there, but with my wrapper macros i have the option to include default bindings

micha15:05:45

you can accomplish a similar default binding situation when you define the dynamic vars too, so not sure if the wrappers really provide much value overall

micha15:05:52

still working things out there, you know

micha16:05:30

re: L233 not sure what you mean?

micha16:05:47

ah yeah, that was a sort of recent tweak in hoplon

micha16:05:26

maps can be anywhere and are interpreted as attributes

flyboarder16:05:19

so it would seem that this method is pretty much the same as passing in the state, with the exception you don't actually have to call the :attribute, does this change how cells are expected to behave?

micha16:05:58

no change

micha16:05:06

it's not the same as passing in the state though

micha16:05:27

just like dynamic extent is not the same as passing values in function parameters

micha16:05:46

for example intermediate functions don't need to know or care about them

micha16:05:04

otherwise every function would need to know how to pass things down to any function they call

micha16:05:15

spaghetti code, basically

flyboarder16:05:25

(insert my app here)

micha16:05:29

all global variables, just smuggled in in a map

micha16:05:57

the advantage of dynamic bindings is that they can refer to anonymous things

micha16:05:59

and so on

micha16:05:16

everything in a giant map needs to have a well-known key schema

micha16:05:37

which means your head quickly explodes trying to understand how things are related to each other

micha16:05:11

like if every component gets its configuration and whatnot from some giant map

micha16:05:22

you have effectively recreated a coconut global scope

micha16:05:34

and soon you start to use namespaced keywords

micha16:05:46

thereby recreating a coconut namespace system

micha16:05:58

pointless and an antipattern imho

micha16:05:26

also maps are immutable, so components can't keep references to things

flyboarder16:05:15

so when would passing in state be appropriate then? this still feels similar to a custom component with a let and then a child component using some var in the let

micha16:05:42

yeah it's another tool in the toolbox

micha16:05:15

the let is for components that are not related abstractions, but are related in the specific instance in which they aer assembled at the application layer

micha16:05:35

the dynamic bindings are at the abstraction layer

micha16:05:38

very general

micha16:05:57

you use let when you're assembling reusable components into an application

micha16:05:16

where there is nothing reusable there, because it's specific to a particular screen or workflow

flyboarder16:05:50

ok that makes sense bindings => related abstractions let => related instances

micha16:05:08

at the application layer you don't form new abstractions, you simply assemble more general things there into an instance of a particular thing

micha16:05:16

precisely!

micha16:05:37

the bindings are like the knight in chess

micha16:05:43

a strategic tool

micha16:05:07

jumping over other pieces

micha16:05:40

lol i dunno how far that analogy holds

micha16:05:00

it's the image i have in my head though

flyboarder16:05:29

makes sense imo, using it in forms makes more sense now too

flyboarder16:05:52

as forms are a very general thing that applications do

micha16:05:59

it's important that the dynamic bindings provide only default configurations

flyboarder16:05:12

right since they can be different at any time

micha16:05:15

when you need to override them in a more specific case you can

micha16:05:32

surgically, and without disturbing the rest of the abstraction

micha16:05:48

and you can even then build a new abstraction based on the other more general one

micha16:05:37

we also have a pagination context, for instance

micha16:05:01

because pagination is a single abstraction that encompasses many different ui elements

micha16:05:21

and we use that in totally different situations with maximal code reuse

micha16:05:28

like for example a paginated table of data

micha16:05:45

with the next/previous page buttons, current page, # items per page, etc

micha16:05:03

but we also use the same abstraction inside the typeahead widget

micha16:05:18

because typeahead hints must also be paginated, naturally

micha16:05:47

totally different application layer, but built on the same underlying abstraction

flyboarder16:05:24

right and thats where you get to take advantage of the code reuse

micha16:05:31

yeah it's huge

micha16:05:16

handling paginated state and managing how to get that data from the backend in a clean no boilerplate way was a huge step toward maintainability

flyboarder16:05:33

yeah I can already think of instances in my app where that would have saved a bunch of time and pass around way less

micha16:05:22

the only tricky/messy part is that you need to convert dynamic bindings into lexical ones in the "consumer" if there will be async callbacks thta need those values

micha16:05:37

unfortunately cljs doesn't have bound-fn

micha16:05:59

that's how you'd normally do things in clojure when you use dynamic vars like this in an asynchronous program

micha16:05:06

for example

micha16:05:32

(defelem submit-primary
  [{:keys [state text active-text] :or {text "Save" active-text "Working..."} :as attr} kids]
  (let [state (or state *form-loading*)
        attr  (dissoc attr :state :text :active-text)]
    [(button/primary
       (merge {:type "submit" :toggle (cell= (= 0 state))} attr)
       (or (seq kids) text))
     (button/primary
       attr
       (merge {:disabled true :toggle (cell= (not= 0 state))} attr)
       (or (seq kids) active-text))]))

micha16:05:49

the (let [state (or state *form-loading*) part

micha16:05:02

slack lol

flyboarder16:05:44

yeah thats what I was thinking of doing

micha16:05:49

the cells are the async part in that example

flyboarder16:05:55

which would still allow local state to be used instead

micha16:05:07

works great for us so far

flyboarder16:05:36

thats awesome!

flyboarder16:05:41

great pattern

flyboarder16:05:59

seems so clean when you think about it

micha16:05:10

i've been trying to factor it out into something that can be crystallized into a library

micha16:05:18

but it's surprisingly hard

flyboarder16:05:40

yeah thats a fine line between a library and the language

micha16:05:51

indicating to me that it's still not completely right yet

micha16:05:14

some coconut water still sloshing around in the brain somewhere

micha16:05:12

i've also been trying to figure out how to make this into a more rigorous state machine thing, but don't know how to do it: https://gist.github.com/micha/622a62d6a52ff419e5ac

micha16:05:47

want to factor that into a library too, but it would be too specific in its current form

micha16:05:39

expressing it in terms of a more correct state machine seems like it would help there

flyboarder16:05:49

yeah but much of that is form specific

flyboarder16:05:54

although submit and validate are probably pretty common for state systems

flyboarder16:05:10

interesting challenge

flyboarder18:05:37

@micha what about when using loop-tpl? for example right now I have this (not the best) https://gist.github.com/flyboarder/8a5055b212785818ebfa53ddeb05e2f2

micha18:05:30

usually i will make the let binding in the loop-tpl body

micha18:05:56

really want bound-fn there

micha18:05:57

current research is leading toward a type of macro that goes along with the context macros to automate that binding process

micha18:05:36

or alternatively perhaps a macro for defining dynamic vars such that they can be discovered at runtime, like clojure has

flyboarder19:05:12

how come this loop-tpl gets executed when my :bindings is set to null?

flyboarder19:05:34

it doesnt appear but it forced me to do LN21

flyboarder19:05:59

or is that just the formula cell doing that?

micha19:05:16

hm really? that looks like a bug

micha19:05:36

it shouldn't evaluate the body until the seqable is not empty

flyboarder19:05:09

yeah if I do :class [severity]I get a dose not support name:

micha19:05:20

also i'm not sure how that loop-tpl is working ,shouldn't it evaluate to just a single element?

flyboarder19:05:43

yeah compact-message

micha19:05:46

that error is different

flyboarder19:05:57

formatting error from copy paste

micha19:05:14

or did we merge the change that lets you have a vector of classes already?

micha19:05:29

it should be expecting a map, and it can't get the name of the keys there

flyboarder19:05:44

yeah the vector works but not when severity = nil

flyboarder19:05:13

which happens when the cell is reset so it doesnt show anything but it throws an error

flyboarder19:05:18

i believe it converts vectors to maps, i copied the same part from core and put it in semui-hl

micha19:05:18

ah so the vector shouldn't work

micha19:05:40

it looks for maps or kvs

micha19:05:04

are you using hoplon ui?

micha19:05:17

i think that was included in the reactive-elements branch of hoplon

flyboarder19:05:19

doesnt that convert it

micha19:05:43

it does zipmap

micha19:05:58

which expects key value key value

flyboarder19:05:01

right so as long as my vector doesnt contain nil it works

flyboarder19:05:22

otherwise it works like usual :class [:class1 :class2]

micha19:05:32

that shouldn't work lol

flyboarder19:05:37

but it does 😛

micha19:05:53

that should set only class1

flyboarder19:05:58

thanks to zipmap it gets {:class1 :class1 :class2 :class2}

micha19:05:14

whoa really?

flyboarder19:05:29

so as long as kvs isnt nil

micha19:05:34

ah ok yes

micha19:05:38

so that was merged

micha19:05:45

whew thought i was losing my mind there

flyboarder19:05:55

hahaha sanity restored

micha19:05:03

math still works...for now

flyboarder19:05:13

until we discover force 5

micha19:05:59

what's force 5?

flyboarder19:05:32

like known forces in physics, gravity electromagnetism, weak/strong atomic bonds

flyboarder19:05:44

they believe they might have inadvertently experienced the result a 5th force, other 95% of the universe here we come!

flyboarder19:05:32

@micha I think a simple fix would be to change (seq kvs) to (seq (or kvs [:class false]))

flyboarder19:05:39

wait that wont work

flyboarder19:05:06

it needs to be in the clmap

flyboarder19:05:23

so maybe change clmap if to cond and check for nil

micha19:05:46

doseq shouldn't do anything if the sequence is empty though

micha19:05:02

ah i see the problem

flyboarder19:05:09

its in ->map tho

micha19:05:17

no i think it's the way you're using cells

micha19:05:36

you want :class (cell= [severity])

micha19:05:44

because severity there is a cell

micha19:05:47

which is never nil

micha19:05:52

i mean the cell itself

micha19:05:09

and vectors aren't cells so hoplon doesn't wire it up reactively

micha19:05:21

if you wrap the vector in a formula then you get the thing you want

flyboarder19:05:35

ah ok so it’s becuase im wrapping it in the vector

micha19:05:48

yeah but i think it still won't really work the way it's supposed to

micha19:05:06

there doesn't appear to be any way to remove classes there

micha19:05:39

something would need to diff the classes then

flyboarder19:05:49

that change worked

micha19:05:51

which seems overly complicated for such a simple thing

micha19:05:12

what happens when you add classes to the vector and then remove them?

micha19:05:24

i would think that the classes won't be removed from the element

micha19:05:38

because there is no mechanism to remember the ones that were set before

flyboarder19:05:06

does the same thing on removing them, throws the same error on remove

flyboarder19:05:32

Uncaught Error: Doesn't support name:

micha19:05:34

yeah i would probably just use the map

flyboarder19:05:10

like so? (cell= {severity severity})

micha19:05:53

that will also make an error

micha19:05:03

you want to have a map with the classes

micha19:05:06

or a string

micha19:05:14

actually not a string

micha19:05:53

that has like {:ok (= severity :ok) :bad (= severity :bad)} basically

flyboarder19:05:21

but the severity is my class

micha19:05:30

how do you mean

flyboarder19:05:39

severity is a value that is the class name

micha19:05:55

yeah that's not really supported currently

micha19:05:02

you need to have a map really

micha19:05:13

with keys for each severity

micha19:05:23

and values that say whether that class is on or off

flyboarder19:05:35

so right now i dont know what the class name is as it comes from the db

flyboarder19:05:54

i guess ill stick with (cell= (when severity [severity]))

micha19:05:01

that won't work though

micha19:05:29

at least not if you want to change the class dynamically

micha19:05:53

once it sets a class that way i don't think it will ever unset it, no matter how severity changes

micha19:05:00

it will jsut accumulate more and more classes

flyboarder19:05:27

well severity should never change it’s the severity of the error that was saved to db

flyboarder19:05:47

it’s just a key with vectors of errors

micha19:05:48

ah ok, so you're good to go then

micha19:05:03

we really can't leave that like that though

micha19:05:40

not sure how we can fix it other than removing the feature

micha19:05:05

i recommend you do a db query to get a map of all severity things from the database

flyboarder19:05:18

well this is my db structure:

micha19:05:21

they must be enumerable and finite beacuse you need to implement things in css

micha19:05:37

so they can't be just anything

micha19:05:48

they can only be a limited set of things that are implemented in css already

micha19:05:55

so you can totally make a map of them

micha19:05:10

and if it's not in that map then it's not implemented in css and can be ignored

flyboarder19:05:22

oh I see what you mean just use the known class names since there are only a handfull

micha19:05:41

because we are going to have to deprecate that class vctor thing

micha19:05:01

unless i'm missing something, i don't see how it can work properly

flyboarder19:05:28

im sure you understand it more than I so 😛

micha19:05:32

all it can do is add classes

micha19:05:39

which is only half the job

micha19:05:36

perhaps i can add a little dynamic binding magic 🙂

flyboarder19:05:40

so would (cell= (when severity [severity])) cause memory leaks or just static content if severity were to change

micha19:05:50

it wouldn't cause any leaks

flyboarder19:05:54

ill have to test this

micha19:05:22

but like once you set severity cell to non-nil it will add a class to the element

micha19:05:31

and if you set it back to nil the class won't be removed

flyboarder19:05:53

hmmm so i cant change keys in my db anyway, it would be the same as removing it and adding it under the new name

micha19:05:53

and if you change the value of severity to a different class, then the element will have both classes

micha19:05:37

ok yeah i see how to fix it

flyboarder19:05:52

so (cell= (when severity [severity])) does hide things tho without throwing the error

micha20:05:25

hide things?

flyboarder20:05:40

well what is in loop-tpl doesnt show

flyboarder20:05:56

which is weird that it throws the error, without it

micha20:05:10

the error is from calling name on nil

micha20:05:33

when it tries to add the nil class to the element

flyboarder20:05:45

but it’s calling name on nil once the :bindings have become nil, why is it trying to add the class to the element that it is hiding?

micha20:05:19

because the severity cell contains the value nil

micha20:05:33

and you dtold it to add the value of the severity cell as a class to the element

micha20:05:36

so it's trying to do that

flyboarder20:05:03

so the attributes are updated even tho the elements are not being displayed

micha20:05:10

yes that's key

flyboarder20:05:15

because the cell changes

micha20:05:41

that's a crucial thing because otherwise you'd need to manually manage how to "catch up" the element's state when it goes back in the dom

micha20:05:07

that's the kind of thing you need to do with react, they have a ton of protocols you need to implement basically

flyboarder20:05:12

which is kinda what im doing with the when

micha20:05:17

with this system it's way simpler though

micha20:05:29

well you'd need that anyway

micha20:05:47

beacuse there is still no guarantee that severity isn't nil when it's in the dom or not

flyboarder20:05:52

unless i change it to a map

micha20:05:54

the concerns are orthogonal

flyboarder20:05:49

so is this really a bug then? or simply a poor usage pattern

micha20:05:06

i think it's a bug, i have an idea to fix it

flyboarder20:05:43

ok i think im tracking now, so the class is still present while it’s hidden, im just not seeing it because 1) it doesnt change for me 2) either exists or is deleted in my instance

flyboarder20:05:05

it’s never in another state which could then have both classes

micha20:05:51

i think if you have a situation where severity is "foo" and then later severity is "bar" you will see both classes on that element

flyboarder20:05:28

just wouldnt happen in my instance but it could if anyone else is using a vector in a loop-tpl like that

micha20:05:47

or with a vector in a cell