@projectcaluma/ember-form 10.0.3 → 11.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,130 +1,166 @@
1
1
  import { getOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import { computed } from "@ember/object";
4
- import { inject as service } from "@ember/service";
5
3
  import { camelize } from "@ember/string";
6
4
  import { isEmpty } from "@ember/utils";
5
+ import { dedupeTracked, cached } from "tracked-toolbox";
7
6
 
8
7
  import { decodeId } from "@projectcaluma/ember-core/helpers/decode-id";
9
8
  import Base from "@projectcaluma/ember-form/lib/base";
10
9
  import { parseDocument } from "@projectcaluma/ember-form/lib/parsers";
11
10
 
11
+ /**
12
+ * Class that automatically defines all keys of the passed object as deduped
13
+ * tracked property and assigns the initial value.
14
+ *
15
+ * @class DedupedTrackedObject
16
+ * @private
17
+ */
18
+ class DedupedTrackedObject {
19
+ constructor(obj) {
20
+ Object.entries(obj).forEach(([key, value]) => {
21
+ Object.defineProperty(
22
+ this,
23
+ key,
24
+ dedupeTracked(this, key, { initializer: () => value })
25
+ );
26
+ });
27
+ }
28
+ }
29
+
12
30
  /**
13
31
  * Object which represents an answer in context of a field
14
32
  *
15
33
  * @class Answer
16
34
  */
17
- export default Base.extend({
18
- calumaStore: service(),
35
+ export default class Answer extends Base {
36
+ constructor({ raw, field, ...args }) {
37
+ assert("`field` must be passed as an argument", field);
19
38
 
20
- init(...args) {
21
39
  assert(
22
40
  "A graphql answer `raw` must be passed",
23
- this.raw && /Answer$/.test(this.raw.__typename)
41
+ /Answer$/.test(raw?.__typename)
24
42
  );
25
43
 
26
- if (this.raw.id) {
27
- this.set("pk", `Answer:${decodeId(this.raw.id)}`);
28
- }
44
+ super({ raw, ...args });
45
+
46
+ this.field = field;
47
+ this.raw = new DedupedTrackedObject(raw);
29
48
 
30
- this._super(...args);
49
+ this.pushIntoStore();
50
+ }
51
+
52
+ /**
53
+ * The field this answer originates from
54
+ *
55
+ * @property {Field} field
56
+ */
57
+ field = null;
58
+
59
+ /**
60
+ * The raw data of the answer. This is the only `raw` property that is tracked
61
+ * since only the answers properties are changed while rendering the form.
62
+ *
63
+ * @property {DedupedTrackedObject} raw
64
+ */
65
+ raw = {};
31
66
 
32
- this.setProperties(this.raw);
33
- },
67
+ /**
68
+ * The primary key of the answer.
69
+ *
70
+ * @property {String} pk
71
+ */
72
+ @cached
73
+ get pk() {
74
+ return this.uuid && `Answer:${this.uuid}`;
75
+ }
34
76
 
35
77
  /**
36
78
  * The uuid of the answer
37
79
  *
38
80
  * @property {String} uuid
39
- * @accessor
40
81
  */
41
- uuid: computed("raw.id", function () {
82
+ @cached
83
+ get uuid() {
42
84
  return this.raw.id ? decodeId(this.raw.id) : null;
43
- }),
85
+ }
44
86
 
45
- isNew: computed("uuid", "value", function () {
87
+ /**
88
+ * Whether the answer is new. This is true when there is no object from the
89
+ * backend or the value is empty.
90
+ *
91
+ * @property {Boolean} isNew
92
+ */
93
+ @cached
94
+ get isNew() {
46
95
  return !this.uuid || isEmpty(this.value);
47
- }),
96
+ }
48
97
 
49
98
  /**
50
99
  * The name of the property in which the value is stored. This depends on the
51
100
  * type of the answer.
52
101
  *
53
- *
54
102
  * @property {String} _valueKey
55
- * @accessor
56
103
  * @private
57
104
  */
58
- _valueKey: computed("__typename", function () {
105
+ @cached
106
+ get _valueKey() {
59
107
  return (
60
- this.__typename && camelize(this.__typename.replace(/Answer$/, "Value"))
108
+ this.raw.__typename &&
109
+ camelize(this.raw.__typename.replace(/Answer$/, "Value"))
61
110
  );
62
- }),
111
+ }
63
112
 
64
113
  /**
65
114
  * The value of the answer, the type of this value depends on the type of the
66
115
  * answer. For table answers this returns an array of documents.
67
116
  *
68
117
  * @property {String|Number|String[]|Document[]} value
69
- * @computed
70
118
  */
71
- value: computed(
72
- "field.document",
73
- "_valueKey",
74
- "dateValue",
75
- "fileValue",
76
- "floatValue",
77
- "integerValue",
78
- "listValue.[]",
79
- "stringValue",
80
- "tableValue.[]",
81
- {
82
- get() {
83
- const value = this.get(this._valueKey);
84
-
85
- if (this._valueKey === "tableValue" && value) {
86
- return value.map((document) => {
87
- const existing = this.calumaStore.find(
88
- `Document:${decodeId(document.id)}`
89
- );
90
-
91
- return (
92
- existing ||
93
- getOwner(this)
94
- .factoryFor("caluma-model:document")
95
- .create({
96
- raw: parseDocument(document),
97
- parentDocument: this.field.document,
98
- })
99
- );
100
- });
101
- }
102
-
103
- return value;
104
- },
105
- set(_, value) {
106
- value = [undefined, ""].includes(value) ? null : value;
107
-
108
- if (this._valueKey) {
109
- this.set(this._valueKey, value);
110
- }
111
-
112
- return value;
113
- },
119
+ @cached
120
+ get value() {
121
+ const value = this.raw[this._valueKey];
122
+
123
+ if (this._valueKey === "tableValue" && value) {
124
+ const owner = getOwner(this);
125
+ const Document = owner.factoryFor("caluma-model:document").class;
126
+
127
+ return value.map((document) => {
128
+ if (document instanceof Document) return document;
129
+
130
+ const existing = this.calumaStore.find(
131
+ `Document:${decodeId(document.id)}`
132
+ );
133
+
134
+ return (
135
+ existing ||
136
+ new Document({
137
+ raw: parseDocument(document),
138
+ parentDocument: this.field.document,
139
+ owner,
140
+ })
141
+ );
142
+ });
143
+ }
144
+
145
+ return value;
146
+ }
147
+
148
+ set value(value) {
149
+ if (this._valueKey) {
150
+ this.raw[this._valueKey] = [undefined, ""].includes(value) ? null : value;
114
151
  }
115
- ),
152
+ }
116
153
 
117
154
  /**
118
155
  * The value serialized for a backend request.
119
156
  *
120
157
  * @property {String|Number|String[]} serializedValue
121
- * @accessor
122
158
  */
123
- serializedValue: computed("__typename", "value", function () {
124
- if (this.__typename === "TableAnswer") {
159
+ get serializedValue() {
160
+ if (this.raw.__typename === "TableAnswer") {
125
161
  return (this.value || []).map(({ uuid }) => uuid);
126
162
  }
127
163
 
128
164
  return this.value;
129
- }),
130
- });
165
+ }
166
+ }
package/addon/lib/base.js CHANGED
@@ -1,35 +1,44 @@
1
- import { getOwner } from "@ember/application";
1
+ import { setOwner } from "@ember/application";
2
2
  import { assert } from "@ember/debug";
3
- import EmberObject from "@ember/object";
3
+ import { registerDestructor } from "@ember/destroyable";
4
4
  import { inject as service } from "@ember/service";
5
5
 
6
- export default EmberObject.extend({
7
- calumaStore: service(),
6
+ export default class Base {
7
+ @service calumaStore;
8
8
 
9
- init(...args) {
10
- this._super(...args);
9
+ constructor({ raw, owner }) {
10
+ assert("`owner` must be passed as an argument", owner);
11
11
 
12
- assert("Owner must be injected", getOwner(this));
12
+ assert("A primary key `pk` must be defined on the object", "pk" in this);
13
13
 
14
- // answers don't need a pk if they are new
15
- if (!/Answer$/.test(this.get("raw.__typename"))) {
16
- assert("A primary key `pk` must be passed", this.pk);
17
- assert(
18
- "The primary key `pk` must be readonly",
19
- !Object.getOwnPropertyDescriptor(this, "pk").writable
20
- );
21
- }
14
+ setOwner(this, owner);
22
15
 
23
- if (this.pk) {
24
- this.calumaStore.push(this);
16
+ if (raw) {
17
+ this.raw = raw;
25
18
  }
26
- },
27
19
 
28
- willDestroy(...args) {
29
- this._super(...args);
20
+ registerDestructor(this, () => {
21
+ if (this.pk) {
22
+ this.calumaStore.delete(this.pk);
23
+ }
24
+ });
25
+ }
30
26
 
27
+ /**
28
+ * The raw data of the object
29
+ *
30
+ * @property {Object} raw
31
+ */
32
+ raw = {};
33
+
34
+ /**
35
+ * Push the object into the caluma store
36
+ *
37
+ * @method pushIntoStore
38
+ */
39
+ pushIntoStore() {
31
40
  if (this.pk) {
32
- this.calumaStore.delete(this.pk);
41
+ this.calumaStore.push(this);
33
42
  }
34
- },
35
- });
43
+ }
44
+ }
@@ -1,4 +1,5 @@
1
- import { computed, get } from "@ember/object";
1
+ import { get } from "@ember/object";
2
+ import { cached } from "tracked-toolbox";
2
3
 
3
4
  import { getAST, getTransforms } from "@projectcaluma/ember-core/utils/jexl";
4
5
 
@@ -47,79 +48,43 @@ export function getDependenciesFromJexl(jexl, expression) {
47
48
  }
48
49
 
49
50
  /**
50
- * Computed property to get all nested dependency parents of an expression. A
51
- * nested dependency parent would be a table field that is used with a mapby
52
- * transform in the JEXL expression.
53
- *
54
- * E.g: 'foo'|answer in 'bar'|answer|mapby('column') where 'bar' would be a
55
- * nested dependency parent.
56
- *
57
- * Those need to be extracted seperately since the overall dependencies need to
58
- * depend on the values of the nested dependency parents to recompute
59
- * correctly.
51
+ * Getter to extract all fields used in an expression.
60
52
  *
61
53
  * @param {String} expressionPath The path of the expression
62
- * @return {Field[]} Returns an array of nested dependency parent fields
54
+ * @return {Field[]} An array of all dependency fields
63
55
  */
64
- export function nestedDependencyParents(expressionPath) {
65
- return dependencies(expressionPath, { onlyNestedParents: true });
66
- }
67
-
68
- /**
69
- * Computed property to get all dependencies of an expression.
70
- *
71
- * @param {String} expressionPath The path of the expression
72
- * @param {Object} options
73
- * @param {Boolean} options.onlyNestedParents Only include nested parent fields
74
- * @param {String} options.nestedParentsPath Path of the nested parent fields to trigger recomputation
75
- * @return {Field[]} Returns an array of all dependency fields
76
- */
77
- export function dependencies(
78
- expressionPath,
79
- { onlyNestedParents = false, nestedParentsPath = null } = {}
80
- ) {
81
- // If there are nested parents we need to recompute the property if their
82
- // values change
83
- const nestedTriggers = nestedParentsPath
84
- ? [`${nestedParentsPath}.@each.value`]
85
- : [];
86
-
87
- return computed(
88
- "document.{jexl,fields.[]}",
89
- expressionPath,
90
- ...nestedTriggers,
91
- function () {
92
- const expression = get(this, expressionPath);
93
-
94
- if (!expression) return [];
95
-
96
- const slugs = getDependenciesFromJexl(this.document.jexl, expression);
97
-
98
- return slugs
99
- .flatMap((slug) => {
100
- const [fieldSlug, nestedSlug = null] = slug.split(".");
101
-
102
- if (onlyNestedParents && !nestedSlug) {
103
- return null;
104
- }
105
-
106
- const field = this.document.findField(fieldSlug);
107
-
108
- if (!onlyNestedParents && nestedSlug && field?.value) {
109
- // Get the nested fields from the parents value (rows)
110
- const childFields =
111
- nestedSlug === "__all__"
112
- ? field.value.flatMap((row) => row.fields)
113
- : field.value.map((row) => row.findField(nestedSlug));
114
-
115
- return [field, ...childFields];
116
- }
117
-
118
- return [field];
119
- })
120
- .filter(Boolean);
121
- }
122
- );
56
+ export function dependencies(expressionPath) {
57
+ return function (target, key) {
58
+ return cached(target, key, {
59
+ get() {
60
+ const expression = get(this, expressionPath);
61
+
62
+ if (!expression) return [];
63
+
64
+ const slugs = getDependenciesFromJexl(this.document.jexl, expression);
65
+
66
+ return slugs
67
+ .flatMap((slug) => {
68
+ const [fieldSlug, nestedSlug = null] = slug.split(".");
69
+
70
+ const field = this.document.findField(fieldSlug);
71
+
72
+ if (nestedSlug && field?.value) {
73
+ // Get the nested fields from the parents value (rows)
74
+ const childFields =
75
+ nestedSlug === "__all__"
76
+ ? field.value.flatMap((row) => row.fields)
77
+ : field.value.map((row) => row.findField(nestedSlug));
78
+
79
+ return [field, ...childFields];
80
+ }
81
+
82
+ return [field];
83
+ })
84
+ .filter(Boolean);
85
+ },
86
+ });
87
+ };
123
88
  }
124
89
 
125
90
  export default dependencies;