Skip to content
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

React4s/Materialize Modal Popup not Updating as Expected #19

Closed
abrighton opened this issue Feb 25, 2019 · 5 comments
Closed

React4s/Materialize Modal Popup not Updating as Expected #19

abrighton opened this issue Feb 25, 2019 · 5 comments

Comments

@abrighton
Copy link

I have a React4s/materialize modal popup where you choose an item from a <select> menu in the dialog and that should update the layout below depending on the selection. However, even though the state variable is updated and render is called as expected, the changes are not propagated to the display. Is there anything special in React or React4s (or materialize) about modal popups that would prevent them from being updated once displayed?

The basic render method looks like this:

  override def render(get: Get): Node = {
    val trigger = E.a(A.className("modal-trigger"), A.href(s"#$id"), Text("Load"))
    val body = E.div(
      A.id(id),
      A.className("modal modal-fixed-footer"),
      E.div(A.className("model-content"), makeDialogBody(get)),
      E.div(A.className("modal-footer"), makeButtons(get))
    )
    E.li(trigger, body)
  }

And I have verified that the items are being added correctly in the methods called. Its just that they don't show up if changed as a result of an event. The items only show up if created that way the first time. It seems like the layout won't change, once displayed.

@Ahnfelt
Copy link
Owner

Ahnfelt commented Feb 25, 2019

There's nothing special about modal popups in React4s. The render method looks OK to me. Can you post the code for the whole component, including the State variables and makeDialogBody/makeButtons?

@abrighton
Copy link
Author

Here is a simplified version, without external dependencies that shows the problem.

package csw.eventmon.client.test
import com.github.ahnfelt.react4s._
import csw.eventmon.client.test.TestComponent._

object TestComponent {
  private val id = "testConfig"
  val localStorageKey = "test"

  sealed trait LoadType {
    val displayName: String
  }
  case object LoadFromLocalStorage extends LoadType {
    override val displayName = "Load from Local Storage"
  }
  case object LoadFromFile extends LoadType {
    override val displayName = "Load from File"
  }
  case object LoadFromConfigService extends LoadType {
    override val displayName = "Load from Config Service"
  }
  val loadTypes: List[LoadType] = List(LoadFromLocalStorage, LoadFromFile, LoadFromConfigService)
  case class LoadSettings(name: String, loadType: LoadType)
}

// A modal dialog for adding events to subscribe to
case class TestComponent() extends Component[LoadSettings] {
  private val selectedName     = State("")
  private val selectedLoadType = State[Option[LoadType]](None)
  private val savedConfigs     = State[Map[String, Set[String]]](Map.empty)

  private def makeNameItem(get: Get): Element = {
    val map           = get(savedConfigs)
    val maybeLoadType = get(selectedLoadType)
    if (map.isEmpty || maybeLoadType.isEmpty) {
      E.div()
    } else {
      val names    = map.keySet.toList
      val loadType = maybeLoadType.get
      val items    = names.map(name => E.option(A.value(name), Text(name)))
      println(s"XXX makeNameItem: names = $names")
      E.div(
        A.className("row"),
        E.div(A.className("input-field col s6"), E.select(A.onChangeText(nameSelected), A.value(names.head), Tags(items)))
      )
    }
  }

  private def nameSelected(name: String): Unit = {
    selectedName.set(name)
  }

  private def loadTypeSelected(loadTypeStr: String): Unit = {
    val maybeLoadType = loadTypes.find(_.displayName == loadTypeStr)
    selectedLoadType.set(maybeLoadType)
    loadSavedConfigsForLoadType(maybeLoadType)
  }

  private def loadFromLocalStorage(): Map[String, Set[String]] = {
//    import upickle.default._
//    LocalStorage(localStorageKey) match {
//      case Some(json) => read[Map[String, Set[String]]](json)
//      case None       => Map.empty
//    }
    Map(
      "name1" -> Set("A", "B", "C"),
      "name2" -> Set("D", "E"))
  }

  private def loadSavedConfigsForLoadType(maybeLoadType: Option[LoadType]): Unit = {
    val map = maybeLoadType match {
      case Some(loadType) =>
        loadType match {
          case LoadFromLocalStorage => loadFromLocalStorage()
          // XXX TODO
          case x => Map.empty[String, Set[String]]
        }
      case None => Map.empty[String, Set[String]]
    }
    println(s"XXX set savedConfigs to $map")
    savedConfigs.set(map)
  }

  private def makeLoadtypeItem(): Element = {
    val defaultItem =
      E.option(A.value("-"), A.disabled(), Text("Load from ..."))
    val items = defaultItem :: loadTypes.map(t => E.option(A.value(t.displayName), Text(t.displayName)))
    E.div(A.className("row"),
      E.div(A.className("input-field col s6"), E.select(A.onChangeText(loadTypeSelected), A.value("-"), Tags(items))))
  }

  private def makeButtons(get: Get): Element = {
    val disabled = get(savedConfigs).isEmpty.toString
    E.div(
      A.className("modal-footer"),
      E.a(A.href("#!"), A.className("modal-close waves-effect waves-green btn-flat"), Text("Cancel")),
      E.a(A.href("#!"),
        A.className("modal-close waves-effect waves-green btn-flat"),
        A.disabled(disabled),
        A.onClick(okButtonClicked(get)),
        Text("OK"))
    )
  }

  private def okButtonClicked(get: Get)(ev: MouseEvent): Unit = {
    val name     = get(selectedName)
    val loadType = get(selectedLoadType)
    if (name.nonEmpty && loadType.nonEmpty)
      emit(LoadSettings(name, loadType.get))
    else {
      // XXX TODO: display error message
      val msg = if (name.isEmpty || name.equals("-")) "Please choose a name to load" else "Please select where to load from"
      println(msg)
    }
  }

  private def makeDialogBody(get: Get): Element = {
    E.div(makeLoadtypeItem(), makeNameItem(get))
  }

  override def render(get: Get): Node = {
    val trigger = E.a(A.className("modal-trigger"), A.href(s"#$id"), Text("Test"))
    val body = E.div(
      A.id(id),
      A.className("modal modal-fixed-footer"),
      E.div(A.className("model-content"), makeDialogBody(get)),
      E.div(A.className("modal-footer"), makeButtons(get))
    )
    E.li(body, trigger)
  }
}

The class is used in a navbar like this:

package csw.eventmon.client
import com.github.ahnfelt.react4s._
import Navbar._
import csw.eventmon.client.test.TestComponent

case class Navbar() extends Component[NavbarCommand] {

  override def render(get: Get): Node = {
    val navbar = E.nav(
      E.div(
        A.className("nav-wrapper teal lighten-2"),
        E.a(A.href("#!"), A.className("brand-logo"), Text("Event Monitor")),
        E.ul(
          A.className("right"),
          Component(TestComponent)
        )
      )
    )

    navbar
  }
}

If you click on the Test button in the navbar and then select "Load from Local Storage" from the menu, you can see in the browser console that some name elements were generated in makeNameItem(), but they don't show up in the display. The initial, empty div is still displayed.

@Ahnfelt
Copy link
Owner

Ahnfelt commented Feb 25, 2019

When I choose "Load from Local Storage", a new <select> appears with the options "name1" and "name2". Is this not the expected behavior?

image

@abrighton
Copy link
Author

Yes, that is the expected behavior, however I am not seeing it.
I just did a test and could see that if I comment out all the Materialize css related parts from the HTML file, it also works as expected, so I guess this has something to do with materialize.
I'm using this to generate the HTML via the sbt-web plugin:

@(title: String)(content: Html)

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8">
        <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/css/materialize.min.css">
        <link rel="stylesheet" href="/assets/css/layout.css">
        <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
        <link rel="shortcut icon" href="/assets/images/favicon.ico">
        <title>@title</title>
    </head>
    <body>
        <div id="main"></div>
        <script src="https://cdnjs.cloudflare.com/ajax/libs/materialize/1.0.0-rc.2/js/materialize.min.js"></script>
        <script src="/assets/js/materializeInit.js"></script>
        @content
        @scalajs.html.scripts("csw-event-monitor-client", name => s"/assets/$name", name => getClass.getResource(s"/public/$name") != null)
        <script src="/assets/js/chartSettings.js"></script>
    </body>
</html>

@abrighton
Copy link
Author

It looks like you need to call some Materialize JavaScript code after dynamically updating a select element. Something like:

M.FormSelect.init(document.getElementById(selectId))

So this is not a React4s issue. Thanks for pointing me in the right direction.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants