Disable some radio buttons dynamically?

122 views
Skip to first unread message

Barry Kaplan

unread,
Jul 18, 2011, 6:22:04 AM7/18/11
to lif...@googlegroups.com
I have the following ...

  def worktimeIntervalSelector =
    "*" #> SHtml.ajaxRadio[Worktimes.Value](
      Worktimes.values.toSeq,
      Full(Worktimes.OneHour),
      worktimeTypeVar () = _
    ).toForm

... and I need to dynamically disable the individual radio buttons based on some changing state. 

I can't seem to find any example for this. Also I am using reactive-web if that can help. Any pointers would be much appreciated.

Naftoli Gugenheim

unread,
Jul 20, 2011, 4:53:58 AM7/20/11
to lif...@googlegroups.com
Hi, sorry I haven't been available lately.
If you want to take advantage of reactive-web, you could do as follows:


Template:
<span class="radio>
<label><input type="radio" name="name" /><span class="text"></span></label>

Snippet:

".radio" #> Worktimes.values.toSeq.map { v =>
  val clicks = DOMEventSource.click ->> { someAction }
  val enabled = PropertyVar("disabled")(false) <<: someDependentSignal
  (".text" #> v.toString &
  "input [value]" #> v.toString) andThen
  "input" #> (enabled andThen clicks)
}

Note that (IIRC) "input [value]" and "input" cannot be combined with &, so andThen is used instead.

someDependentSignal is a signal whose value is forwarded to the PropertyVar. If you need more help, please explain specifically what state affects the radio button's disabled state and how.

someAction is whatever code you run when it's clicked.

Of course you could write it inline if you prefer:

"input" #> ((PropertyVar("disabled")(false) <<: someDependentSignal) andThen (DOMEventSource.click ->> { someAction }))



--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/bd2zzjEQZOAJ.
To post to this group, send email to lif...@googlegroups.com.
To unsubscribe from this group, send email to liftweb+u...@googlegroups.com.
For more options, visit this group at http://groups.google.com/group/liftweb?hl=en.

Barry Kaplan

unread,
Jul 20, 2011, 1:53:39 PM7/20/11
to lif...@googlegroups.com
Perfect. Almost. Not reactive specific, but because "disabled" is a boolean property its existence, not its value, is all that matters. So what I would to do is be able to remove that property when the signal goes true. Looking at PropertyVar this does not seem possible. Maybe a "BooleanPropertyVar  extends Var[Boolean]"

BTW, I'm simply loving reactive. Thanks much!

Naftoli Gugenheim

unread,
Jul 21, 2011, 12:11:57 AM7/21/11
to lif...@googlegroups.com
I assume you mean the existence as an attribute. Indeed it does deal with this correctly. Rendering gets delegated to PropertyCodec implicitly, so see this code in PropertyVar.scala:
object PropertyCodec {
  implicit val string: PropertyCodec[String] = new PropertyCodec[String] {
    def fromString = s => s
    val toJS = ToJs.string
    def toAttributeValue = v => _ => Some(v)
  }
  implicit val int: PropertyCodec[Int] = new PropertyCodec[Int] {
    def fromString = _.toInt
    val toJS = ToJs.int(_:Int)
    def toAttributeValue = (v: Int) => _ => Some(v.toString)
  }
  implicit val intOption: PropertyCodec[Option[Int]] = new PropertyCodec[Option[Int]] {
    def fromString = _.toInt match { case -1 => None case n => Some(n) }
    val toJS = (io: Option[Int]) => ToJs.int(io getOrElse -1: Int)
    def toAttributeValue = v => _ => v.map(_.toString)
  }
  implicit val boolean: PropertyCodec[Boolean] = new PropertyCodec[Boolean] {
    def fromString = _.toLowerCase match {
      case "" | "false" | net.liftweb.util.Helpers.AsInt(0) => false
      case _ => true
    }
    def toJS = (b: Boolean) => if (b) true.$ else false.$
    def toAttributeValue = (v: Boolean) => name => if (v) Some(name) else None
  }
}

So toAttributeValue returns an Option, which is then used in DOMProperty.PropertyRenderer:

class PropertyRenderer(attributeValue: String => Option[String] = _ => None)(implicit page: Page)
    extends Renderer(this)(elem => includedEvents.foldLeft(
      elem %
        attributeValue(attributeName).map(
          new UnprefixedAttribute(name, _, Null)
        ).getOrElse(Null)
    ) { (e, es) =>
        es(e)
      }
    )(page)

So if it's None, it renders Null, an empty MetaData.


On Wed, Jul 20, 2011 at 1:53 PM, Barry Kaplan <mem...@gmail.com> wrote:
Perfect. Almost. Not reactive specific, but because "disabled" is a boolean property its existence, not its value, is all that matters. So what I would to do is be able to remove that property when the signal goes true. Looking at PropertyVar this does not seem possible. Maybe a "BooleanPropertyVar  extends Var[Boolean]"

BTW, I'm simply loving reactive. Thanks much!

--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/2PPjzEEZryQJ.

Barry Kaplan

unread,
Jul 21, 2011, 12:30:55 PM7/21/11
to lif...@googlegroups.com
So just could not my test to pass. The Jim in another thread suggested (https://groups.google.com/forum/#!starred/liftweb/u7COkDrDuJ4):

def selector = {
    "disabled=disabled [disabled]" #> (None:Option[String])
}


And that did the trick!

So this now works perfectly.

  ...
  val disabled = PropertyVar("disabled")(isDisabledNow) <<: isDisabledSig
  ...
  "input" #> ("disabled=disabled [disabled]" #> disabled andThen clicks) andThen {
        "input [value]" #> kind.toString &
        "input [name]" #> "foobar"
      }
  ...

Naftoli Gugenheim

unread,
Jul 21, 2011, 5:59:36 PM7/21/11
to lif...@googlegroups.com
Why don't you just leave out the disabled attribute in the template, and let reactive add it as appropriate?


--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/dQ73C2XMks8J.

Barry Kaplan

unread,
Jul 21, 2011, 8:33:55 PM7/21/11
to lif...@googlegroups.com
On Thursday, July 21, 2011 2:59:36 PM UTC-7, nafg wrote:
Why don't you just leave out the disabled attribute in the template, and let reactive add it as appropriate?

That is what I am doing.  But with the wrong selector -- eg "disabled=disabled" vs "disabled=disabled [disabled]" -- once the attribute got set it would never get unset.

Naftoli Gugenheim

unread,
Jul 21, 2011, 10:06:17 PM7/21/11
to lif...@googlegroups.com
I don't understand why you want that selector.
In the code you posted,
("disabled=disabled [disabled]" #> disabled andThen clicks) 
doesn't do what it should, because the #> (non-alphanumeric) operator has higher precedence than andThen, since the latter is alphanumeric. So what's happening is that first ("..." #> disabled) is evaluated (not sure what applying a PropertVar to an attribute does), and returns a NodeSeq=>NodeSeq (because css selectors extend NodeSeq=>NodeSeq). Then this function is andThen'ed with clicks, another NodeSeq=>NodeSeq. Now, since all this happens within "input" #> ..., the resulting function's input is only the input element, so clicks does the right thing. But I can't imagine how disabled is working. It needs to operate on the element, not the attribute, since in order for it to update the disabled property later it needs to get or set the element's id attribute.

What was wrong with the code I posted? If you got a compiler error, please post it. Here's my code again (which I didn't try to compile).

Template:
<!-- I wrote this before but it's extra: <span class="radio>  -->
<label><input type="radio" name="name" /><span class="text"></span></label>

Snippet:

".radio" #> Worktimes.values.toSeq.map { v =>
  val clicks = DOMEventSource.click ->> { someAction }
  val enabled = PropertyVar("disabled")(false) <<: someDependentSignal
  (".text" #> v.toString &
  "input [value]" #> v.toString) andThen
  "input" #> (enabled andThen clicks)
}

Meanwhile my guess is that you got a compiler error because the implicit conversion from PropertyVar to NodeSeq=>NodeSeq wasn't found. If so write enabled.render to get its PropertyRenderer (which extends NodeSeq=>NodeSeq; PropertyVar doesn't extend it directly because it needs to get an implicit Page).


--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/Qp0GcQKXBCEJ.

Barry Kaplan

unread,
Jul 21, 2011, 10:18:09 PM7/21/11
to lif...@googlegroups.com
Here's my test using your code (with some extra stuff removed)

  @Test def no_selector_for_disabled() {
    S.initIfUninitted(session) {
      val xml = <span class="kind">
                  <label>
                    <input type="radio"/>
                    <span class="text"/>
                  </label>
                </span>

      println("*** xml="+ xml)

      val disabled = PropertyVar("disabled")(false)
      val xr1 = (".kind" #> disabled).apply(xml)
      println("*** xr1="+ xr1)

      disabled ()= true
      val xr2 = (".kind" #> disabled).apply(xr1)
      println("*** xr2="+ xr2)

      disabled ()= false
      val xr3 = (".kind" #> disabled).apply(xr2)
      println("*** xr3="+ xr3)

      disabled ()= true
      val xr4 = (".kind" #> disabled).apply(xr3)
      println("*** xr4="+ xr4)
    }
  }


And here's the output:

*** xml=<span class="kind">
                  <label>
                    <input type="radio"></input>
                    <span class="text"></span>
                  </label>
                </span>
*** xr1=<span class="kind" id="reactiveWebId_000000">
                  <label>
                    <input type="radio"></input>
                    <span class="text"></span>
                  </label>
                </span>
*** xr2=<span disabled="disabled" class="kind" id="reactiveWebId_000000">
                  <label>
                    <input type="radio"></input>
                    <span class="text"></span>
                  </label>
                </span>
*** xr3=<span class="kind" disabled="disabled" id="reactiveWebId_000000">
                  <label>
                    <input type="radio"></input>
                    <span class="text"></span>
                  </label>
                </span>
*** xr4=<span disabled="disabled" class="kind" id="reactiveWebId_000000">
                  <label>
                    <input type="radio"></input>
                    <span class="text"></span>
                  </label>
                </span>

So even though the code rendering code you walked me thru does indeed return nil when the boolean property is false, once the disabled property gets into xml it never gets removed. Am I missing something?

Barry Kaplan

unread,
Jul 21, 2011, 10:27:58 PM7/21/11
to lif...@googlegroups.com
But now that I re-run the test with "disabled=disabled [disabled]" I see that while an existing disabled will be removed by the PropertyVar, it will not get added back. So I guess I don't have a solution that works just yet.

Naftoli Gugenheim

unread,
Jul 21, 2011, 10:28:20 PM7/21/11
to lif...@googlegroups.com
Ah, the problem is that currently reactive-web doesn't expect you to have the disabled attribute in the template (let me know if you feel otherwise). Either it adds the attribute or it doesn't. That's regarding the initial html rendering. Changes that occur later are applied by executing javascript like document.getElementById("reactiveWebId_000000").disabled = false, so your test is misleading.


--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/hMZpRoaGcoIJ.

Naftoli Gugenheim

unread,
Jul 21, 2011, 10:29:04 PM7/21/11
to lif...@googlegroups.com
On Thu, Jul 21, 2011 at 10:27 PM, Barry Kaplan <mem...@gmail.com> wrote:
But now that I re-run the test with "disabled=disabled [disabled]" I see that while an existing disabled will be removed by the PropertyVar, it will not get added back. So I guess I don't have a solution that works just yet.

I'm not clear what you mean (but see my previous email first).
 

--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/VVdgpAoFBzUJ.

Barry Kaplan

unread,
Jul 21, 2011, 10:52:07 PM7/21/11
to lif...@googlegroups.com


On Thursday, July 21, 2011 7:28:20 PM UTC-7, nafg wrote:
Ah, the problem is that currently reactive-web doesn't expect you to have the disabled attribute in the template (let me know if you feel otherwise). Either it adds the attribute or it doesn't. That's regarding the initial html rendering. Changes that occur later are applied by executing javascript like document.getElementById("reactiveWebId_000000").disabled = false, so your test is misleading.


Ah,  yes I see. I will create a true test that does the comet update. Thanks!

When I was hacking within my application I could not get the PropertyVar to update, but I can't really say now what the selector looked like and the turn around was way too slow. Which is why I tried to create a small test.  Right now I'm just whipping up prototype to evaluate moving our UI from Sproutcore to Lift. 



Naftoli Gugenheim

unread,
Jul 21, 2011, 11:02:23 PM7/21/11
to lif...@googlegroups.com
By the way, part of the point of abstracting out DomMutations was for testing: Besides having renderers that convert a DomMutation to a javascript command, a DomMutation also knows how to transform a NodeSeq. Currently property updates don't have a DomMutation, because although they initially render as an HTML attribute, later they modify the corresponding javascript property, which doesn't exist in the DOM per se. I'm questioning this design though...

Also, note that the update won't necessarily take place via comet. If disabled changes during processing of a reactive-web ajax call, then the javascript command will be returned as the ajax response.

In any case the simplest way to test it is to surround the test code with Reactions.inClientScope, which collects all the javascript queued and returns it (that's what the ajax mechanism uses), so even if in your application it will need to got through comet, you can use an inClientScope block to intercept it in the test.


--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/yqDxSSOCzh4J.

Barry Kaplan

unread,
Jul 22, 2011, 12:22:12 PM7/22/11
to lif...@googlegroups.com
Thanks for the help Naftoli.

I've create a simple standalone test that runs in jetty.

  <div class="lift:DisabledPage.render1">
      <label class="text">
        <input type="checkbox"/>
        CHECBOX: <span class="count">0</span>
      </label>
  </div>

class DisabledPage extends Observing {

  class D {
    val fastTimer = new reactive.Timer(now.millis, 5000)
    val fastTimerSig = fastTimer.hold(now.millis)

    var isDisabled = false

    val disabler = PropertyVar("disabled")(true) <<: fastTimer.map[Boolean] { _ =>
      isDisabled = if (isDisabled) false else true
      println("*** isDisabled="+isDisabled)
      isDisabled
    }
  }

  def render1 = {
    val d = new D
    (".text" #> Cell(d.fastTimerSig.map( ".count" #> _.toString))) andThen
    "input" #> d.disabler
  }

  def render2 = {
    val d = new D
    "input" #> d.disabler
  }

}

With render2, the checkbox (and label) are disabled/enabled as per the timer. 

But with render1 things are a bit different. The updates (as seen by firebox xhr view) for ".count" and "input".disabled 
come in two separate messages. In the browser the checkbox/label are never disabled. Clearly the replacement of the 
entire <input> element is overriding the disabled.

try{jQuery("#reactiveWebId_000000").each(function(i) {this.innerHTML = "\u000d\u000a        <input type=\"checkbox\" />\u000d\u000a        CHECBOX: 1311350006909\u000d\u000a      ";});} catch (e) {}
try { document.getElementById('reactiveWebId_000001').disabled = true; } catch (e) {}
try{jQuery("#reactiveWebId_000000").each(function(i) {this.innerHTML = "\u000d\u000a        <input type=\"checkbox\" />\u000d\u000a        CHECBOX: 1311350011908\u000d\u000a      ";});} catch (e) {}
try { document.getElementById('reactiveWebId_000001').disabled = false; } catch (e) {}

Each of the above are separate message received by the browser.
So my selectors are not quite right? Or maybe this is simply an issue in general with comet updates? 

Barry Kaplan

unread,
Jul 22, 2011, 1:49:16 PM7/22/11
to lif...@googlegroups.com
BTW, this comet actor can both update the label and disable the controls. Of course this works because all the html is sent up 
on reRender. (I know reRender is not to be encourage...)

So I am wonder if the model of how reactive implements PropertyVar updates will only work as long as no 
other snippet is also updating the same element.

Barry Kaplan

unread,
Jul 22, 2011, 1:49:37 PM7/22/11
to lif...@googlegroups.com
Sorry, here's the comet actor:

class DisabledCometActor extends CometActor with Observing {

  val fastTimer = new reactive.Timer(now.millis, 5000)
  val fastTimerSig = fastTimer.hold(now.millis)

  var isDisabled = false

  fastTimerSig.change ->> {
    isDisabled = if (isDisabled) false else true
    println("*** isDisabled="+isDisabled)
    reRender()
  }

  def disabledProperty = if (isDisabled) Some("disabled") else None

  def render = {
    ".count" #> <span class="count">{fastTimerSig.now}</span> &
    "input [disabled]" #> disabledProperty
  }

}

Naftoli Gugenheim

unread,
Jul 22, 2011, 5:53:27 PM7/22/11
to lif...@googlegroups.com
I'm not exactly sure what's happening, but with reactive-web you aren't supposed to use CometActor explicitly, at least not to re-render the same area of the page. Reactive-web needs to know the id of the element its updating the properties of. (If for whatever reason you need to, at least the id should be defined in the template, and you should use version 0.1 or the בע"ה-soon-to-be-available 0.2-SNAPSHOT.)
One of the main ideas of reactive-web is to be an abstraction that saves you from having to write comet actors. Instead, just call Reactions.initComet in Boot, and add a <div class="lift:reactive"></div> to your template (e.g., default). Reactive will create a per-page comet actor for you at that location.
If you really need the actor model for whatever reason, use regular actors of any type --- scala/lift/akka/scalaz/high-performance. But that relates to the design of your business logic, not how updates get to the browser --- that you shouldn't have to worry about. Moreover, updates will only go via comet if necessary (such as a timer tick originating on the server). If your code updates a page in response to interactions from that page, the updates will automatically be sent as the response of that ajax call.



--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/Sd1oD2e0_B4J.

Naftoli Gugenheim

unread,
Jul 22, 2011, 6:13:37 PM7/22/11
to lif...@googlegroups.com
Wait, were you just showing the comet actor for contrast, but it's a separate page? Sorry then. I read your message more closely now. You are correct that replacing the input element prevents the property from updating, since the new element does not have id reactiveWebId_000001. You can either specify the id in the template, or just rearrange the code more like this:

  def render1 = {
    val d = new D
    ".text" #> Cell { d.fastTimerSig.map{ t =>
      ".count" #> t.toString &
      "input" #> d.disabler
    }}
  }
This way, whenever it re-renders the contents of the .text, it will re-apply the PropertyVar.

As far as why the comet updates are sent separately, it's true that that's the general behavior of comet. Reactive-web is designed to work around that by wrapping updates with a call to Reactions.inAnyScope or Reactions.inServerScope. However, in this case I'm not sure if it's possible to wrap the entire timer tick propagation with a call to inServerScope. Try it first with the above re-structured code and see if it helps. You could also try wrapping some part of the re-structured Cell code with a call to Reactions.inServerScope, perhaps.

I think what's really needed is the ability to wrap further propagation of a Signal or EventStream, so one could write
def serverTimer(implicit page: Page) = fastTimerSig.wrap(Reactions.inServerScope(page))
def wrap[T](wrapper: (=>T)=>T): Signal[T]




--
You received this message because you are subscribed to the Google Groups "Lift" group.
To view this discussion on the web visit https://groups.google.com/d/msg/liftweb/-/PNy8FNEOhqkJ.

Barry Kaplan

unread,
Jul 22, 2011, 9:57:42 PM7/22/11
to lif...@googlegroups.com


On Friday, July 22, 2011 2:53:27 PM UTC-7, nafg wrote:
I'm not exactly sure what's happening, but with reactive-web you aren't supposed to use CometActor explicitly...

I'm not mixing the two. This was just a non reactive-web implementation.
Reply all
Reply to author
Forward
0 new messages