-
Notifications
You must be signed in to change notification settings - Fork 64
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[WIP] Enormous rework of (mostly frontend) code. #211
Conversation
I wrote some Blink stuff but it's not quite working; when a WebIO Node is rendered via update: it's |
Great work! |
This is pretty awesome! I'll review it soon. Thanks!! |
@SimonDanisch The Julia changes are relatively minor and aren't compatible with the previous version of the frontend code, unfortunately, so I don't think it makes much sense to separate them. I'm also gonna fix up Blink then work on a JupyterLab extension. I'd like to also make a notebook extension which would increase stability of WebIO across page reloads but the current stuff I have implemented should (need to confirm!) work more-or-less as it does now (with the same level of flakiness-on-reload). |
Thanks a lot! This is a lot of work. I'm a bit confused as there are in simultaneous this PR and #208 which both seems to be ways to get WebIO to work more reliably: are the two PRs orthogonal or does this one make #208 unnecessary? I'd tend to agree with Simon that this is very hard to review, but I can definitely check out the PR and see if all the various Interact interfaces still work with this. Are the tests failing only because or Blink? Fixing the page reload stuff is definitely very important, there were issues about it in Interact as well (see JuliaGizmos/Interact.jl#250), especially since this is a regression compared to the pre-WebIO implementation of Interact. |
Also pending this IJulia pull request. |
I haven't looked too hard at #208 , but my first impression is that this pull request would make it unnecessary.
I'm not sure why blink is failing on Travis (I'll look into it more in a bit) because it works for me™. I haven't really started the Jupyter Notebook integration yet, but I think that should be just a minor change (I think just having And thanks for the support 😂. |
I'm actually convinced that Blink isn't working as expected, at least when it comes to headless/containerized environments (which includes Travis). I tried (using the official Pkg.add("Blink")
using Blink
Blink.AtomShell.install()
w = Window(Dict(:show => false)) and got
Probably related to this Blink.jl issue. |
Ah, okay, I figured out all of my woes with Blink.jl (it's definitely not Blink's fault, no matter how much I wanted it to be...). The issue is that it's passing on my machine™ because I have my own modified (as described above) version of JSExpr. It seems kind of weird that testing WebIO requires JSExpr considering how JSExpr depends on WebIO, so unless anyone has enormous objections, I'm going to remove the JSExpr testing dependency. This should also make it easier to update WebIO and JSExpr independently in the future. |
Make sure to test Interact widgets before merging this: I see a lot of regressions. In Blink I can't show simple sliders: julia> using Blink
julia> using Interact
julia> w = Window();
julia> body!(w, slider(1:100)) console error:
whereas in Jupyter notebook the |
@piever Whoops, my intention was not to suggest that this was really ready for testing (but it's getting closer and closer!). My only remaining concern is... is rendering observables that contain DOM nodes a necessity (also ping @shashi and @SimonDanisch on this)? It's kind of hard to do this without a single WebIO global which is one of the reasons I made this PR. I see that Interact does this - I'm not sure if it's on purpose or not. |
If you want to "replace a DOM node" dynamically in your app, the simplest solution is to put it in |
Yeah, that's kind of what I thought (/was afraid) you were going to say. So then I think the best attack strategy might be to have Observables be Nodes (in terms of the way that things are presented to the frontend code when So say you have this Julia code. w = Scope()
counter = Observable(w, "counter", 0)
w(dom"div"(
counter,
dom"button"(
"increment"
; events=Dict("click" => (@js function() $counter[] = $counter[] + 1 end))
)
)) Currently, this is "transformed" into something like this node representation.
What happens then is that when an observable appears as a child of some node, it's set up with its own scope whose dom node is the html representation of that observable. This is tricky because then the HTML representation will be injected and will then try to use the WebIO global, which is bad news bears. So maybe we could have an Observable be rendered as a direct child of a node (without forcing it to be enclosed within its own scope). This is the main point of what I'm trying to say in this long rambling comment. The only issue I could foresee with this is that there would be issues if the observable rendered is not countained within the closest-ancestor scope. This is already an issue (on the current master of WebIO) if you try to use an observable within javascript while inside of a scope that doesn't contain that observable, and the simplest solution is to just attach all your observables to the same scope. However, what's not currently an issue (and what might become an issue) is that if you just render an observable as the child of a node, it's placed within it's own isolated scope that doesn't depend on whether-or-not that observable is within the ancestor scope. So, question for @shashi (or anyone else who has thoughts/question):
It seems like the "fix" would have to be something involving recursively finding observables which might not be that big of a deal... function find_observables(node::Node)::Vector{Observable}
observables = Vector{Observable}()
for child in node.children
if isa(child, Observable)
push!(observables, child)
elseif isa(child, Node)
append!(observables, find_observables(child)
end
end
end then attach all of the "found" observables to the scope when So, assuming the above is not an issue or is taken care of at the very least, this is what I think would be the "optimal" solution. w = Scope()
nodes = [dom"div"("hello"), dom"div"("goodbye")]
nodeobs = Observable(w, "node", rand(nodes))
w(dom"div"(
dom"button"(
"change the node"
; events=Dict("onclick" => (@js ...))
),
nodeobs
) would generate the following "node tree":
(whereas before, the tldrI hope the above made sense. Current IssueWhen an observable that contains a node is rendered, that observable then contains the big Proposed "Fix"Modify the observable to contain a JSON representation of the node and don't wrap it in it's own scope. AddendumWe'd need to, I think, modify every rendered observable to become an observable of a node. For example, w = Scope()
counter = Observable(w, "counter", 0)
WebIO.render(counter) would then render a scope ( map!(x -> render_as_node(x), rendered_observable, counter) |
This is of course the easiest and most natural way to implement this. What we could do (I think this is what you suggest) is to make the underlying implementation morally better than
I don't think this is the case... Every observable is identified by the (scope id, observable name) pair (see
We already do this right? |
packages/webio/src/DomNode.ts
Outdated
class WebIODomNode extends WebIONode { | ||
readonly element: WebIODomElement; | ||
children: Array<WebIONode | string>; | ||
private eventListeners: {[eventType: string]: EventListenerOrEventListenerObject | undefined} = {}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have you seen microsoft/TypeScript#21922 ?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also any reason we need to keep these in the struct here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm not sure what exactly you're referencing with the TypeScript issue. Do you just mean that we should use EventListener
rather than EventLIstenerOrEventListenerObject
?
And are you asking why we need to keep the event listeners in the object? I think it's so that we can .removeEventListener
them if they change.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I looked up that type but it seems it got removed in Feb? We should probably use whatever alternative if it works.
I think it's so that we can .removeEventListener them if they change.
Ok I see, but I don't think we have a way of doing that (at least from Julia).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🤷♀️ I just copied that functionality from the current/master version of the frontend code. I changed the type to EventListener
(will push soonish).
packages/webio/src/Node.ts
Outdated
export const enum WebIONodeType { | ||
DOM = "DOM", | ||
SCOPE = "Scope", | ||
IFRAME = "IFrame", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why do we need IFrame
here? I think there was an example of a different package implementing its own NodeType
. and extending WebIO.NodeTypes[Foo] = {create:...}
, I don't know if it still does that, but hypothetically someone could want to do that. Why constrain this?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Well the original reason was because iframes just have some different considerations than regular DOM nodes. In particular, I moved all of the iframe setup code out of iframe.jl
and into the frontend.
I can modify things so that the construction of nodes is extensible though.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I figured out what you did, it looks good.
Yeah, would be great to make nodes extensible...
After that push ( |
A couple of feedback from the Interact side:
tabulator(OrderedDict("a" => 1, "b" => 2, index = 1) Now when clicking on different buttons the value on screen (1 or 2) doesn't get updated.
There are some strange issues when rerendering widgets. If I start with: using Blink
w = Window()
v = Observable(20)
s = slider(1:100; value = v)
body!(w, s) the slider marks body!(w, s) the displayed value goes back to |
Strange. Do you mean you're calling What happens if you do |
Same effect, it still re-renders to the original value: no idea how that's possible... |
I think that's a Julia side fix. The _setup_scope command should cause Julia to send the current value of all the observables within that scope to the client. |
@piever |
Is it something that I should fix on the Interact side or there is a way to fix it from here? |
I looked into it a little bit more and it seems(?) that it's happening because you have an observable that contains a scope. I can continue looking into it a bit later today I think. |
It's kind of crucial that this works though, because every Interact widget is its own scope and it's quite common to put them inside |
@piever Try now. There were two factors that were inhibiting things from working.
The brilliant and wonderful @shashi pointed the latter out to me a while ago and, as the devoted contributor I strive to be, I promptly forgot. |
Thanks! That fixes the example above, I think more complex cases are still broken, will try to come up with a MWE soon. |
Current status:
|
…etup_scope after imports are done.
EDIT: I've edited below as I found a much simpler MWE... I still see some issues with Interact in more complex scenarios. julia> using Blink, Interact
julia> w = Window()
julia> ui = tabulator(OrderedDict("a" => button(), "b" => button()));
julia> body!(w, ui) Basically here I'm doing a tabulator whose items are also widgets (each has its own scope) and somehow it seems that both are displayed one on top of another instead. If that can be of any help, |
As a double check, I would also try the more complex (depending on TableWidgets): julia> using Blink, Interact, TableWidgets, DataFrames
julia> df = DataFrame(x = 1:10);
julia> w = Window()
julia> wdg1 = addfilter(df);
julia> wdg2 = dataeditor(df);
julia> ui = tabulator(OrderedDict("a" => wdg1, "b" => wdg2));
julia> body!(w, ui) before merging as it seems to fail in a slightly different way than the buttons (here both options are always displayed one on top of the other - with the buttons instead if I click on "b" the first button disappears as it should). |
@piever I think it works for me now. The issue was BIZARRE; it's not that important but... I was looking at the // Code operated under assumption that this was a string, not array
const importsLoadedSource = ["function() { ... }"];
importsLoaded = eval(`(${importsLoadedSource})`);
importsLoaded(dependencies); which caused all kinds of weird nondeterminism that I admit I do not understand. |
Great job! It seems like even pretty complex GUIs work reliably now (actually, more reliably than they used to...). Maybe this can be merged soonish and the Jupyter plugin happen in a separate PR? The diff already looks massive... |
🎉 👏 🍰 🍻 |
@shashi
I did a lot of stuff.
I tested this with Mux (and only implemented a Mux provider) and want to do more stuff with better integration with IJulia but I think this represents a better way of doing things as far as client side code is concerned.
Questions/comments/concerns appreciated. I'd really like to get something like this merged.
My own notes:
node.querySelector('*[data-webio-scope="scope-id"]')
).WebIO.render
pathways of the Julia code and ended up introducing some stuff that I'm personally not fond of and would want to refactor (e.g.renderScopePreferJSON
). The reason this is so ugly right now is because I was just having a hard time figuring out the complex ways in which render is recursively called (and I wanted to get rid of <script> tags that render things that then have <script> tags...)</script>
simply becomes escaping all/
characters (which is allowed since all/
characters appear inside of strings and\/
is interpereted as just/
).This will also require a very slight modification to JSExpr to not use a global WebIO instance. I modified JSExpr locally in two places and everything just worked™:
(
_webIOScope
is defined in the context where the function is evaluated).When done, will close #203, #8. Supersedes #201.
JupyterLab plugin (which I'm more than happy to write/maintain) will close #139.
Again, I'm very open to comments and opinions and changing anything.