rubyreactjsruby-on-rails-3opalrbhyperstack

Hyperstack and MaterialUI Drawer Toggling State is causing the drawer to open and close repeatedly


I am implementing a title bar and menu drawer using MaterialUI in a Hyperstack project. I have two components, a Header component, and a Menu component. The Menu component is the expandable Drawer. I am storing the state in the Header component and passing it and a handler to the Menu component to toggle the drawer state when the drawers close button is clicked. For some reason, the drawer is just toggling open and closed very rapidly and repeatedly.

The drawer was opening fine before I implemented the close button. I have tried moving the state up to the main app component and passing it all the way down, but it produces the same result. I tried setting a class function on the Header component and calling it from within the Menu component instead of passing in an event handler.

The Header component

class Header < HyperComponent
  before_mount do
    @drawer_open = false
  end

  def toggle_drawer
    mutate @drawer_open = !@drawer_open
  end

  render(DIV) do
    AppBar(position: 'static', class: 'appBar') do
      Toolbar do
        IconButton(class: 'menuButton', color: 'inherit', aria_label: 'Menu') do
          MenuIcon(class: 'icon')
        end
        .on(:click) do
          toggle_drawer
        end
        Typography(variant: 'h6', color: 'inherit', class: 'grow') { 'Admin Main' }
        Button(color: 'inherit', class: 'float-right') { 'Login' } # unless App.history != '/admin'
      end
    end
    Menu(open_drawer: @drawer_open, toggle_drawer: toggle_drawer)
  end
end

The Menu component

class Menu < HyperComponent
  param :open_drawer
  param :toggle_drawer

  def menu_items
    %w(Main Inventory Customers)
  end

  def is_open?
    @OpenDrawer
  end

  render(DIV) do
    Drawer(className: 'drawer, drawerPaper', variant: 'persistent', anchor: 'left', open: is_open?) do
      IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
      .on(:click) { @ToggleDrawer }

      List do
        menu_items.each do |mi|
          ListItem(button: true, key: mi) { ListItemText(primary: mi) }
        end
      end
    end
  end
end

I expected for the drawer to open on the open button click and close when the close button is clicked, but it is just opening and closing very rapidly.


Solution

  • The reason its opening and closing rapidly is that you are passing the value of toggle_drawer from the Header component to the Menu component. Each time you call toggle_drawer it changes the state variable @drawer_open, and rerenders the component, and then lather-rinse-repeat.

    What you need to do is pass a proc to Menu, and then let Menu call the proc in the on_click handler.

    So it would look like this:

    class Header < HyperComponent
     ...
     render(DIV) do
       ...
       Menu(open_drawer: @drawer_open, toggle_drawer: method(:toggle_drawer))
     end
    end
    

    and

    class Menu < HyperComponent
      ...
      param :toggle_drawer
      ...
          IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
          .on(:click) { @ToggleDrawer.call } # note you have to say .call
      ...
    end
    

    By the way nice article here on how method(:toggle_drawer) works and compares it the same behavior in Javascript.

    But wait! Hyperstack has some nice syntactic sugar to make this more readable.

    Instead of declaring toggle_drawer as a normal param, you should declare it with the fires method, indicating you are going to fire an event (or callback) to the calling component. This not only will make you life a little easier, but will also announce to the reader your intentions.

    class Menu < HyperComponent
      ...
      fires :toggle_drawer # toggle_drawer is a callback/event that we will fire!
      ...
          IconButton(className: 'drawerHeader') { ChevronLeftIcon() }
          .on(:click) { toggle_drawer! } # fire the toggle_drawer event (note the !) 
      ...
    end
    

    now Header can use the normal event handler syntax:

    class Header < HyperComponent
     ...
     render(DIV) do
       ...
       Menu(open_drawer: @drawer_open)
       .on(:toggle_drawer) { toggle_drawer }
     end
    end 
    

    BTW if I could give a little style advice: Since the Menu can only close the drawer that is what I would call the event, and in the event handler I would just directly mutate the drawer state (and just lose the toggle_drawer method).

    This way reading the code is very clear what state you are transitioning to.

    The resulting code would look like this:

    class Header < HyperComponent
      before_mount do
        @drawer_open = false  # fyi you don't need this, but its also not bad practice
      end
    
      render(DIV) do
        AppBar(position: 'static', class: 'appBar') do
          Toolbar do
            IconButton(class: 'menuButton', color: 'inherit', aria_label: 'Menu') do
              MenuIcon(class: 'icon')
            end.on(:click) { mutate @drawer_open = true }
            Typography(variant: 'h6', color: 'inherit', class: 'grow') { 'Admin Main' }
            Button(color: 'inherit', class: 'float-right') { 'Login' } # unless App.history != '/admin'
          end
        end
        Menu(open_drawer: @drawer_open)
        .on(:close_drawer) { mutate @drawer_open = false }
      end
    end