For anyone who might come across this post. I figured it out!
I used this post as a reference :
Here is my solution's code:
/**
* Example of passing params via href element to a dynamic page
*
* Copyright 2015 SmartThings
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
* in compliance with the License. You may obtain a copy of the License at:
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software distributed under the License is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License
* for the specific language governing permissions and limitations under the License.
*
*/
definition(
name: "href params example v2",
namespace: "Testing",
author: "Mike Dam",
description: "Testing Params & dynamic pages",
documentationLink: "TBD",
singleInstance: true,
installOnOpen: true,
importUrl: "github... TBD",
videoLink: "Youtube Tutorial video... TBD",
iconUrl: "",
iconX2Url: "")
// Required Library Imports
import java.util.LinkedHashMap
// Required Classes
def installed() {
log.debug "installed()"
resetVariables()
}
def resetVariables(){
atomicState.nestedMaps = [:] //Hashmap, Key = Name | Value = [Height, Weight, Gender]
atomicState.orderList = [] //List, Index is order the keys are called on the main page
updated()
}
def updated() {
log.debug "updated()"
}
def uninstalled() {
log.debug "uninstalled()"
}
/*
Notes I found during experimentation:
1) Preferences needs to hold "pages" (ex: "page(name: "moveLogicMethod")") that are the same as the Methods you add below if the Method either:
a) has page within it
b) is used as the "nextPage:" for a page
c) is used as the "page:" for a href
2) If you are referencing a Method as a page (see note #1) in any way, you need to ensure that Method itself either:
a) has a page within it
b) finishes by referencing a method who has a page within it
3) You must add the pages as a Method instead of within preferences if either:
a) they are called directly by a method (ex: "mainPageMethod()")
b) they do utilize params being passed to it from an href
4) The page you want to start on (look at upon starting the app) should be "top" of the preferences, even if it just references a Method containing the actual page.
5) When listing params in the href, they must be in the form of a map, even if only 1 variable. Examples:
a) params: [key : mapKey, move : "down"]
b) params: [key : mapKey]
6) The Method being called doesn't need to use "param" as the input variable. Example:
The href :
href(name: "toMoveUp", title: "Move Up", page: "moveLogicMethod", params: [key : mapKey, move : "up"], description: "↑", width: 2)
The method :
def moveLogicMethod(differentName) {
def name = differentName.key
def upOrDown = differentName.move
}
7) Pages within Methods must be Dynamic Pages.
8) You must fill in all required fields prior to hitting "back" in the browser OR using an href to go to another page. It is for this reason, I made an "error page" saying the required information isn't inputted ot continue, forcing the user to go back and fill in the data. In doing this, I allow the browser back button, the href "cancel" button, and href "delete" button to work without the fields to be filled in.
9) Design UI to have "cancel" and "next page" buttons appropriately. This is because :
a) "Done" usually goes to the last page. This does not work if the last page was generated by a Method. In that case, use "nextPage:" and use that to go to the Method that was the last page instead.
b) The browser's back button doesn't work when the last page was Method generated... in that case, make an "cancel" href that references the last page that was generated by a Method
*/
preferences {
page(name: "mainPageMethod")
page(name: "addPage", title: "Add Page", nextPage: "addLogic"){
section {
input "nameInput", "text", title: "Name", required: false, submitOnChange: true
input "heightInput", "number", range: "0..99", title: "Height in inches", required: false, submitOnChange: true
input "weightInput", "number", range: "0..999", title: "Weight in lbs", required: false, submitOnChange: true
input "genderInput", "enum", title: "Gender", options: ["Male", "Female"], required: false, submitOnChange: true
def keys = state.orderList ?: ["not null"]
if (keys.contains(nameInput) || nameInput == null)
{paragraph ("Name is required and must be unique.")}
paragraph ("To save, hit 'Next'")
href(name: "cancelAdd", title: "Cancel Add", page: "mainPageMethod", description: "Go back to Main Page", width: 2)
}
}
page(name: "editPageMethod1")
page(name: "editPageMethod2")
page(name: "addLogic")
page(name: "editLogic")
page(name: "deletePageMethod")
page(name: "moveLogicMethod")
}
def mainPageMethod() {
app.removeSetting("nameInput") //removes unwanted variables saved to settings
app.removeSetting("heightInput") //removes unwanted variables saved to settings
app.removeSetting("weightInput") //removes unwanted variables saved to settings
app.removeSetting("genderInput") //removes unwanted variables saved to settings
dynamicPage(name: "mainPage", title: "Main Page", uninstall: true) {
section {
//the List is just to give order to the entries in the higher level map
state.orderList.eachWithIndex { mapKey, i ->
paragraph("<h2 style='border: 1px solid'>") // could also use "hr sytle"... "div style" doesn't work for some reason
paragraph("#${i+1}) Name: $mapKey | Height: ${state.nestedMaps[mapKey]['Height']} | Weight: ${state.nestedMaps[mapKey]['Weight']} | Gender: ${state.nestedMaps[mapKey]['Gender']}")
href(name: "toEditPage", title: "Edit", page: "editPageMethod1", params: [key : mapKey], description: "Edit ${mapKey}", width: 4)
if(state.orderList.indexOf(mapKey) != (state.orderList.size()-1)){
href(name: "toMoveDown", title: "Move Down", page: "moveLogicMethod", params: [key : mapKey, move : "down"], description: "↓", width: 2)
} else {paragraph("", width: 2)}
if(state.orderList.indexOf(mapKey) !=0){
href(name: "toMoveUp", title: "Move Up", page: "moveLogicMethod", params: [key : mapKey, move : "up"], description: "↑", width: 2)
}
}
paragraph("<h2 style='border: 1px solid'>")
href(name: "toAddPage", title: "Add Entry", page: "addPage", description: "Add an entry into the Nested Maps")
}
}
}
//checks if all inputs are provided, if not, goes back to add page. If good, saves data and goes back to main page.
//I use these pages to do the saving & not auto-save as the info is inputted into the edit/add pages since I don't want partial info to be inputted, info saved, and then the app exited, making the entry only partially entered.
def addLogic() {
def keys = state.orderList ?: ["not null"]
if (keys.contains(nameInput) || nameInput == null || heightInput == null || weightInput == null || genderInput == null){
dynamicPage(name: "ErrorPage", title: "Error Page") {
section{
paragraph "Fill in all fields. Name must also be unique to add a new entry."
}
}
}
else
{
state.orderList.add(nameInput)
state.nestedMaps[nameInput] = [Height: heightInput, Weight: weightInput, Gender : genderInput]
mainPageMethod()
}
}
//checks if all inputs are provided, if not, goes back to edit page. If good, saves data and goes back to main page.
//I use these pages to do the saving & not auto-save as the info is inputted into the edit/add pages since I don't want partial info to be inputted, info saved, and then the app exited, making the entry only partially entered.
def editLogic(){
if (heightInput == null || weightInput == null || genderInput == null){
dynamicPage(name: "ErrorPage", title: "Error Page", nextPage: "editPageMethod2") {
section{
paragraph "Fill in all fields."
}
}
}
else
{
state.nestedMaps[nameInput] = [Height: heightInput, Weight: weightInput, Gender : genderInput]
mainPageMethod()
}
}
//required a intermediary Method since "Edit Logic" goes to "editPageMethod2" if there is an error (missing data) and due to that "editPageMethod2" can't have "params" as an input. Due to this, I use this Method to shift the param from the last page into a setting and have the next step reference the setting.
def editPageMethod1(params) {
def name = params.key
app.updateSetting("nameInput", name)
editPageMethod2()
}
def editPageMethod2() {
dynamicPage(name: "editPage", title: "Edit Page", nextPage : "editLogic") {
section{
paragraph ("Changing values in Nested Map for ${nameInput}")
input "heightInput", "number", range: "0..99", title: "Height in inches", required: false, submitOnChange: false, defaultValue : state.nestedMaps[nameInput]['Height']
input "weightInput", "number", range: "0..999", title: "Weight in lbs", required: false, submitOnChange: false, defaultValue : state.nestedMaps[nameInput]['Weight']
input "genderInput", "enum", title: "Gender", options: ["Male", "Female"], required: false, submitOnChange: false, defaultValue : state.nestedMaps[nameInput]['Gender']
paragraph ("To save, hit 'Next'")
href(name: "toDeletePage", title: "Delete", page: "deletePageMethod", params: [key : nameInput], description: "Remove ${nameInput} from nested map", width: 2)
href(name: "cancelEdit", title: "Cancel Edit", page: "mainPageMethod", description: "Go back to Main Page", width: 2)
}
}
}
//Could of just referenced the setting "nameInput" here, but wanted to use params for testing & experience
def deletePageMethod(params) {
state.nestedMaps.remove(params.key)
state.orderList.remove(params.key)
mainPageMethod()
}
//Used a variable "move" provided through param to determine what happens. This could have been 2 methods, but I was able to combine them into 1.
def moveLogicMethod(differentName) {
def name = differentName.key
def upOrDown = differentName.move
index = state.orderList.indexOf(name)
state.orderList.removeAt(index)
if (upOrDown == "up"){
state.orderList.add(index-1, name) // add nameInput just before index (index-1)
}
if (upOrDown == "down"){
state.orderList.add(index+1, name) // add nameInput just before index (index+1)
}
mainPageMethod()
}```
Preferences needs to hold "pages" (ex: "page(name: "moveLogicMethod")") that are the same as the Methods you add below if the Method either:
a) has page within it
b) is used as the "nextPage:" for a page
c) is used as the "page:" for a href
If you are referencing a Method as a page (see note #1) in any way, you need to ensure that Method itself either:
a) has a page within it
b) finishes by referencing a method who has a page within it
You must add the pages as a Method instead of within preferences if either:
a) they are called directly by a method (ex: "mainPageMethod()")
b) they do utilize params being passed to it from an href
The page you want to start on (look at upon starting the app) should be "top" of the preferences, even if it just references a Method containing the actual page.
When listing params in the href, they must be in the form of a map, even if only 1 variable. Examples:
a) params: [key : mapKey, move : "down"]
b) params: [key : mapKey]
The Method being called doesn't need to use "param" as the input variable. Example:
The href :
href(name: "toMoveUp", title: "Move Up", page: "moveLogicMethod", params: [key : mapKey, move : "up"], description: "↑", width: 2)
The method :
def moveLogicMethod(differentName) {
def name = differentName.key
def upOrDown = differentName.move
}
Pages within Methods must be Dynamic Pages.
You must fill in all required fields prior to hitting "back" in the browser OR using an href to go to another page. It is for this reason, I made an "error page" saying the required information isn't inputted ot continue, forcing the user to go back and fill in the data. In doing this, I allow the browser back button, the href "cancel" button, and href "delete" button to work without the fields to be filled in.
Design UI to have "cancel" and "next page" buttons appropriately. This is because :
a) "Done" usually goes to the last page. This does not work if the last page was generated by a Method. In that case, use "nextPage:" and use that to go to the Method that was the last page instead.
b) The browser's back button doesn't work when the last page was Method generated... in that case, make an "cancel" href that references the last page that was generated by a Method