inesita-rb / inesita

Frontend web application framework in Ruby using Opal.
https://inesita.fazibear.me/
MIT License
259 stars 15 forks source link

Create Render hook/unhook Examples (e.g. after_render) #31

Closed jmstacey closed 7 years ago

jmstacey commented 7 years ago

I see the after_render hook was removed about a year ago and replaced with hook and unhook in VirtualDOM, but I can't reason out how to use this for my use case. Examples or alternative solutions would be helpful.

In my case I'm using the nestable2 JQuery plugin that of course directly manipulates the DOM. I need the virtual DOM diff to be applied before I mount the JQuery plugin to the element. Right now I have it all working except for overcoming the first DOM load--so I have to click on the same nav link 3 times which for reasons I haven't figured out allow nestable to then hook on to its target element and start working.

This surprisingly works combined with the store including versioning, but I haven't been able to overcome the first page load issue.

def render
  Element['#outline'].nestable('destroy') # Deactivate plugin so it can be rebuilt [since the plugin only has an append method]

  div class: "dd", id: "outline"

  puts store.app.state.visible_data

  # Rebuild the outline based on the sate [in the event that there's a redo/undo operation 
  Element['#outline'].nestable({ 'json': store.app.state.visible_data, 'callback': -> { on_drop(l, e) } }.to_n)
end

Grateful for any tips and examples.

jmstacey commented 7 years ago

No joy after digging through the stack. I don't fully understand the virtualdom internals so it's possible I'm missing something. While I could get a vdom hook to fire, it would be called right after the render to the virtual dom. What I need is a hook right after the virtual dom has been patched.

I brought back this bit of code in PR https://github.com/inesita-rb/inesita/pull/32 and it seems to be working in the sample below:

def before_render
  Element['#outline'].nestable('destroy') 
  # Deactivate nestable plugin so it can be rebuilt from the state store
end

def after_render
  Element['#outline'].nestable({ 'json': store.app.state.visible_data, 'callback': -> { on_dd_change(l, e) } }.to_n)
  # Todo (caution): binding persists across route changes and may break other component trees
end

def render
  # Works w/ restored after_render hooks in https://github.com/inesita-rb/inesita/pull/32
  div class: "dd", id: "outline"

  # Does not work. #after_render is called after this render block but before vdom changes are applied to the real DOM
  div class: "dd", id: "outline", hook: hook(:after_render)
end

If there's a better way to pull in JQuery plugins that directly change the real DOM I'm all ears.

fazibear commented 7 years ago

Hi, You're close. Hook callbacks give you a node reference. Instead of querying node with the Element, you should use this reference.

Here is an example: https://github.com/fazibear/web-audio-playground/blob/master/app/layout.rb#L11

fazibear commented 7 years ago

Also, hook usage should be included in the documentation.

jmstacey commented 7 years ago

I'm still stumped. The web-audio-playground example isn't doing anything with the node reference in the vdom callback so all changes are happening natively from the Inesita render.

In the case of this jQuery nestable2 plugin it's expecting a DOM node, not vdom. Are you saying there's some way to hook this up so it'll work like that?

Other frameworks such as React and Vue introduce lifecycle events like onComponentDidMount and componentWillUnmount where they inject the JQuery plugin initialize.

Any suggestions?

Here's the full source I have for reference: https://github.com/jmstacey/dnd_list/blob/master/app/components/outline.rb#L23-L33 If you hack at that remember to use an alternate to #after_render, like #after_render_tmp, since the former will be called by the modifications in the PR.

fazibear commented 7 years ago

In web-audio-playground example hook is used to draw the line between two dom elements. To get offset or width properties vdom elements must be attached to dom.

You can try:

def after_render(node)
  # Element[node] or just node
  Element[node].nestable({ 'json': store.app.state.visible_data, 'callback': -> { on_dd_change(l, e) } }.to_n) 
end

div class: "dd", id: "outline", hook: hook(:after_render)

Or try to hook outer element and select inner nodes with jqeury`

I'm wondering why you try to mix vdom with jquery. It's really hard to deal with it.

jmstacey commented 7 years ago

I'm wondering why you try to mix vdom with jquery. It's really hard to deal with it.

Indeed, and it feels wrong. Upside is that a lot of existing code that works today can be reused without rewriting into native components. More complex examples could be charting libraries, maps, and so forth.

It's a challenge with the other frameworks as well. Here are a few examples how some of them handled this--common theme is the idea of mount and unmount hooks to create sections of DOM that aren't managed by the virtual dom. clearwater blackbox nodes Wrapping jQuery with React Vue.js virtualDOM mixing with jQuery plugin

I didn't have any luck with Element[node]., or node.nestable(. . . since it's triggered in the wrong context [nestable manages the dom]. In comparison to the playground example, that's a native component implementation and the DOM is entirely managed by Inesita and the vdom.

Perhaps modify the after_render PR to follow a similar convention with #on_mount and #will_unmount?

fazibear commented 7 years ago

Hook gives you a reference to given node, just after mount.

Here is the example:

  def after_render(node)
    Element[node.to_n].nestable({ 'json': store.app.state.visible_data, 'callback': -> { on_dd_change(l, e) } }.to_n) # #to_n converts to native javascript. Provided by opal-jquery
    # Todo (caution): nestable binding is on real DOM and is likely to break on route changes. Consider a "terminate" equivalent to unload on other pages.
  end

  def render
    div class: "dd", id: "outline", hook: hook(:after_render)
    ..
  end