]> git.proxmox.com Git - proxmox-widget-toolkit.git/blob - src/mixin/CBind.js
cbind: document cbind by adding a small summary and example
[proxmox-widget-toolkit.git] / src / mixin / CBind.js
1 /*
2 * The Proxmox CBind mixin is intended to supplement the 'bind' mechanism
3 * of ExtJS. In contrast to the 'bind', 'cbind' only acts during the creation
4 * of the component, not during its lifetime. It's only applied once before
5 * the 'initComponent' method is executed, and thus you have only access
6 * to the basic initial configuration of it.
7 *
8 * You can use it to get a 'declarative' approach to component declaration,
9 * even when you need to set some properties of sub-components dynamically
10 * (e.g., the 'nodename'). It overwrites the given properties of the 'cbind'
11 * object in the component with their computed values of the computed
12 * cbind configuration object of the 'cbindData' function (or object).
13 *
14 * The cbind syntax is inspired by ExtJS' bind syntax ('{property}'), where
15 * it is possible to negate values ('{!negated}'), access sub-properties of
16 * objects ('{object.property}') and even use a getter function,
17 * akin to viewModel formulas ('(get) => get("prop")') to execute more
18 * complicated dependencies (e.g., urls).
19 *
20 * The 'cbind' will be recursively applied to all properties (objects/arrays)
21 * that contain an 'xtype' or 'cbind' property, but stops for a subtree if the
22 * object in question does not have either (if you have one or more levels that
23 * have no cbind/xtype property, you can insert empty cbind objects there to
24 * reach deeper nested objects).
25 *
26 * This reduces the code in the 'initComponent' and instead we can statically
27 * declare items, buttons, tbars, etc. while the dynamic parts are contained
28 * in the 'cbind'.
29 *
30 * It is used like in the following example:
31 *
32 * Ext.define('Some.Component', {
33 * extend: 'Some.other.Component',
34 *
35 * // first it has to be enabled
36 * mixins: ['Proxmox.Mixin.CBind'],
37 *
38 * // then a base config has to be defined. this can be a function,
39 * // which has access to the initial config and can store persistent
40 * // properties, as well as return temporary ones (which only exist during
41 * // the cbind process)
42 * // this function will be called before 'initComponent'
43 * cbindData: function(initialconfig) {
44 * // 'this' here is the same as in 'initComponent'
45 * let me = this;
46 * me.persistentProperty = false;
47 * return {
48 * temporaryProperty: true,
49 * };
50 * },
51 *
52 * // if there is no need for persistent properties, it can also simply be an object
53 * cbindData: {
54 * temporaryProperty: true,
55 * // properties itself can also be functions that will be evaluated before
56 * // replacing the values
57 * dynamicProperty: (cfg) => !cfg.temporaryProperty,
58 * numericProp: 0,
59 * objectProp: {
60 * foo: 'bar',
61 * bar: 'baz',
62 * }
63 * },
64 *
65 * // you can 'cbind' the component itself, here the 'target' property
66 * // will be replaced with the content of 'temporaryProperty' (true)
67 * // before the components initComponent
68 * cbind: {
69 * target: '{temporaryProperty}',
70 * },
71 *
72 * items: [
73 * {
74 * xtype: 'checkbox',
75 * cbind: {
76 * value: '{!persistentProperty}',
77 * object: '{objectProp.foo}'
78 * dynamic: (get) => get('numericProp') + 1,
79 * },
80 * },
81 * {
82 * // empty cbind so that subitems are reached
83 * cbind: {},
84 * items: [
85 * {
86 * xtype: 'textfield',
87 * cbind: {
88 * value: '{objectProp.bar}',
89 * },
90 * },
91 * ],
92 * },
93 * ],
94 * });
95 */
96
97 Ext.define('Proxmox.Mixin.CBind', {
98 extend: 'Ext.Mixin',
99
100 mixinConfig: {
101 before: {
102 initComponent: 'cloneTemplates',
103 },
104 },
105
106 cloneTemplates: function() {
107 let me = this;
108
109 if (typeof me.cbindData === "function") {
110 me.cbindData = me.cbindData(me.initialConfig);
111 }
112 me.cbindData = me.cbindData || {};
113
114 let getConfigValue = function(cname) {
115 if (cname in me.initialConfig) {
116 return me.initialConfig[cname];
117 }
118 if (cname in me.cbindData) {
119 let res = me.cbindData[cname];
120 if (typeof res === "function") {
121 return res(me.initialConfig);
122 } else {
123 return res;
124 }
125 }
126 if (cname in me) {
127 return me[cname];
128 }
129 throw "unable to get cbind data for '" + cname + "'";
130 };
131
132 let applyCBind = function(obj) {
133 let cbind = obj.cbind, cdata;
134 if (!cbind) return;
135
136 for (const prop in cbind) { // eslint-disable-line guard-for-in
137 let match, found;
138 cdata = cbind[prop];
139
140 found = false;
141 if (typeof cdata === 'function') {
142 obj[prop] = cdata(getConfigValue, prop);
143 found = true;
144 } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*)\}$/i.exec(cdata))) {
145 let cvalue = getConfigValue(match[2]);
146 if (match[1]) cvalue = !cvalue;
147 obj[prop] = cvalue;
148 found = true;
149 } else if ((match = /^\{(!)?([a-z_][a-z0-9_]*(\.[a-z_][a-z0-9_]*)+)\}$/i.exec(cdata))) {
150 let keys = match[2].split('.');
151 let cvalue = getConfigValue(keys.shift());
152 keys.forEach(function(k) {
153 if (k in cvalue) {
154 cvalue = cvalue[k];
155 } else {
156 throw "unable to get cbind data for '" + match[2] + "'";
157 }
158 });
159 if (match[1]) cvalue = !cvalue;
160 obj[prop] = cvalue;
161 found = true;
162 } else {
163 obj[prop] = cdata.replace(/{([a-z_][a-z0-9_]*)\}/ig, (_match, cname) => {
164 let cvalue = getConfigValue(cname);
165 found = true;
166 return cvalue;
167 });
168 }
169 if (!found) {
170 throw "unable to parse cbind template '" + cdata + "'";
171 }
172 }
173 };
174
175 if (me.cbind) {
176 applyCBind(me);
177 }
178
179 let cloneTemplateObject;
180 let cloneTemplateArray = function(org) {
181 let copy, i, found, el, elcopy, arrayLength;
182
183 arrayLength = org.length;
184 found = false;
185 for (i = 0; i < arrayLength; i++) {
186 el = org[i];
187 if (el.constructor === Object && (el.xtype || el.cbind)) {
188 found = true;
189 break;
190 }
191 }
192
193 if (!found) return org; // no need to copy
194
195 copy = [];
196 for (i = 0; i < arrayLength; i++) {
197 el = org[i];
198 if (el.constructor === Object && (el.xtype || el.cbind)) {
199 elcopy = cloneTemplateObject(el);
200 if (elcopy.cbind) {
201 applyCBind(elcopy);
202 }
203 copy.push(elcopy);
204 } else if (el.constructor === Array) {
205 elcopy = cloneTemplateArray(el);
206 copy.push(elcopy);
207 } else {
208 copy.push(el);
209 }
210 }
211 return copy;
212 };
213
214 cloneTemplateObject = function(org) {
215 let res = {}, prop, el, copy;
216 for (prop in org) { // eslint-disable-line guard-for-in
217 el = org[prop];
218 if (el === undefined || el === null) {
219 res[prop] = el;
220 continue;
221 }
222 if (el.constructor === Object && (el.xtype || el.cbind)) {
223 copy = cloneTemplateObject(el);
224 if (copy.cbind) {
225 applyCBind(copy);
226 }
227 res[prop] = copy;
228 } else if (el.constructor === Array) {
229 copy = cloneTemplateArray(el);
230 res[prop] = copy;
231 } else {
232 res[prop] = el;
233 }
234 }
235 return res;
236 };
237
238 let condCloneProperties = function() {
239 let prop, el, tmp;
240
241 for (prop in me) { // eslint-disable-line guard-for-in
242 el = me[prop];
243 if (el === undefined || el === null) continue;
244 if (typeof el === 'object' && el.constructor === Object) {
245 if ((el.xtype || el.cbind) && prop !== 'config') {
246 me[prop] = cloneTemplateObject(el);
247 }
248 } else if (el.constructor === Array) {
249 tmp = cloneTemplateArray(el);
250 me[prop] = tmp;
251 }
252 }
253 };
254
255 condCloneProperties();
256 },
257 });