Dynamically updating app HTML? Any info on this?

Continuing the discussion from Release 2.3.7 Available:

See above link. Is this usable in user apps/drivers? If so, how?

3 Likes

I would be very interested in the answer to this question also.

@gopher.ny made a post about this in the beta category when the feature was new. Perhaps it could be moved public (I can, just don't want to if that's intentional :smiley: ).

1 Like

Dang, I looked all over and didn't see that. Thanks!

And yea, probably should move it somewhere everyone can see it, not just those with access to the beta forum.

Good point, I just moved the post from Beta to Developers section.
This feature is for apps only, at least for now.

4 Likes

Not sure what use this would have in a driver, other than maybe sending something to "states" (not attribute) and having that update, since right now it only updates on a page refresh. But for the most part, in those cases I use an attribute, which already updated dynamically.

Maybe if there was some new area added entirely, I could see it being useful. If we had a "page" definition for devices where we could freely add stuff like a single-page app of sorts, but embedded in a driver. But otherwise, yeah, it really only makes sense to have on an app, which is where I plan on using it.

I rolled as bad version of dynamically updating in the HubiThings app, so was playing around with this and put together an example to have persistence and builder functions for app messaging.

It is very rare to use events inside of an App, so not many documented use cases to review. Would be great to have others improve upon this code, since repainting is often not a good solution. YMMV on what you can use this with...but putting this here to share as discuss.

SSR with peristance application
definition(
	name: "SSR with peristance",
	singleInstance: true,
	namespace: "demo",
	author: "bloodtick",
	description: "Sample App - server side rendering with peristance",
	category: "My Apps",
	iconUrl: "",
	iconX2Url: "",
	iconX3Url: "",
	installOnOpen: true
)

preferences {
	page(name: "mainPage")
}

def eventNow1() {
    log.info "${app.getLabel()} executing 'eventNow1' using type:\"UNIT\""
    sendEvent(name: "now1", value: now().toString(), unit:"%", type:"UNIT")
}

def eventNow2() {
    runIn(1,eventNow2delay)
}

def eventNow2delay() {
    log.info "${app.getLabel()} executing 'eventNow2'using type:\"TEXT\""
    sendEvent(name: "now2", value: now().toString(), displayName:"displayName", type:"TEXT", descriptionText:"descriptionText is ${now()}")
}

def eventNow3() {
    log.info "${app.getLabel()} executing 'eventNow3'using type:\"CALLBACK\""
    sendEvent(name: "now3", value: now().toString(), unit:"%", displayName:"displayName", data:[test:"this doesn't make it"], type:"CALLBACK", descriptionText:"eventNow3Callback")
}

def eventNow3Callback(event) {
    return "callback event was: ${event.sort().toString()}"
}

def eventDefault1() {
    log.info "${app.getLabel()} executing 'eventDefault1'"
    sendEvent(name: "now1", value: "this will show until a repaint", type:"DELETE")
}

def eventDefault2() {
    runIn(1,eventDefault2delay)
}

def eventDefault2delay() {
    log.info "${app.getLabel()} executing 'eventDefault2'"
    sendEvent(name: "now2", value: "", type:"DELETE+TEXT", descriptionText:"this will show until a repaint")
}

def eventDefault3() {
    log.info "${app.getLabel()} executing 'eventDefault3'"
    sendEvent(name: "now3", value: "" )
}

void appButtonHandler(String btn) {
    //log.info "${app.getLabel()} executing 'appButtonHandler($btn)'"
    def (k, v, a) = btn.tokenize("::")
    switch(k) {                
        case "dynamic":
            if(a) this."$v"(a); else this."$v"(); break
        default:
            log.debug "$btn not supported"
    }    
}

def mainPage() {
    dynamicPage(name: "mainPage", title: " ", install: true, uninstall: true) {//, containerClass: "w-full") {
        section {
            input(name: "dynamic::eventNow1", type: "button", title: "Event Now 1", width: 3, style:"width:50%;")
            input(name: "dynamic::eventNow2", type: "button", title: "Event Now 2", width: 3, style:"width:50%;")
            input(name: "dynamic::eventNow3", type: "button", title: "Event Now 3", width: 3, style:"width:50%;", newLineAfter:true)
            input(name: "dynamic::eventDefault1", type: "button", title: "Delete 1", width: 3, style:"width:50%;")
            input(name: "dynamic::eventDefault2", type: "button", title: "Delete 2", width: 3, style:"width:50%;")
            input(name: "dynamic::eventDefault3", type: "button", title: "Delete 3", width: 3, style:"width:50%;")
            
            paragraph(
                "event1: " + ssrEventSpan(name:"now1", defaultValue:"", newLineAfter:true) +
                "event2: " + ssrEventSpan("now2", "This will use descriptionText", "color:red;font-size:140%;font-style:italic;") + " <-[this has no break return]<br/>" +
                "event3: " + ssrEventSpan(name:"now3", defaultValue:"This is default callback", style:"color:blue;", newLineAfter:true)
            )
        }
	}
}

String ssrEventSpan(String name, String defaultValue, String style=null, Boolean newLineAfter=false) {
    return "<span class='ssr-app-state-${app.getId()}-$name'; ${style?"style='$style'":""}>${processServerSideRender(name:name, defaultValue:defaultValue)}</span>${newLineAfter?"<br/>":""}"
}
String ssrEventSpan(Map p) { return ssrEventSpan(p.name, p?.defaultValue, p?.style, p?.newLineAfter) }

String processServerSideRender(Map event) {
    event?.each{ k,v -> if(v=="null") { event[k]=null } } //lets normailize nulls
    log.debug event
    
    if(!state?.event) state.event = [:]
    // anytime we see defaultValue without having a new value we will use it. Should only be from paragraph itself
    if(!state.event.containsKey(event.name) && event.containsKey('defaultValue')) return event.defaultValue
    // this is special since descriptionText has a character limitation, lets allow for a way to have long strings returned.
    if(event?.type?.toUpperCase()?.contains("CALLBACK") && event?.descriptionText)
        state.event[event.name] = this."$event.descriptionText"(event)
    // normal event use case.
    else if(event?.value || event?.descriptionText)
        state.event[event.name] = (event?.type?.toUpperCase()?.contains("TEXT")) ? event?.descriptionText : (event?.type?.toUpperCase()?.contains("UNIT")) ? event?.value+event?.unit : event?.value    
    
    String response = state.event[event.name] ?: event?.defaultValue
    // we are clearing this value from persistance. need to handle 'defaultValue' problem if not repainting.
    if(state.event?.containsKey(event.name) && event?.name!=null && event.containsKey('value') && (event?.type?.toUpperCase()?.contains("DELETE") || event?.value==null)) 
    {   state.event.remove(event.name); log.trace "DELETE $event.name" }
    
    return response    
}