diff --git a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
index 3005662d87379ced5542292deed0693d2fba8f46..aba9d999fe9d041dc9fa8bae9086b40a625dba88 100644
--- a/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
+++ b/src/main/resources/META-INF/xnat/spawner/site-admin-elements.yaml
@@ -31,7 +31,7 @@ siteDescriptionPage:
         <p class="description">Specify a velocity template file to display on the login page</p>
 
 siteDescriptionText:
-    tag: textarea|data-code-editor;data-code-language=html
+    tag: textarea|data-code-editor=html|data-code-language=html
     element:
         name: siteDescriptionText
         rows: 8
@@ -39,23 +39,19 @@ siteDescriptionText:
         <p class="description">Specify a simple text description of this site.</p>
 
 passwordExpirationInterval:
-    tag: input
-    element:
-        type: text
-        id: passwordExpirationInterval
-        name: passwordExpirationInterval
-        label: Password Expiration (Interval)
+    kind: input.text
+#    id: passwordExpirationInterval
+    name: passwordExpirationInterval
+    label: Password Expiration (Interval)
     after: >
       <p class="description">Interval of time after which unchanged passwords expire and users have to change them.
       Uses <a target="_blank" href="http://www.postgresql.org/docs/9.0/static/functions-datetime.html">PostgreSQL interval notation</a></p>
 
 passwordExpirationDate:
-    tag: input
-    element:
-        type: text
-        id: passwordExpirationDate
-        name: passwordExpirationDate
-        label: Password Expiration (Date)
+    kind: input.text
+#    id: passwordExpirationDate
+    name: passwordExpirationDate
+    label: Password Expiration (Date)
     after:
         datePicker:
             tag: span#datePicker
@@ -78,21 +74,19 @@ siteDescriptionType:
                 style: "margin: 3px 0 8px 0;"
             content: Select a site description option below
         siteDescriptionTypePage:
-            tag: input|data-value=Page
-            element:
-                type: radio
-                name: siteDescriptionType
-                value: Page
+            kind: input.radio
+            name: siteDescriptionType
+            id: siteDescriptionTypePage
+            value: Page
             after:
                 label:
                     tag: label.pad5h|for=siteDescriptionTypePage
                     content: Page
         siteDescriptionTypeText:
-            tag: input|data-value=Text
-            element:
-                type: radio
-                name: siteDescriptionType
-                value: Text
+            kind: input.radio
+            name: siteDescriptionType
+            id: siteDescriptionTypeText
+            value: Text
             after:
                 label:
                     tag: label.pad5h|for=siteDescriptionTypeText
@@ -106,9 +100,7 @@ siteDescriptionType:
             contents:
                 ${siteDescriptionText}
         siteInfoJs:
-            tag: script
-            element:
-                src: ~/scripts/xnat/admin/siteInfo.js
+            tag: script|src=/scripts/xnat/admin/siteInfo.js
 
 siteLoginLanding:
     kind: panel.input.text
@@ -410,34 +402,28 @@ passwords:
                         style: "margin: 3px 0 8px 0;"
                     content: Select a password expiration type below
                 passwordExpirationTypeDisabled:
-                    tag: input|data-value=Disabled
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeDisabled
-                        name: passwordExpirationType
-                        value: Disabled
+                    kind: input.radio
+                    id: passwordExpirationTypeDisabled
+                    name: passwordExpirationType
+                    value: Disabled
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeDisabled
                             content: Disabled
                 passwordExpirationTypeInterval:
-                    tag: input|data-value=Interval
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeInterval
-                        name: passwordExpirationType
-                        value: Interval
+                    kind: input.radio
+                    id: passwordExpirationTypeInterval
+                    name: passwordExpirationType
+                    value: Interval
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeInterval
                             content: Interval
                 passwordExpirationTypeDate:
-                    tag: input|data-value=Date
-                    element:
-                        type: radio
-                        id: passwordExpirationTypeDate
-                        name: passwordExpirationType
-                        value: Date
+                    kind: input.radio
+                    id: passwordExpirationTypeDate
+                    name: passwordExpirationType
+                    value: Date
                     after:
                         label:
                             tag: label.pad5h|for=passwordExpirationTypeDate
@@ -451,9 +437,7 @@ passwords:
                     contents:
                         ${passwordExpirationDate}
                 pwExpTypeJs:
-                    tag: script
-                    element:
-                        src: ~/scripts/xnat/admin/passwords.js
+                    tag: script|src=/scripts/xnat/admin/passwords.js
         passwordReuseRestriction:
             kind: panel.select.single
             id: passwordReuseRestriction
@@ -713,13 +697,10 @@ themeManagement:
     action: /xapi/theme
     contents:
         themeScript:
-            tag: script
-            element:
-                src: ~/scripts/xnat/admin/themeManagement.js
+            tag: script|src=/scripts/xnat/admin/themeManagement.js
         themeStyle:
             tag: style
-            element:
-                html: ".themeUploader{width:270px;display:inline !important;}"
+            contents: ".themeUploader{width:270px;display:inline !important;}"
         currentTheme:
             kind: panel.display
             id: currentTheme
@@ -737,14 +718,11 @@ themeManagement:
             description: Selected a new global theme from those available on the system.
             value: ""
             options:
-                default:
-                    label: None
-                    value: None
-            after:
-                - "<span style=\"position: relative; top: -78px;left: 270px;\"> <!-- &nbsp;<button id=\"submitThemeButton\" onclick=\"setTheme();\">Set Theme</button>&nbsp;&nbsp; --> <button id=\"removeThemeButton\" onclick=\"removeTheme();\">Remove Theme</button></span>"
+                None: None
             element:
-                style:
-                    min-width: 250px
+                style: "width:220px;margin-right:39px;"
+            afterElement: >
+                <button class="btn btn-sm" id="removeThemeButton" onclick="removeTheme()">Remove Theme</button>
         uploadTheme:
             kind: panel.input.upload
             id: themeFileUpload
@@ -939,6 +917,7 @@ sessionBuilder:
             name: sessionXmlRebuilderRepeat
             label: Session Idle Check Interval
             placeholder: Interval in milliseconds
+            afterElement: milliseconds
             description: >
                 This controls how often the system checks to see if any incoming DICOM sessions in the prearchive have
                 been idle for longer than the configured session idle time. This value should be specified in
@@ -948,6 +927,7 @@ sessionBuilder:
             name: sessionXmlRebuilderInterval
             label: Session Idle Time
             placeholder: Time in minutes
+            afterElement: minutes
             description: >
                 This tells the system how long a DICOM session should sit idle—that is, with no new data added to the
                 session—before attempting to build a session document from the DICOM data. This value is specified in
diff --git a/src/main/webapp/scripts/globals.js b/src/main/webapp/scripts/globals.js
index 74784137ff60b1c3f4cb373550e6f6ea498270cc..dbaef7f0d43731d9a8791315b46678c1006e8b28 100644
--- a/src/main/webapp/scripts/globals.js
+++ b/src/main/webapp/scripts/globals.js
@@ -320,7 +320,8 @@ function lookupObjectValue(root, objStr, prop){
         delim = '.',
         brackets = /[\]\[]/,
         hasBrackets = false,
-        parts = [];
+        parts = [],
+        undefined;
     
     if (!objStr) {
         objStr = root+'';
@@ -362,7 +363,7 @@ function lookupObjectValue(root, objStr, prop){
             val = root[part] || '';
         }
         else {
-            if (!val) return false;
+            if (val === undefined) return false;
             val = val[part];
         }
     });
@@ -666,6 +667,11 @@ function toDashed(str){
 //hyphenate = toDashed;
 //dashify   = toDashed;
 
+// like toDashed() but with underscores instead of hyphens
+function toUnderscore(str){
+    return toDashed(str).replace(/-+/g, '_');
+}
+
 // set 'forceLower' === true (or omit argument)
 // to ensure *only* 'cameled' letters are uppercase
 function toCamelCase(str) {
diff --git a/src/main/webapp/scripts/lib/spawn/spawn.js b/src/main/webapp/scripts/lib/spawn/spawn.js
index 9f8282994ed039fd12afbc0838b47b55c7d39ae3..4f847fd522b1640808940a4e93a5d3dd349cfd8d 100644
--- a/src/main/webapp/scripts/lib/spawn/spawn.js
+++ b/src/main/webapp/scripts/lib/spawn/spawn.js
@@ -2,8 +2,8 @@
  * DOM element spawner with *optional* jQuery functionality
  *
  * EXAMPLES:
- * var p1 = spawn('p|id:p1', 'Text for paragraph 1.');
- * var div2 = spawn('div|class=div2', ['Text for div2.', p1]) // inserts text and puts p1 inside div2
+ * var p1 = spawn('p#p1', 'Text for paragraph 1.');
+ * var div2 = spawn('div.div2', ['Text for div2.', p1]) // inserts text and puts p1 inside div2
  * var ul1 = spawn('ul', [['li', 'Content for <li> 1.'], ['li', 'Content for the next <li>.']]);
  * div2.appendChild(ul1); // add ul1 to div2
  */
@@ -84,12 +84,16 @@
 
 
     function parseAttrs(el, attrs){
-        // allow ';' or ',' for attribute delimeter
-        (attrs.split(/;|,/) || []).forEach(function(att, i){
+        // allow 'attrs' to be a string or array
+        if (typeof attrs == 'string'){
+            // use '|' for attribute delimiter
+            attrs = attrs.split('|');
+        }
+        attrs.forEach(function(att, i){
             if (!att) return;
-            // allow ':' or '=' for key/value separator
-            var sep = /:|=/;
-            // tolerate quotes around values
+            // use '=' for key/value separator
+            var sep = '=';
+            // remove quotes around values
             var quotes = /^['"]+|['"]+$/g;
             var key = att.split(sep)[0].trim();
             var val = (att.split(sep)[1]||'').trim().replace(quotes, '') || key;
@@ -252,10 +256,10 @@
 
         if (parts.length){
             // pass element attributes in 'tag' string, like:
-            // spawn('a|id="foo-link";href="foo";class="bar"');
-            // or (colons for separators, commas for delimeters, no quotes),:
-            // spawn('input|type:checkbox,id:foo-ckbx');
-            parseAttrs(el, parts[0]||'');
+            // spawn('a|id="foo-link"|href="foo"|class="bar"');
+            // or (without quotes),:
+            // spawn('input|type=checkbox|id=foo-ckbx');
+            parseAttrs(el, parts);
         }
 
         if (!opts && !children){
diff --git a/src/main/webapp/scripts/xnat/admin/passwords.js b/src/main/webapp/scripts/xnat/admin/passwords.js
index fc31a06be43efd93d3da11e2bd56b1bf607d2b3b..9fa3adfa66a707bbf5f4949cdb3c955013e9978d 100644
--- a/src/main/webapp/scripts/xnat/admin/passwords.js
+++ b/src/main/webapp/scripts/xnat/admin/passwords.js
@@ -11,13 +11,13 @@ console.log('passwordExpirationType.js');
     (function(){
 
         var fieldInterval$ =
-                container$.find('#passwordExpirationInterval')
+                container$.find('[name="passwordExpirationInterval"]')
                     .css({ marginTop: '10px' });
 
         var oldInterval = fieldInterval$.val();
 
         var fieldDate$ =
-                container$.find('#passwordExpirationDate')
+                container$.find('[name="passwordExpirationDate"]')
                     .attr({
                         size: 10,
                         placeholder: 'MM/DD/YYYY'
@@ -40,7 +40,7 @@ console.log('passwordExpirationType.js');
                         fieldDate$.datetimepicker('show');
                     });
 
-        container$.find('input[name="passwordExpirationType"]').on('change', function(){
+        container$.find('[name="passwordExpirationType"]').on('change', function(){
 
             // Does the interval need to be set to "-1" to disable expiration?
 
@@ -69,7 +69,7 @@ console.log('passwordExpirationType.js');
     (function(){
 
         var durationContainer$ = $('div[data-name="passwordHistoryDuration"]');
-        var durationInput$ = durationContainer$.find('input#passwordHistoryDuration');
+        var durationInput$ = durationContainer$.find('[name="passwordHistoryDuration"]');
 
         $('#passwordReuseRestriction').on('change', function(){
             changePasswordReuseType(this.value);
diff --git a/src/main/webapp/scripts/xnat/ui/input.js b/src/main/webapp/scripts/xnat/ui/input.js
index 66875c5a49e0cb3f17d0a4aa399c38bc025b49d5..ab081a3d11c7402e8b3cec3d4ac745d139eeb2c8 100644
--- a/src/main/webapp/scripts/xnat/ui/input.js
+++ b/src/main/webapp/scripts/xnat/ui/input.js
@@ -55,16 +55,16 @@ var XNAT = getObject(XNAT);
             config = type;
             type = null; // it MUST contain a 'type' property
         }
-        config = getObject(config);
+        config = cloneObject(config);
         config.type = type || config.type || 'text';
         // lookup a value if it starts with '??'
         var doLookup = '??';
-        if (config.value && config.value.toString().indexOf(doLookup) === 0) {
+        if (config.value && (config.value+'').indexOf(doLookup) === 0) {
             config.value = lookupValue(config.value.split(doLookup)[1])
         }
         // lookup a value from a namespaced object
         // if no value is given
-        if (!config.value && config.data && config.data.lookup) {
+        if (config.value === undefined && config.data && config.data.lookup) {
             config.value = lookupValue(config.data.lookup)
         }
         var spawned = spawn('input', config);
diff --git a/src/main/webapp/scripts/xnat/ui/panel.js b/src/main/webapp/scripts/xnat/ui/panel.js
index 01ffa1529ac1e878ba0b6023c2a1708647273b86..6a5a9dfb2797bfb2e39116eb23e87e941427aac2 100644
--- a/src/main/webapp/scripts/xnat/ui/panel.js
+++ b/src/main/webapp/scripts/xnat/ui/panel.js
@@ -230,8 +230,6 @@ var XNAT = getObject(XNAT || {});
                 var $this = $(this);
                 var val = lookupObjectValue(dataObj, this.name||this.title);
 
-                //if (!val) return;
-
                 if (Array.isArray(val)) {
                     val = val.join(', ');
                     $this.addClass('array-list')
@@ -240,7 +238,15 @@ var XNAT = getObject(XNAT || {});
                     val = stringable(val) ? val : JSON.stringify(val);
                 }
 
-                $this.not(':checkbox, :radio').changeVal(val);
+                // used on hidden inputs to reset values
+                if ($this.hasClasses('proxy && dirty')) {
+                    this.value = $this.dataAttr('value');
+                }
+
+                //if (val === "") return;
+
+                // $this.not(':checkbox, :radio').changeVal(val);
+                $this.not(':radio').changeVal(val);
 
                 if (/checkbox/i.test(this.type)) {
                     this.checked = realValue(val);
@@ -253,6 +259,8 @@ var XNAT = getObject(XNAT || {});
                     }
                 }
 
+                $this.removeClass('dirty').dataAttr('value', val);
+
             });
 
             loadingDialog().closeAll();
@@ -492,7 +500,7 @@ var XNAT = getObject(XNAT || {});
                     var obj = {};
                     // actually, NEVER use returned data...
                     // ALWAYS reload from the server
-                    obj.refresh = opts.refresh || opts.reload || opts.url || opts.load;
+                    obj.load = opts.refresh || opts.reload || opts.url || opts.load;
                     if (!silent){
                         XNAT.ui.banner.top(2000, 'Data saved successfully.', 'success');
                         loadData($form, obj);
@@ -509,6 +517,7 @@ var XNAT = getObject(XNAT || {});
                 ajaxConfig.processData = false;
                 ajaxConfig.contentType = 'application/json';
                 $.ajax(ajaxConfig);
+                // XNAT.xhr.form($form, ajaxConfig);
             }
             else {
                 $(this).ajaxSubmit(ajaxConfig);
@@ -846,7 +855,7 @@ var XNAT = getObject(XNAT || {});
                 multiple: true,
                 className: addClassName(opts, 'file-upload-input')
             }],
-            ['button', {
+            ['button.btn.btn-sm', {
                 type: 'submit',
                 id: opts.id +'-button',
                 html: 'Upload'
diff --git a/src/main/webapp/scripts/xnat/ui/templates.js b/src/main/webapp/scripts/xnat/ui/templates.js
index 44c568feda7b22579680648d23c3e0fc546dc95b..2207c18a2882a74a2577b6169fdaafb026f98223 100644
--- a/src/main/webapp/scripts/xnat/ui/templates.js
+++ b/src/main/webapp/scripts/xnat/ui/templates.js
@@ -188,7 +188,7 @@ var XNAT = getObject(XNAT);
             addClassName(opts.element, opts.data.validate);
         }
 
-        addDataObjects(opts.element, opts.data||{});
+        addDataObjects(opts.element, opts.data);
         
         if (opts.placeholder) {
             opts.element.placeholder = opts.placeholder;
@@ -202,16 +202,17 @@ var XNAT = getObject(XNAT);
         var $element = $(element);
 
         // set the value of individual form elements
+        var hasValue = isDefined(opts.value);
         
         // look up a namespaced object value if the value starts with '??'
         var doLookup = '??';
-        if (opts.value && opts.value.toString().indexOf(doLookup) === 0) {
+        if (hasValue && opts.value.toString().indexOf(doLookup) === 0) {
             // element.value = lookupValue(opts.value.split(doLookup)[1].trim());
-            $element.val(lookupObjectValue(opts.value.split(doLookup)[1].trim())).change();
+            $element.val(lookupObjectValue(opts.value.split(doLookup)[1].trim()));
         }
 
         var doEval = '!?';
-        if (opts.value && opts.value.toString().indexOf(doEval) === 0) {
+        if (hasValue && opts.value.toString().indexOf(doEval) === 0) {
             opts.value = (opts.value.split(doEval)[1]||'').trim();
             try {
                 $element.val(eval(opts.value)).change();
@@ -226,7 +227,7 @@ var XNAT = getObject(XNAT);
         var ajaxPrefix = '$?';
         var ajaxUrl = '';
         var ajaxProp = '';
-        if (opts.value && opts.value.toString().indexOf(ajaxPrefix) === 0) {
+        if (hasValue && opts.value.toString().indexOf(ajaxPrefix) === 0) {
             ajaxUrl = (opts.value.split(ajaxPrefix)[1]||'').split('|')[0];
             ajaxProp = opts.value.split('|')[1] || '';
             ajaxValue(element, ajaxUrl.trim(), ajaxProp.trim());
@@ -256,23 +257,34 @@ var XNAT = getObject(XNAT);
             inner.push(spawn('span.after', opts.afterElement));
         }
 
-        var hiddenInput;
+        var $hiddenInput, hiddenInput;
 
         // check buttons if value is true
         if (/checkbox/i.test(element.type||'')) {
 
-            element.checked = /true|checked/i.test((opts.checked||element.value||'').toString());
+            element.checked = /true|checked/i.test(opts.checked||element.value||'');
 
             // add a hidden input to capture the checkbox/radio value
-            hiddenInput = spawn('input', {
+            $hiddenInput = $.spawn('input.proxy', {
                 type: 'hidden',
                 name: element.name,
-                value: element.checked ? element.value || opts.value || element.checked : false
+                value: element.checked ? (element.value || opts.value || element.checked || 'true') : 'false'
             });
 
+            hiddenInput = $hiddenInput[0];
+
+            // add [data-value] attribute
+            $hiddenInput.dataAttr('value', hiddenInput.value);
+
             // change the value of the hidden input onclick
             element.onclick = function(){
-                hiddenInput.value = this.checked ? this.value || this.checked.toString() : false;
+                // if the checkbox value is boolean,
+                // match the value to the 'checked' state
+                if (/true|false/i.test(this.value)) {
+                    this.value = this.checked;
+                }
+                hiddenInput.value = this.checked ? (this.value || this.checked || 'true') : 'false';
+                $hiddenInput.toggleClass('dirty');
             };
             
             // copy name to title