You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@ignite.apache.org by an...@apache.org on 2015/06/26 13:07:05 UTC

incubator-ignite git commit: # ignite-843 Refactor model binding.

Repository: incubator-ignite
Updated Branches:
  refs/heads/ignite-843 1d0069d0c -> 7b22d69a5


# ignite-843 Refactor model binding.


Project: http://git-wip-us.apache.org/repos/asf/incubator-ignite/repo
Commit: http://git-wip-us.apache.org/repos/asf/incubator-ignite/commit/7b22d69a
Tree: http://git-wip-us.apache.org/repos/asf/incubator-ignite/tree/7b22d69a
Diff: http://git-wip-us.apache.org/repos/asf/incubator-ignite/diff/7b22d69a

Branch: refs/heads/ignite-843
Commit: 7b22d69a5614d1f2538c7bc50da8e8a0d881915b
Parents: 1d0069d
Author: Andrey <an...@gridgain.com>
Authored: Fri Jun 26 18:07:22 2015 +0700
Committer: Andrey <an...@gridgain.com>
Committed: Fri Jun 26 18:07:22 2015 +0700

----------------------------------------------------------------------
 .../nodejs/public/form-models/clusters.json     | 105 ++++++++++++-------
 .../public/javascripts/controllers/caches.js    |   1 +
 .../public/javascripts/controllers/clusters.js  |   1 +
 .../public/javascripts/controllers/common.js    |  59 ++++++++---
 .../nodejs/views/includes/controls.jade         |  45 ++++----
 5 files changed, 139 insertions(+), 72 deletions(-)
----------------------------------------------------------------------


http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/7b22d69a/modules/webconfig/nodejs/public/form-models/clusters.json
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/public/form-models/clusters.json b/modules/webconfig/nodejs/public/form-models/clusters.json
index e839e02..cb43c5b 100644
--- a/modules/webconfig/nodejs/public/form-models/clusters.json
+++ b/modules/webconfig/nodejs/public/form-models/clusters.json
@@ -32,7 +32,8 @@
     {
       "label": "Discovery",
       "type": "dropdown-details",
-      "model": "discovery.kind",
+      "path": "discovery",
+      "model": "kind",
       "required": true,
       "placeholder": "Choose discovery",
       "items": "discoveries",
@@ -47,7 +48,8 @@
               "tableLabel": "Addresses",
               "label": "address",
               "type": "table-simple",
-              "model": "discovery.Vm.addresses",
+              "path": "discovery.Vm",
+              "model": "addresses",
               "editIdx": -1,
               "reordering": true,
               "placeholder": "IP address:port",
@@ -77,7 +79,8 @@
             {
               "label": "IP address",
               "type": "text",
-              "model": "discovery.Multicast.multicastGroup",
+              "path": "discovery.Multicast",
+              "model": "multicastGroup",
               "placeholder": "228.1.2.4",
               "tip": [
                 "IP address of multicast group."
@@ -86,7 +89,8 @@
             {
               "label": "Port number",
               "type": "number",
-              "model": "discovery.Multicast.multicastPort",
+              "path": "discovery.Multicast",
+              "model": "multicastPort",
               "max": 65535,
               "placeholder": "47400",
               "tip": [
@@ -96,7 +100,8 @@
             {
               "label": "Waits for reply",
               "type": "number",
-              "model": "discovery.Multicast.responseWaitTime",
+              "path": "discovery.Multicast",
+              "model": "responseWaitTime",
               "placeholder": "500",
               "tip": [
                 "Time in milliseconds IP finder waits for reply to multicast address request."
@@ -105,7 +110,8 @@
             {
               "label": "Attempts count",
               "type": "number",
-              "model": "discovery.Multicast.addressRequestAttempts",
+              "path": "discovery.Multicast",
+              "model": "addressRequestAttempts",
               "placeholder": "2",
               "tip": [
                 "Number of attempts to send multicast address request.",
@@ -115,7 +121,8 @@
             {
               "label": "Local address",
               "type": "text",
-              "model": "discovery.Multicast.localAddress",
+              "path": "discovery.Multicast",
+              "model": "localAddress",
               "tip": [
                 "Local host address used by this IP finder.",
                 "If provided address is non-loopback then multicast socket is bound to this interface.",
@@ -130,7 +137,8 @@
             {
               "label": "Bucket name",
               "type": "text",
-              "model": "discovery.S3.bucketName",
+              "path": "discovery.S3",
+              "model": "bucketName",
               "placeholder": ""
             }
           ]
@@ -141,7 +149,8 @@
             {
               "label": "Credential",
               "type": "text",
-              "model": "discovery.Cloud.credential",
+              "path": "discovery.Cloud",
+              "model": "credential",
               "placeholder": "",
               "tip": [
                 "Credential that is used during authentication on the cloud.",
@@ -151,7 +160,8 @@
             {
               "label": "Path to credential",
               "type": "text",
-              "model": "discovery.Cloud.credentialPath",
+              "path": "discovery.Cloud",
+              "model": "credentialPath",
               "placeholder": "",
               "tip": [
                 "Path to a credential that is used during authentication on the cloud.",
@@ -161,7 +171,8 @@
             {
               "label": "Identity",
               "type": "text",
-              "model": "discovery.Cloud.identity",
+              "path": "discovery.Cloud",
+              "model": "identity",
               "placeholder": "",
               "tip": [
                 "Identity that is used as a user name during a connection to the cloud.",
@@ -185,7 +196,8 @@
             {
               "label": "Project name",
               "type": "text",
-              "model": "discovery.GoogleStorage.projectName",
+              "path": "discovery.GoogleStorage",
+              "model": "projectName",
               "placeholder": "",
               "tip": [
                 "Google Cloud Platforms project name.",
@@ -195,7 +207,8 @@
             {
               "label": "Bucket name",
               "type": "text",
-              "model": "discovery.GoogleStorage.bucketName",
+              "path": "discovery.GoogleStorage",
+              "model": "bucketName",
               "placeholder": "",
               "tip": [
                 "Google Cloud Storage bucket name.",
@@ -206,7 +219,8 @@
             {
               "label": "Private key path",
               "type": "text",
-              "model": "discovery.GoogleStorage.serviceAccountP12FilePath",
+              "path": "discovery.GoogleStorage",
+              "model": "serviceAccountP12FilePath",
               "placeholder": "",
               "tip": [
                 "Full path to the private key in PKCS12 format of the Service Account."
@@ -215,7 +229,8 @@
             {
               "label": "Account id",
               "type": "text",
-              "model": "discovery.GoogleStorage.accountId",
+              "path": "discovery.GoogleStorage",
+              "model": "accountId",
               "placeholder": "",
               "tip": [
                 "Service account ID (typically an e-mail address)."
@@ -229,7 +244,8 @@
             {
               "label": "DB schema should be initialized by Ignite",
               "type": "check",
-              "model": "discovery.Jdbc.initSchema",
+              "path": "discovery.Jdbc",
+              "model": "initSchema",
               "tip": [
                 "Flag indicating whether DB schema should be initialized by Ignite or was explicitly created by user."
               ]
@@ -242,7 +258,8 @@
             {
               "label": "File path",
               "type": "text",
-              "model": "discovery.SharedFs.path",
+              "path": "discovery.SharedFs",
+              "model": "path",
               "placeholder": "disco/tcp"
             }
           ]
@@ -260,7 +277,8 @@
         {
           "label": "Backups",
           "type": "number",
-          "model": "atomicConfiguration.backups",
+          "path": "atomicConfiguration",
+          "model": "backups",
           "placeholder": "0",
           "tip": [
             "Number of backup nodes."
@@ -269,7 +287,8 @@
         {
           "label": "Cache mode",
           "type": "dropdown",
-          "model": "atomicConfiguration.cacheMode",
+          "path": "atomicConfiguration",
+          "model": "cacheMode",
           "placeholder": "PARTITIONED",
           "items": "cacheModes",
           "tip": [
@@ -284,7 +303,8 @@
         {
           "label": "Sequence reserve",
           "type": "number",
-          "model": "atomicConfiguration.atomicSequenceReserveSize",
+          "path": "atomicConfiguration",
+          "model": "atomicSequenceReserveSize",
           "placeholder": "1,000",
           "tip": [
             "Default number of sequence values reserved for IgniteAtomicSequence instances.",
@@ -394,7 +414,8 @@
         {
           "label": "Marshaller",
           "type": "dropdown-details",
-          "model": "marshaller.kind",
+          "path": "marshaller",
+          "model": "kind",
           "placeholder": "Choose marshaller",
           "items": "marshallers",
           "tip": [
@@ -407,7 +428,8 @@
                 {
                   "label": "Streams pool size",
                   "type": "number",
-                  "model": "marshaller.OptimizedMarshaller.poolSize",
+                  "path": "marshaller.OptimizedMarshaller",
+                  "model": "poolSize",
                   "placeholder": "0",
                   "tip": [
                     "Specifies size of cached object streams used by marshaller.",
@@ -421,7 +443,8 @@
                 {
                   "label": "Require serializable",
                   "type": "check",
-                  "model": "marshaller.OptimizedMarshaller.requireSerializable",
+                  "path": "marshaller.OptimizedMarshaller",
+                  "model": "requireSerializable",
                   "tip": [
                     "Whether marshaller should require Serializable interface or not."
                   ]
@@ -555,7 +578,8 @@
         {
           "label": "Swap space SPI",
           "type": "dropdown-details",
-          "model": "swapSpaceSpi.kind",
+          "path": "swapSpaceSpi",
+          "model": "kind",
           "items": "swapSpaceSpis",
           "placeholder": "Choose swap SPI",
           "tip": [
@@ -568,7 +592,8 @@
                 {
                   "label": "Base directory",
                   "type": "text",
-                  "model": "swapSpaceSpi.baseDirectory",
+                  "path": "swapSpaceSpi.FileSwapSpaceSpi",
+                  "model": "baseDirectory",
                   "placeholder": "swapspace",
                   "tip": [
                     "Base directory where to write files."
@@ -577,7 +602,8 @@
                 {
                   "label": "Read stripe size",
                   "type": "number",
-                  "model": "swapSpaceSpi.readStripesNumber",
+                  "path": "swapSpaceSpi.FileSwapSpaceSpi",
+                  "model": "readStripesNumber",
                   "placeholder": "available CPU cores",
                   "tip": [
                     "Read stripe size defines number of file channels to be used concurrently."
@@ -586,7 +612,8 @@
                 {
                   "label": "Maximum sparsity",
                   "type": "number",
-                  "model": "swapSpaceSpi.maximumSparsity",
+                  "path": "swapSpaceSpi.FileSwapSpaceSpi",
+                  "model": "maximumSparsity",
                   "placeholder": "0.5",
                   "tip": [
                     "This property defines maximum acceptable wasted file space to whole file size ratio.",
@@ -596,7 +623,8 @@
                 {
                   "label": "Max write queue size",
                   "type": "number",
-                  "model": "swapSpaceSpi.maxWriteQueueSize",
+                  "path": "swapSpaceSpi.FileSwapSpaceSpi",
+                  "model": "maxWriteQueueSize",
                   "placeholder": "1024 * 1024",
                   "tip": [
                     "Max write queue size in bytes.",
@@ -606,7 +634,8 @@
                 {
                   "label": "Write buffer size",
                   "type": "number",
-                  "model": "swapSpaceSpi.writeBufferSize",
+                  "path": "swapSpaceSpi.FileSwapSpaceSpi",
+                  "model": "writeBufferSize",
                   "placeholder": "Available CPU cores",
                   "tip": [
                     "Write buffer size in bytes.",
@@ -711,7 +740,8 @@
         {
           "label": "Cache concurrency",
           "type": "dropdown",
-          "model": "transactionConfiguration.defaultTxConcurrency",
+          "path": "transactionConfiguration",
+          "model": "defaultTxConcurrency",
           "placeholder": "PESSIMISTIC",
           "items": "transactionConcurrency",
           "tip": [
@@ -721,7 +751,8 @@
         {
           "label": "Isolation",
           "type": "dropdown",
-          "model": "transactionConfiguration.transactionIsolation",
+          "path": "transactionConfiguration",
+          "model": "transactionIsolation",
           "placeholder": "REPEATABLE_READ",
           "items": "transactionIsolation",
           "tip": [
@@ -731,7 +762,8 @@
         {
           "label": "Default timeout",
           "type": "number",
-          "model": "transactionConfiguration.defaultTxTimeout",
+          "path": "transactionConfiguration",
+          "model": "defaultTxTimeout",
           "placeholder": "0",
           "tip": [
             "Default transaction timeout."
@@ -740,7 +772,8 @@
         {
           "label": "Pessimistic log cleanup delay",
           "type": "number",
-          "model": "transactionConfiguration.pessimisticTxLogLinger",
+          "path": "transactionConfiguration",
+          "model": "pessimisticTxLogLinger",
           "placeholder": "10,000",
           "tip": [
             "Delay, in milliseconds, after which pessimistic recovery entries will be cleaned up for failed node."
@@ -749,7 +782,8 @@
         {
           "label": "Pessimistic log size",
           "type": "number",
-          "model": "transactionConfiguration.pessimisticTxLogSize",
+          "path": "transactionConfiguration",
+          "model": "pessimisticTxLogSize",
           "placeholder": "0",
           "tip": [
             "Size of pessimistic transactions log stored on node in order to recover transaction commit if originating node has left grid before it has sent all messages to transaction nodes."
@@ -758,7 +792,8 @@
         {
           "label": "Enable serializable cache transactions",
           "type": "check",
-          "model": "transactionConfiguration.txSerializableEnabled",
+          "path": "transactionConfiguration",
+          "model": "txSerializableEnabled",
           "tip": [
             "Flag to enable/disable isolation level for cache transactions.",
             "Serializable level does carry certain overhead and if not used, should be disabled."

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/7b22d69a/modules/webconfig/nodejs/public/javascripts/controllers/caches.js
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/public/javascripts/controllers/caches.js b/modules/webconfig/nodejs/public/javascripts/controllers/caches.js
index 1a51064..705ca90 100644
--- a/modules/webconfig/nodejs/public/javascripts/controllers/caches.js
+++ b/modules/webconfig/nodejs/public/javascripts/controllers/caches.js
@@ -20,6 +20,7 @@ configuratorModule.controller('cachesController', ['$scope', '$alert', '$http',
         $scope.joinTip = commonFunctions.joinTip;
         $scope.getFldMdl = commonFunctions.getFldMdl;
         $scope.setFldMdl = commonFunctions.setFldMdl;
+        $scope.getModel = commonFunctions.getModel;
 
         $scope.atomicities = [
             {value: 'ATOMIC', label: 'ATOMIC'},

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/7b22d69a/modules/webconfig/nodejs/public/javascripts/controllers/clusters.js
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/public/javascripts/controllers/clusters.js b/modules/webconfig/nodejs/public/javascripts/controllers/clusters.js
index a9d4c86..75de767 100644
--- a/modules/webconfig/nodejs/public/javascripts/controllers/clusters.js
+++ b/modules/webconfig/nodejs/public/javascripts/controllers/clusters.js
@@ -20,6 +20,7 @@ configuratorModule.controller('clustersController', ['$scope', '$alert', '$http'
         $scope.joinTip = commonFunctions.joinTip;
         $scope.getFldMdl = commonFunctions.getFldMdl;
         $scope.setFldMdl = commonFunctions.setFldMdl;
+        $scope.getModel = commonFunctions.getModel;
 
         $scope.templates = [
             {value: {}, label: 'blank'},

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/7b22d69a/modules/webconfig/nodejs/public/javascripts/controllers/common.js
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/public/javascripts/controllers/common.js b/modules/webconfig/nodejs/public/javascripts/controllers/common.js
index 7cf8d19..3e0353c 100644
--- a/modules/webconfig/nodejs/public/javascripts/controllers/common.js
+++ b/modules/webconfig/nodejs/public/javascripts/controllers/common.js
@@ -17,15 +17,36 @@
 
 var configuratorModule = angular.module('ignite-web-configurator', ['smart-table', 'mgcrea.ngStrap', 'ngSanitize']);
 
-configuratorModule.service('commonFunctions', function() {
-   return {
-       swapSimpleItems: function(a, ix1, ix2) {
-           var tmp = a[ix1];
+configuratorModule.service('commonFunctions', function () {
+    return {
+        getModel: function(obj, path) {
+            if (!path)
+                return obj;
+
+            path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
+            path = path.replace(/^\./, '');           // strip a leading dot
+
+            var segs = path.split('.');
+            var root = obj;
+
+            while (segs.length > 0) {
+                var pathStep = segs.shift();
+
+                if (typeof root[pathStep] === 'undefined')
+                    root[pathStep] = {};
+
+                root = root[pathStep];
+            }
+
+            return root;
+        },
+        swapSimpleItems: function (a, ix1, ix2) {
+            var tmp = a[ix1];
 
             a[ix1] = a[ix2];
             a[ix2] = tmp;
         },
-        joinTip: function (arr) {
+        joinTip: function(arr) {
             if (!arr) {
                 return arr;
             }
@@ -42,7 +63,7 @@ configuratorModule.service('commonFunctions', function() {
 
             return lines.join("");
         },
-        getFldMdl: function (obj, path) {
+        getFldMdl: function(obj, path) {
             path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
             path = path.replace(/^\./, '');           // strip a leading dot
 
@@ -51,15 +72,17 @@ configuratorModule.service('commonFunctions', function() {
             for (var i = 0; i < a.length; ++i) {
                 var k = a[i];
 
-                if (k in obj)
+                if (k in obj) {
                     obj = obj[k];
-                else
+                }
+                else {
                     return;
+                }
             }
 
             return obj;
         },
-        setFldMdl: function (obj, path, value) {
+        setFldMdl: function(obj, path, value) {
             path = path.replace(/\[(\w+)\]/g, '.$1'); // convert indexes to properties
             path = path.replace(/^\./, '');           // strip a leading dot
 
@@ -69,19 +92,23 @@ configuratorModule.service('commonFunctions', function() {
                 var k = a[i];
 
                 if (k in obj) {
-                    if (!obj[k])
+                    if (!obj[k]) {
                         obj[k] = {};
+                    }
                 }
-                else
+                else {
                     obj[k] = {};
+                }
 
                 obj = obj[k];
             }
 
-            if (value)
+            if (value) {
                 obj[a[a.length - 1]] = value;
-            else
+            }
+            else {
                 delete obj[a[a.length - 1]];
+            }
 
             //
             //if (group && group.model && field.group)
@@ -132,11 +159,13 @@ configuratorModule.filter('displayValue', function () {
             return item.value == v;
         });
 
-        if (i >= 0)
+        if (i >= 0) {
             return m[i].label;
+        }
 
-        if (dflt)
+        if (dflt) {
             return dflt;
+        }
 
         return 'Unknown value';
     }

http://git-wip-us.apache.org/repos/asf/incubator-ignite/blob/7b22d69a/modules/webconfig/nodejs/views/includes/controls.jade
----------------------------------------------------------------------
diff --git a/modules/webconfig/nodejs/views/includes/controls.jade b/modules/webconfig/nodejs/views/includes/controls.jade
index 1880fc1..ead42bf 100644
--- a/modules/webconfig/nodejs/views/includes/controls.jade
+++ b/modules/webconfig/nodejs/views/includes/controls.jade
@@ -28,7 +28,7 @@ mixin exclamation(mdl, err, msg)
 mixin details-row
     - var lblDetailClasses = ['col-sm-4', 'details-label']
 
-    - var detailModel = {'ng-init': 'detailMdl=getFldMdl(backupItem, detail.model)', 'ng-change': 'setFldMdl(backupItem, detail.model, detailMdl)', 'ng-model': 'detailMdl'};
+    - var detailModel = {'ng-model': 'getModel(backupItem, detail.path)[detail.model]'};
 
     div(ng-switch='detail.type')
         div.checkbox(ng-switch-when='check')
@@ -86,12 +86,13 @@ mixin details-row
 mixin form-row
     - var lblClasses = ['col-sm-2']
 
-    - var fieldModel = {'ng-init': 'fieldMdl=getFldMdl(backupItem, field.model)', 'ng-change': 'setFldMdl(backupItem, field.model, fieldMdl)', 'ng-model': 'fieldMdl'};
+    - var fieldMdl = 'getModel(backupItem, field.path)[field.model]';
+    - var fieldCommon = {'ng-model': fieldMdl};
 
     div(ng-switch='field.type')
         div.checkbox.col-sm-6(ng-switch-when='check')
             label
-                input(type='checkbox')&attributes(fieldModel)
+                input(type='checkbox')&attributes(fieldCommon)
                 | {{field.label}}
                 +tipLabel('field.tip')
         div(ng-switch-when='text')
@@ -99,19 +100,19 @@ mixin form-row
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    input.form-control(type='text' placeholder='{{field.placeholder}}' ng-required='field.required')&attributes(fieldModel)
+                    input.form-control(type='text' placeholder='{{field.placeholder}}' ng-required='field.required')&attributes(fieldCommon)
         div(ng-switch-when='password')
             label(class=lblClasses ng-class='{required: field.required}') {{field.label}}:
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    input.form-control(type='password' placeholder='{{field.placeholder}}' ng-required='field.required')&attributes(fieldModel)
+                    input.form-control(type='password' placeholder='{{field.placeholder}}' ng-required='field.required')&attributes(fieldCommon)
         div(ng-switch-when='number')
             label(class=lblClasses ng-class='{required: field.required}') {{field.label}}:
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    input.form-control(name='{{field.model}}' type='number' placeholder='{{field.placeholder}}' min='{{field.min ? field.min : 0}}' max='field.max ? field.max : Number.MAX_VALUE' ng-required='field.required')&attributes(fieldModel)
+                    input.form-control(name='{{field.model}}' type='number' placeholder='{{field.placeholder}}' min='{{field.min ? field.min : 0}}' max='field.max ? field.max : Number.MAX_VALUE' ng-required='field.required')&attributes(fieldCommon)
                     +exclamation('{{field.model}}', 'min', 'Value is less than allowable minimum.')
                     +exclamation('{{field.model}}', 'max', 'Value is more than allowable maximum.')
                     +exclamation('{{field.model}}', 'number', 'Invalid value. Only numbers allowed.')
@@ -120,47 +121,47 @@ mixin form-row
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    button.form-control(bs-select ng-required='field.required' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldModel)
+                    button.form-control(bs-select ng-required='field.required' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldCommon)
         div(ng-switch-when='dropdown-multiple')
             label(class=lblClasses ng-class='{required: field.required}') {{field.label}}:
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    button.form-control(bs-select ng-disabled='{{field.items}}.length == 0' data-multiple='1' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldModel)
+                    button.form-control(bs-select ng-disabled='{{field.items}}.length == 0' data-multiple='1' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldCommon)
             a.customize(ng-show='field.addLink' ng-href='{{field.addLink.ref}}') {{field.addLink.label}}
         div(ng-switch-when='dropdown-details')
-            - var expanded = 'field.details[backupItem[field.group][field.model]].expanded'
+            - var expanded = 'field.details[' + fieldMdl + '].expanded'
 
             label(class=lblClasses ng-class='{required: field.required}') {{field.label}}:
             .col-sm-4
                 +tipField('field.tip')
                 .input-tip
-                    button.form-control(bs-select ng-required='field.required' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldModel)
-            a.customize(ng-show='fieldMdl && field.details[fieldMdl].fields' ng-click='#{expanded} = !#{expanded}') {{#{expanded} ? "Hide settings" : "Show settings"}}
-            .col-sm-6.panel-details(ng-show='#{expanded} && fieldMdl')
-                .details-row(ng-repeat='detail in field.details[fieldMdl].fields')
+                    button.form-control(bs-select ng-required='field.required' data-placeholder='{{field.placeholder}}' bs-options='item.value as item.label for item in {{field.items}}')&attributes(fieldCommon)
+            a.customize(ng-show='#{fieldMdl} && field.details[#{fieldMdl}].fields' ng-click='#{expanded} = !#{expanded}') {{#{expanded} ? "Hide settings" : "Show settings"}}
+            .col-sm-6.panel-details(ng-show='#{expanded} && #{fieldMdl}')
+                .details-row(ng-repeat='detail in field.details[#{fieldMdl}].fields')
                     +details-row
-        div(ng-switch-when='table-simple')&attributes(fieldModel)
+        div(ng-switch-when='table-simple')&attributes(fieldCommon)
             div
-                label {{field.tableLabel}}: {{fieldMdl.length}}
+                label {{field.tableLabel}}: {{#{fieldMdl}.length}}
                 +tipLabel('field.tip')
-            table.links-edit.col-sm-12(st-table='fieldMdl' ng-show='fieldMdl.length > 0')
+            table.links-edit.col-sm-12(st-table='#{fieldMdl}' ng-show='#{fieldMdl}.length > 0')
                 tbody
-                    tr.col-sm-12(ng-repeat='item in fieldMdl track by $index')
+                    tr.col-sm-12(ng-repeat='item in #{fieldMdl} track by $index')
                         td.col-sm-6
                             div(ng-show='field.editIdx != {{$index}}')
-                                a(ng-click='field.editIdx = $index; curValue = fieldMdl[$index]') {{$index + 1}}) {{item | compact}}
-                                i.tipField.fa.fa-remove(ng-click='field.editIdx = -1; fieldMdl.splice($index, 1)')
+                                a(ng-click='field.editIdx = $index; curValue = #{fieldMdl}[$index]') {{$index + 1}}) {{item | compact}}
+                                i.tipField.fa.fa-remove(ng-click='field.editIdx = -1; #{fieldMdl}.splice($index, 1)')
                             div(ng-show='field.editIdx == {{$index}}')
                                 label.labelField {{$index + 1}})
-                                i.tipField.fa.fa-floppy-o(ng-click='field.editIdx = -1; fieldMdl[$index]=curValue')
+                                i.tipField.fa.fa-floppy-o(ng-click='field.editIdx = -1; #{fieldMdl}[$index]=curValue')
                                 .input-tip
                                     input.form-control(type='text' ng-model='curValue' placeholder='{{field.placeholder}}')
                         td.col-sm-1(ng-if='field.reordering')
                             i.fa.fa-arrow-up(ng-show='$index > 0' ng-click='swapSimpleItems(fieldMdl, $index, $index - 1); field.editIdx = -1;')
-                            i.fa.fa-arrow-down(ng-show='$index < fieldMdl.length - 1' ng-click='swapSimpleItems(fieldMdl, $index, $index + 1); field.editIdx = -1;')
+                            i.fa.fa-arrow-down(ng-show='$index < #{fieldMdl}.length - 1' ng-click='swapSimpleItems(#{fieldMdl}, $index, $index + 1); field.editIdx = -1;')
             .col-sm-6
-                button.btn.btn-primary.fieldButton(ng-disabled='!newValue || fieldMdl.indexOf(newValue) >= 0' ng-click='field.editIdx = -1; fieldMdl ? fieldMdl.push(newValue) : fieldMdl = [newValue]; setFldMdl(backupItem, field.model, fieldMdl);') Add
+                button.btn.btn-primary.fieldButton(ng-disabled='!newValue || #{fieldMdl}.indexOf(newValue) >= 0' ng-click='field.editIdx = -1; #{fieldMdl} ? #{fieldMdl}.push(newValue) : #{fieldMdl} = [newValue];') Add
                 +tipField('field.tip')
                 .input-tip
                     input.form-control(type='text' ng-model='newValue' ng-focus='field.editIdx = -1'  placeholder='{{field.placeholder}}')