Fork me on GitHub
#hoplon
<
2016-03-25
>
thedavidmeister05:03:48

i’m working on a bit of functionality that scrolls an element into view when it gains focus, if it isn’t already

thedavidmeister05:03:52

:focusin #(do
                      (if-not (elem-in-viewport? (.-target %))
                        (c/with-timeout 0 (c/do! (.-target %) :scroll-to true)))
                      (in/clear-select! in/conn))

thedavidmeister05:03:56

this is what i came up with

thedavidmeister05:03:07

but is there a more “hoplon way” to do this than nested do!?

thedavidmeister05:03:28

i was thinking i might stick it in the db too...

thedavidmeister06:03:06

maybe i solved my own problem, lol

levitanong06:03:55

@thedavidmeister: how did you solve it? 😄

dm314:03:42

Micha - did you have any plans on opensourcing the components that you've built up on top of Hoplon in Adzerk. I recall you made some gists out of a workflow form, but nothing complete. It seems that the code was pretty generic and Hoplon could use some more code in the category of templates/demos/design principles to attract more following. People usually look for a simple way to write a crud app when evaluating frameworks for some reason simple_smile

micha14:03:36

that's my current interest, developing the techniques for separating state machine from ui components

dm314:03:01

if I'm ever done with the current project I'll have to make a series like modern-cljs for Hoplon

micha14:03:08

we have successfully done it in specific cases, but there is no overall model yet

micha14:03:52

but like in our application at adzerk the core functionality is all separated into state machine and ui

micha14:03:26

where the state machine is basically an object which has methods that you can call, and properties that are cells

micha14:03:50

you make formulas on the cell properties, and that's how you link the ui to the current state of the machine

micha14:03:23

like the form machine for example, has cells that contain validation error messages from the clientside validation or serverside validation

micha14:03:44

so when you make the ui form you can just have an element whose text is a formula of that cell

micha14:03:13

and the form machine has methods you can call, like (submit! the-form-machine) for instance

micha14:03:05

we also are developing a technique of using dynamic bindings and macros to create something like what i would call "ui domains" or "ui contexts"

micha14:03:48

but we need to ship code, so a lot of this is not fully abstracted into clean library form yet

dm314:03:55

I understand you do something like

(with-form-machine (make-state-machine)
         (input-a)
         (input-b))
and bind formulas to the values/errors/results produced by the machine using the dynamic scope

micha14:03:13

one sec i will paste

micha14:03:37

the with-... macros establish the context

micha14:03:48

so we have a screen context

micha14:03:10

in our app a screen is the equivalent of a page in a non-SPA

micha14:03:51

the value in the context approach is that elements nested deeply in there have access to the context

micha14:03:08

you don't need to pass that down explicitly through all the intermediate elements

dm314:03:13

yep, I recall seeing this one. It's interesting that dynamic scope in Clojurescript is underused (probably carrying over from Clojure where it's trickier to get right due to concurrency)

micha14:03:54

yeah there is one thing in cljs that really limits the usability of dynamic vars: cljs doesn't have bound-fn

micha14:03:12

because cljs is expecting to run in a single threaded environment

micha14:03:21

so dynamic vars are not thread-local

micha14:03:27

they're just global vars really

micha14:03:44

but because of this there is no place where they're stored

micha14:03:02

like in clojure each thread has a place where thread-local bindings are stored

micha14:03:18

so bound-fn is possible because when you call that it queries that place

micha14:03:28

and it knows which bindings it needs to bind

dm314:03:11

does it surface when you return a thing from defelem and then want to apply that Element/fn to additional arguments?

micha14:03:51

like consider this situation:

micha14:03:31

(def ^:dynamic *foo* 100)

(defelem bar [{:keys [foo] :as attr} kids]
  (div (text "foo is ~(or foo *foo*)")))

(html
  (body  
    (binding [*foo* 200]
      (bar))))

micha14:03:47

this is kind of what we want, but there is a bug

micha14:03:02

the idea is that the bar element has this :foo attribute you can use

micha14:03:14

but the default is taken from the context

micha14:03:24

does that make sense so far?

micha14:03:02

the value of the context is that the (bar) could be wrapped in layers of divs or whatever and it would still be able to see the *foo* dynamic var

dm314:03:31

ok, so if you use a 'dynamic' var in a cell, the evaluation will happen when the cell is propagated and the "dynamic" var is unbound

micha14:03:53

well the formula will recompute itself when the foo cell changes

micha14:03:08

so like suppose we had done something like

micha14:03:27

(defc foo 300)

...
(bar :foo foo)

micha14:03:51

so you would see `"foo is 300" in the div

micha14:03:55

initially

micha14:03:08

now suppose you do (reset! foo nil)

micha14:03:01

ok so when you reset to nil, you want to see "foo is 200"

micha14:03:11

like it should take the value from the context, right?

dm314:03:19

yep, I understood that simple_smile

micha14:03:29

but that happens asynchronously, in another thread basically

micha14:03:41

so what you actually get is "foo is 100"

micha14:03:01

so in clojure you could just automatically wrap in bound-fn

micha14:03:07

which doesn't exist in cljs

micha14:03:30

i think the issue can be solved with some tinkering

dm314:03:41

cell-specific bindings

micha14:03:51

what we do now is like this

micha14:03:26

(def ^:dynamic *foo* 100)

(defelem bar [{:keys [foo] :as attr} kids]
  (let [*foo* *foo*]
    (div (text "foo is ~(or foo *foo*)"))))

dm314:03:55

it seems like if you had this capture automatically when constructing cells it could work

dm314:03:23

although things wrapping code in .setTimeout would still fail

micha14:03:23

the difficulty is that you don't have a list of dynamic vars that are bound

micha14:03:54

but i think it can be made to work at least for the use of the context stuff

micha14:03:17

because we could "own" both ends -- constructing the context and making elements

dm314:03:27

wouldn't that work the same way you pick up other cells in cell= macro?

dm314:03:43

find symbols with earmuffs

micha14:03:46

(defcontext screen
  *foo* 100
  *bar* 200)

(defelem bar [{:keys [foo] :as attr} kids]
  (with-using-screen
    (div (text "foo is ~(or foo *foo*)"))))

micha14:03:05

or rather,

micha14:03:10

(with-screen
  :foo 130
  :bar 295
  (bar ...))

micha14:03:24

javelin doesn't do any macro stuff to identify cells or anything

micha14:03:34

in fact javelin doesn't need any macros at all

micha14:03:50

none of the actual implementation is in the macros, that's just syntax sugar

dm315:03:15

yes, I can see how it collects the symbols into *hoist*

micha15:03:30

(cell= (+ a b 1 2 3)) is the same as the functions here: ((formula +) a b 1 2 3)

micha15:03:36

formula is just a function

dm315:03:10

based on whether they are present in the macro &env

dm315:03:21

or not a special

micha15:03:33

right, specials can never be cells, of course

dm315:03:07

now I'm thinking - why does it check if symbol is in *env* at all?

dm315:03:58

the condition is

(defn hoist? [x local]
    (and (not (or (local x) (core? x))) (or (*env* x) (not (special? x)))))

dm315:03:22

so if it's not a symbol bound inside the formula and not a core symbol

dm315:03:27

(or (*env* x) (not (special? x))

dm315:03:56

ah, if it's a special bound in macro &env

dm315:03:01

then that's valid

dm315:03:41

yeah, to collect dynamic vars you'd have to make another pass before this one

dm315:03:45

kinda

(let [*x* *x*] ((formula +) a *x*))

micha15:03:59

you're saying to fix the dynamics at compile time

micha15:03:02

that's interesting

micha15:03:31

i wonder if we have access to that info inside the macro

micha15:03:37

like via the cljs compiler api

micha15:03:02

like given a symbol is it possible to identify whether it's a reference to a dynamic var

dm315:03:14

don't know

dm315:03:02

gotta go, please think about why this wouldn't work simple_smile

raywillig16:03:38

@micha: thanks for the tip on infinite scroll. i had to adapt it slightly differently to trigger on window scrolling but all good

raywillig17:03:35

@micha: can u ‘splain me something?

micha17:03:46

what's up?

raywillig17:03:06

so inside a defelem, i did something like (.on (js/jQuery js/window) “scroll” #(do-stuff with cells in the let block))

raywillig17:03:12

i would have thought that since something like (.on is global, by the time the thing fires those cells would be out of scope, but of course i was wrong and it worked just like i hoped it would but feared it wouldn't

micha17:03:58

you add a new handler for each element though

micha17:03:28

it would probably be more efficient to have the scroll handler just attached once and to have it reset a cell when it fires

raywillig17:03:36

in this case i knew there would only be one element

micha17:03:42

then all the elements can have formulas that refer to that one cell

raywillig17:03:54

oh but i see your point

raywillig17:03:06

i was wondering why it worked at all

micha17:03:08

it doesn't go out of scope because handlers are added to the window object in that case

micha17:03:14

and the window object doesn't go out of scope

raywillig17:03:41

but the cells that are the arguments to the anonymous function are defined in a let block in defelem

micha17:03:10

nothing will go out of scope if it's referenced by something that isn't eligable for GC

micha17:03:33

so the cell is referenced by the callback, which is added to the window object

micha17:03:50

so now the cell can't be garbage collected until the callback is

micha17:03:58

and the callback can't be GC'd until the window is

raywillig17:03:45

i got it. so going back to your original point. your suggestion would be to have that on scroll handler just update a cell that anything could hand a formula on?

micha17:03:09

you you could have a global cell there

micha17:03:13

in a namespace

micha17:03:23

that contains the window scrolling data

micha17:03:41

you could even put it in a delay so the handler wouldn't be added unless something uses it

raywillig17:03:49

oh that’s interesting thought

raywillig17:03:09

delay happens the first time something tries to deref it?

micha17:03:05

or to obtain the cell

roti21:03:59

@micha: I tried to copy case= from https://github.com/hoplon/hoplon/pull/93 , but I keep having the same problem

roti21:03:17

"Failed to execute 'insertBefore' on 'Node': parameter 1 is not of type 'Node'.", https://github.com/hoplon/hoplon/blob/master/src/hoplon/core.cljs#L99

roti21:03:31

I suppose I need to get the latest from the master branch, instead of 6.0.0-alpha13, don't I?

micha22:03:09

no, i don't think so

micha22:03:20

can you paste your code where you use it?

micha22:03:42

that looks like one of the branches is not evaluating to dom element

micha22:03:27

i think the case= macro should work with 6.0.0-alpha13

roti22:03:27

I took the macros case= and safe-deref and put them in a separate namespace

roti22:03:48

(ns habarnam.core
    (:require
      [javelin.core :as j]))



(defmacro ^:private safe-deref [expr] `(deref (or ~expr (atom))))

(defmacro case=
             [expr & clauses]
             (let [[cases tpls] (apply map vector (partition 2 clauses))
                            default      (when (odd? (count clauses)) (last clauses))
                            syms         (take (inc (count cases)) (repeatedly gensym))]
                       `(let [~@(interleave syms (map (fn [x] `(delay ~x)) (conj tpls default)))
                                          tpl# (fn [expr#] (safe-deref (case expr# ~@(interleave cases syms) ~(last syms))))]
                                     (-> (j/cell ::none :meta {:preserve-event-handlers true})
                                                     (j/set-formula! tpl# [~expr])))))

roti22:03:04

then I just use it

micha22:03:04

that looks fine

micha22:03:11

how about where you use the macro?

micha22:03:21

that's where the issue probably is

roti22:03:36

(case= ui-state
           :documents (div "Documents")
           :customers (div "Customers"))

roti22:03:45

(defc ui-state :documents)

micha22:03:55

that seems fine, too

micha22:03:10

can you paste more context please?

roti22:03:45

oh, and the error occurs when boot reloads some code due to changes

roti22:03:08

on the first page load there's no error, but also no div

roti22:03:27

1 sec, I'm preparing a small page

roti22:03:49

(page "index.html"
      (:require-macros [habarnam.core :refer [case=]]))



(defc ui-state :documents)

(html
  (head
    (html-meta :name "viewport" :content "width=device-width, initial-scale=1"))
  (body

    (case= ui-state
           :documents (div "Documents")
           :customers (div "Customers"))

    ))

roti22:03:11

and now I have the error on the first load of the page, don't know why

micha22:03:27

how about trying this:

micha22:03:09

...
(def thing (case= ...))
(cell= (prn :thing thing))
...
(html ...

micha22:03:29

that way you can see what it's doing

micha22:03:33

in the js console

roti22:03:00

#<Cell: #<Element: DIV>>

roti22:03:15

(def thing (case= ui-state
            :documents (div "Documents")
            :customers (div "Customers")))

(println thing)

roti22:03:41

so, it's a cell, not a node apparently

micha22:03:49

that's what it's supposed to be

micha22:03:53

so that's ok

micha22:03:18

the value in the cell must be a node or a sequence of nodes

micha22:03:48

which looks legit, as it's a div

roti22:03:54

(cell= (case ui-state
             :documents (div "Documents")
             :customers (div "Customers")))

roti22:03:00

this yields the same error

roti22:03:15

put directly in body

micha22:03:10

adding a wrapper div seems to fix it

micha22:03:35

there must be a bug in how the <body> is handled in hoplon.core

micha22:03:59

if you put the case= in a div

micha22:03:04

then it will work

roti22:03:41

yes, indeed. and it also gets updated properly by boot

roti22:03:56

that's great, thanks

roti22:03:41

i also just realized that I can put nodes in cells, and it works just like case=

roti22:03:40

case= seems to be just sugar, nothing more

micha22:03:44

it's doing an important thing

micha22:03:52

that putting the elements in a cell won't do

micha22:03:11

it caches the elements, even though they're created on demand

micha22:03:16

that's extremely important

micha22:03:30

because otherwise you will need to manage memory manually

micha22:03:36

or you will have memory leaks

micha22:03:00

if you put case inside a cell it will craete new elements each time the formula is evaluated

roti22:03:22

won't the nodes get collected once they're removed from the dom?

micha22:03:24

with case= it will remove and replace elements that are constructed only once

micha22:03:34

it depends

micha22:03:54

if there are no event handlers that refer to things in their closures then yse

micha22:03:03

but that's only the case for very simple elements

micha22:03:16

once you add event handlers and stuff it gets complicated

roti22:03:37

I wonder how angular deals with this

micha22:03:43

and being able to add event handlers from inside an abstraction is key

micha22:03:45

they don't

micha22:03:00

you just don't have very much power to make abstractions

micha22:03:04

it's the same with react

micha22:03:20

with react they solve the problem by passing state down through all the elements

micha22:03:27

and the "components" are pure functions

micha22:03:40

but that means that you can't encapsulate behavior really

micha22:03:48

because all elements have access to all state

micha22:03:10

if you do have any kind of local state or anyhting like that you need to explicitly manage the memory yourself

micha22:03:16

via lifecycle hooks

micha22:03:27

that's awful because it breaks abstractions

micha22:03:49

with hoplon you can safely encapsulate behavior in layered abstractions

micha22:03:12

but to do that you need to leverage static allocation

micha22:03:38

luckily, you can do all the tricky bits of figuring that out at the lower levels of your abstraction

micha22:03:46

so the application code doesn't need to deal with it

micha22:03:56

that's very powerful

micha22:03:49

the overall philosophy of hoplon is the layered approach

micha22:03:02

you create layers of abstraction

micha22:03:11

the actuall app is simply assembling those

micha22:03:19

using a few combining forms

micha22:03:27

like loop-tpl and case= etc

micha22:03:37

but your app ui should look like html

micha22:03:05

just with elements that are suited to your application

roti22:03:04

i'd love to elaborate on this, but I don't have much time atm. I confess I was drawn to hoplon by the (apparent) possibility to have your code quite clean and focused on the problem, with the details hidden (or abstracted). I experimented with om and quiescent a little bit and hit some walls which I didn't like.

roti22:03:34

I'm saying apparent because I am still discovering things simple_smile

roti22:03:18

components also promise the layered approach you mention

micha22:03:32

not really though

micha22:03:37

react components i mean

micha22:03:54

because you have to pass all the state down through the root

micha22:03:15

that means that you end up with all kinds of coupling, because essentially you can't have anonymous anything anymore

roti22:03:26

doesn't reagent offer local state? or smth similar?

micha22:03:26

everything needs to have a named place in the state atom

micha22:03:52

like look at the :toggle attribute in hoplon

micha22:03:54

for example

micha22:03:01

that's 1 line of code in hoplon

micha22:03:21

and once you define that do! multimethod dispatch, you can use the :toggle attribute on any element

micha22:03:39

and you can also have the value for the :toggle attribute be an anonymous cell

micha22:03:50

how would you do that in a react based thing?

micha23:03:14

you would need to have a place in the big atom that holds the state of that element

micha23:03:19

and whether it's visible or not

micha23:03:27

but that place needs to have a name

micha23:03:45

that means that you can't really have a higher order function do it

micha23:03:01

because by definition higher order functions deal with anonymous things

micha23:03:39

there is local state in react things

micha23:03:53

but if you use it you need to manage memory now, and use all the lifecycle hooks

micha23:03:00

IWillMount IDidMount etc

micha23:03:36

and how do lifecycle hooks compose?

micha23:03:40

they don't at all

micha23:03:57

if you want them to compose you will soon need IBeforeWillMount

micha23:03:04

and IAfterDidMount

micha23:03:12

and then IBeforeBeforeWillMount

micha23:03:23

with hoplon you just program normally

micha23:03:30

each expression is evaluated

micha23:03:44

if you want something to happen before another thing you put that expression before it

micha23:03:52

like a normal program, and like html itself

roti23:03:35

yes, the component composition is one of things I ran into. I tried to make a layout component, which needed to insert styles into it's children

roti23:03:15

when I first saw the idea to have a big atom, I didn't find it bad at first. it's only when I needed to mix by "business" state with the "view" state that I stepped a little bit back

micha23:03:43

exactly, you want lower levels of abstraction to handle things and encapsulate them

roti23:03:48

I didn't get far, but I think animations & such are difficult because of this as well

micha23:03:58

not throw everything all together into a global namespace (the big atom)

micha23:03:17

basically the big atom is reinventing scopes in lisp

micha23:03:48

it takes all the glorious inventions of lisp, lexical scope, dynamic extent, closures, etc

micha23:03:52

and throws it away

micha23:03:01

just because you can add a watch to an atom

micha23:03:28

javelin solves the problem by creating a new reference type that has the same consistency guarantees as the big atom

micha23:03:43

but as a first-class construct

roti23:03:13

well, I wouldn't be so radical. you can hide the big atom, and expose parts of it (e.g. subatom), so you get the illusion you are local

micha23:03:21

not really though

micha23:03:37

you can't use the highly developed tools of lisp anymore

roti23:03:44

but I agree, hoplon looks better here

micha23:03:45

you need to make things out of coconuts

micha23:03:50

like a coconut radio

micha23:03:09

like cursors and whatnot

micha23:03:23

a terrible alternative to closures, imho

micha23:03:49

the fact that everything in the big atom needs to be named is the worst part

micha23:03:02

because we use anonymous thing factories all the time in lisp

micha23:03:08

it's the most powerful part of lisp really

micha23:03:24

the abitlity to make a factor that creates anonymous things

micha23:03:29

that can be fed into other factories

micha23:03:37

that's the only way to make layerd abstractions

micha23:03:49

once you need to coordinate around names you can't have layers like that anymore

micha23:03:59

because they all need to coordinate their names for things

micha23:03:13

so at the end of the long story, hoplon embraces the mutable dom

micha23:03:30

when you make a dom element that's the real element, and not a virtual element

micha23:03:41

but that means also that you can then pass it to things

micha23:03:54

like have a function that decorates the element with a handler or something

micha23:03:12

without the rest of the application needing to know or care about it

micha23:03:51

the downside is that you need to leverage static allocation as much as possible

roti23:03:40

btw, can hoplon control the rendering like do 30fps? I think it should be possible...

micha23:03:56

yeah, i think that's another confusion in react

micha23:03:05

react should not be doing animation

micha23:03:17

you use a highly optimized animation api for that

micha23:03:22

for exam[ple

micha23:03:32

we made a little game, it was like missile command

micha23:03:40

you know, the old atari game

micha23:03:13

missiles come down from the top of the screen toward your cities and you need to fire missiles at them to blow them up before they land

micha23:03:40

we easily achieved 30fps with hundreds of missiles in flight

micha23:03:48

that's like 16 ms per frame

micha23:03:01

how did we do all those calculations in 16ms?

micha23:03:05

we didn't simple_smile

micha23:03:17

we used an animation library

micha23:03:37

our actual processing loop was running at around 100ms intervals

micha23:03:56

each time it ran it would update the course and speed of the missiles

micha23:03:05

and schedule explosions etc

micha23:03:24

you can do interpolation in between the "waypoints" even

micha23:03:29

we just did linear

micha23:03:48

a human being can't tell that we were updating the course and speed of the missiles at 100ms intervals

micha23:03:59

they just saw smoothly flying missile tracks

micha23:03:26

basically the animation was done in hardware

roti23:03:54

so you mean, your computation was slower than the rendering, and the animation library calculated the points in between?

micha23:03:24

we were just using the svg animations i think

micha23:03:44

but there are a number of highly tuned animation libraries that will choose the most efficient implrementation for the platform

micha23:03:11

like sometimes css animations are the fastest, sometimes svg, and sometimes js

micha23:03:39

but i think that's the way to think about the problem

micha23:03:53

separate the concerns of animation housekeeping and application state

micha23:03:12

hoplon makes it very easy to achieve that separation

micha23:03:16

because there is no virtual dom

roti23:03:05

I have to say, this sounds weird to me (processing slower than animation), but then again I had little contact to graphics so far

micha23:03:28

what's the point of updating the application state faster than a human can percieve it?

micha23:03:42

like anything under 100ms is wasted as far as humans go

roti23:03:58

well, faster no, of course not

micha23:03:02

as long as the animations are smooth, of course

roti23:03:58

anyway, thanks a lot for the help. my stuff is working now, I just added the div

micha23:03:11

awesome, i'll have a look and see what the bug is

micha23:03:20

hopefully it's something i can fix easily