This repository has been archived by the owner on Sep 10, 2022. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 21
/
model-view.html
292 lines (250 loc) · 9.62 KB
/
model-view.html
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
<!--
Copyright 2016 Google Inc.
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.
-->
<link rel="import" href="../polymer/polymer-element.html">
<link rel="import" href="action-emitter.html">
<script>
var UniFlow = window.UniFlow || {};
/**
Element rendering data represented by a single object (model) in the
application state should use ModelView mixin. Model View is a powerful
concept that encapsulates model data (likely the data received from the
server and to be persisted to the server if modified as a result of user
actions), status (validity of the data, flag that data was modified,
notifications for the user, etc.). Auxiliary data supplied by action
dispatchers and needed for display purposes or element's logic
should be defined as element’s properties. Same applies to data
created/modified by the element but not intended to be persisted.
If `StateAware` mixin is used along with `ModelView`, you can take advantage
of statePath property that indicates path to the element's state in the
application state tree. Whenever any data is mutated by action dispatchers
at statePath or below, the element will receive notification of its
properties' change (even if there is no explicit binding for those
properties). See `UniFlow.StateAware` for more details and example.
ModelView mixin defines some properties that are intended to be overridden
in the elements:
+ `validation` property allows to specify validation rules
that will be applied when validateModel() method is called. As a result of
this method validation status will be updated to indicate result for each
model field that has validation rule associated with it.
+ `saveAction` property indicates which action should be emitted when
saveModel method is called to perform save of the model.
+ `getMessage` should be overridden with the function returning message
string for given error code (to translate validation error code to message)
### Example:
#### HTML:
<template>
Model: [[model.id]]
<paper-input value="{{model.name}}"
label="Name"
invalid="[[status.validation.name.invalid]]"
error-message="[[status.validation.name.errorMessage]]">
</paper-input>
<paper-button on-tap="onSaveTap">Save</paper-button>
</template>
#### JavaScript:
class MyModel extends UniFlow.ModelView(Polymer.Element) {
static get is() { return "my-model"; }
get saveAction() { return 'MY_SAVE' }
get validation() { return {
name: (value) => {
if (!value || !value.trim()) {
return 'Name is not specified';
}
}
}}
connectedCallback() {
this.super();
this.fetchData();
},
fetchData() {
this.emitAction({
type: 'MY_FETCH',
path: 'model'
});
},
onSaveTap() {
this.validateAndSave();
}
}
customElements.define(MyModel.is, MyModel);
In the example above model view has input field for `name` property and Save button. On
element attach the action is emitted to fetch the model's data. Note that in `emitAction()` method
the path is specified as `'model'`. ActionEmitter mixin is responsible of expanding the path
with element's state path, ensuring that when action dispatcher gets to process the action, the
path contains full path in the state tree. So assuming that `my-model` is declared as follows:
<my-model state-path="state.myModel"></my-model>
the path in `MY_FETCH` action gets expanded to `state.myModel.model`.
`validation` property is an object that contains methods for fields validation. The keys in
this object should match model field names, the values are validation methods. Method receives
current value of the field and should return non-falsy value (string or error code) if the value
of the field didn't pass validation. `status.validation` object will be populated with the results
of validation with the keys matching field names and values being objects containing two fields:
- `invalid`: true when the value is not valid
- `errorMessage`: the message to show to user
So in the example above if user clicks on Save button with name not entered, they will get
'Name is not specified' error message on the input element. When the name is non-empty, validation
will pass and `MY_SAVE` action will be emitted with model passed as a parameter and `'model'` as
path.
@polymer
@mixinFunction
@appliesMixin UniFlow.ActionEmitter
*/
UniFlow.ModelView = Polymer.dedupingMixin((base) =>
class extends UniFlow.ActionEmitter(base) {
static get properties() {
return {
/**
* Object containing model data, usually mirroring server-side object.
*/
model: {
type: Object
},
/**
* Object to contain model status, including validity of the data,
* flag that data was modified, notifications for the user, etc.
*/
status: {
type: Object
}
}
}
/**
* Validation rules for model properties (optional), should be defined in the
* element.
* @type {Object|undefined}
*/
get validation() {
return undefined;
}
/**
* Save action that will be emitted when saveModel() method is called without
* parameters.
* @type {string|undefined}
*/
get saveAction() {
return undefined;
}
/**
* Function that translates error code (numeric or text) into human readable
* error message (used to translate validation error code into error text).
* @type {Function|undefined}
*/
getMessage() {
return undefined;
}
static get observers() {
return [
'modelViewModelChanged_(model.*)'
]
}
/**
* Method emitting passed action or this.saveAction, sending model with
* the action options.
* @param {Object|string=} action
*/
saveModel(action) {
let actionToEmit = {
model: this.model
};
if (typeof action === 'object') {
Object.assign(actionToEmit, action);
} else {
actionToEmit.type = action;
}
if (!actionToEmit.type) {
actionToEmit.type = this.saveAction;
}
if (!actionToEmit.path) {
actionToEmit.path = 'model';
}
this.emitAction(actionToEmit);
}
/**
* Method initializes status.validation object with invalid = false for all
* keys defined in this.validation object. This is needed for proper UI
* binding (if the value of invalid attribute is undefined, paper-input is
* misbehaving).
* @private
*/
initValidationStatus_() {
let validationStatus = {};
if (this.validation) {
for (let key of Object.keys(this.validation)) {
validationStatus[key] = {
invalid: false
};
}
}
this.set('status.validation', validationStatus);
}
/**
* Performs validation of model object according to rules defined in
* this.validation object. Sets status.validation.<property-name> fields with
* two properties: invalid and errorMessage.
*
* @return {boolean} True if all fields validated successfully (or
* this.validation is not defined in the element).
*/
validateModel() {
if (!this.validation) {
return true;
}
let isValid = true;
for (let key of Object.keys(this.validation)) {
let result = this.validation[key].call(this,
this.get('model.' + key));
let errorMessage = !result || typeof result === 'string' ?
result : (this.getMessage ? this.getMessage(result) :
'Message Code ' + result);
this.set('status.validation.' + key, {
invalid: !!result,
errorMessage
});
if (result) {
isValid = false;
}
}
return isValid;
}
/**
* Validates and saves model if there were no validation errors.
* @param {string=} action Optional action type to emit for save action.
*/
validateAndSave(action) {
if (this.validateModel()) {
this.saveModel(action);
}
}
/**
* Observer of any changes to the model. Resets status object and initializes
* validation status.
* @param {!Object} change
* @private
*/
modelViewModelChanged_(change) {
// Resetting status when model changed
if (change.path === 'model' && this.get('model')) {
this.set('status', {
isModified: false
});
this.initValidationStatus_();
} else {
if (this.get('model')) {
this.set('status.isModified', true);
} else {
this.set('status', {});
}
}
}
});
</script>