rasti 1.1.2 → 2.0.0-alpha.0
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.
- package/README.md +21 -27
- package/dist/rasti.js +14 -4
- package/dist/rasti.min.js +1 -1
- package/es/Component.js +398 -0
- package/es/Emitter.js +29 -11
- package/es/Model.js +34 -26
- package/es/View.js +22 -39
- package/es/index.js +2 -1
- package/lib/Component.js +402 -0
- package/lib/Emitter.js +29 -11
- package/lib/Model.js +34 -26
- package/lib/View.js +22 -39
- package/lib/index.js +2 -0
- package/package.json +4 -2
- package/src/Component.js +397 -0
- package/src/Emitter.js +29 -11
- package/src/Model.js +34 -26
- package/src/View.js +22 -39
- package/src/index.js +2 -1
package/src/Component.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import View from './View';
|
|
2
|
+
|
|
3
|
+
// This options keys will be extended on view instance.
|
|
4
|
+
const componentOptions = {
|
|
5
|
+
key : true,
|
|
6
|
+
state : true,
|
|
7
|
+
onCreate : true,
|
|
8
|
+
onChange : true,
|
|
9
|
+
onRender : true
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
/*
|
|
13
|
+
* Helper function. Extract attributes from html tag
|
|
14
|
+
* @param text {string} html text
|
|
15
|
+
* @return {object} Object with keys / values representing attributes.
|
|
16
|
+
*/
|
|
17
|
+
const getAttributes = text => {
|
|
18
|
+
const attributes = {};
|
|
19
|
+
const re = /([a-z]+)=?(\S+)?/gi;
|
|
20
|
+
|
|
21
|
+
let result;
|
|
22
|
+
while ((result = re.exec(text)) !== null) {
|
|
23
|
+
attributes[result[1]] = result[2] ?
|
|
24
|
+
result[2].replace(/'|"/g, '') : true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return attributes;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
/*
|
|
31
|
+
* Helper function. If expression is a function, call it with context and args.
|
|
32
|
+
*/
|
|
33
|
+
const evalExpression = (expression, context, ...args) =>
|
|
34
|
+
typeof expression === 'function' ?
|
|
35
|
+
expression.apply(context, args) :
|
|
36
|
+
expression;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Components are a special kind of `View` that is designed to be easily composable,
|
|
40
|
+
* making it simple to add child views and build complex user interfaces.<br />
|
|
41
|
+
* Unlike views, which are render-agnostic, components have a specific set of rendering
|
|
42
|
+
* guidelines that allow for a more declarative development style.
|
|
43
|
+
* @module
|
|
44
|
+
* @param {object} options Object containing options. The following keys will be merged to `this`: model, state, key, onDestroy, onRender, onCreate, onChange.
|
|
45
|
+
* @example
|
|
46
|
+
* import { Component, Model } from 'rasti';
|
|
47
|
+
* // Create Timer component.
|
|
48
|
+
* const Timer = Component.create`
|
|
49
|
+
* <div>Seconds: <span>${({ model }) => model.seconds}</span></div>
|
|
50
|
+
* `;
|
|
51
|
+
* // Create model to store seconds.
|
|
52
|
+
* const model = new Model({ seconds: 0 });
|
|
53
|
+
* // Mount timer on body.
|
|
54
|
+
* Timer.mount({ model }, document.body);
|
|
55
|
+
* // Increment `model.seconds` every second.
|
|
56
|
+
* setInterval(() => model.seconds++, 1000);
|
|
57
|
+
*/
|
|
58
|
+
export default class Component extends View {
|
|
59
|
+
constructor(options = {}) {
|
|
60
|
+
super(options);
|
|
61
|
+
// Extend "this" with options, mapping componentOptions keys.
|
|
62
|
+
Object.keys(options).forEach(key => {
|
|
63
|
+
if (componentOptions[key]) this[key] = options[key];
|
|
64
|
+
});
|
|
65
|
+
// Store options by default.
|
|
66
|
+
this.options = options;
|
|
67
|
+
// Ensure id.
|
|
68
|
+
this.id = this.attributes.id ?
|
|
69
|
+
// If id is provided, evaluate it.
|
|
70
|
+
evalExpression(this.attributes.id, this, this) :
|
|
71
|
+
// Generate a unique id and set it as id attribute.
|
|
72
|
+
`rasti-component-${this.uid}`;
|
|
73
|
+
// Bind onChange to this to be used as listener.
|
|
74
|
+
// Store bound version, so it can be removed on onDestroy method.
|
|
75
|
+
this.onChange = this.onChange.bind(this);
|
|
76
|
+
// Listen to model changes and call onChange.
|
|
77
|
+
if (this.model) this.model.on('change', this.onChange);
|
|
78
|
+
if (this.state) this.state.on('change', this.onChange);
|
|
79
|
+
// Call life cycle method.
|
|
80
|
+
this.onCreate.apply(this, arguments);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/*
|
|
84
|
+
* Override. We don't want to ensure an element on instantiation.
|
|
85
|
+
* We will provide it later.
|
|
86
|
+
*/
|
|
87
|
+
ensureElement() {
|
|
88
|
+
// If el is provided, delegate events.
|
|
89
|
+
if (this.el) this.delegateEvents();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
findElement(parent) {
|
|
93
|
+
return (parent || document).querySelector(`#${this.id}`);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/*
|
|
97
|
+
* Used internally on the render process. Attach the view to the dom element.
|
|
98
|
+
*/
|
|
99
|
+
hydrate(parent) {
|
|
100
|
+
this.el = this.findElement(parent);
|
|
101
|
+
this.delegateEvents();
|
|
102
|
+
this.children.forEach(child => child.hydrate(this.el));
|
|
103
|
+
this.onRender.call(this);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/*
|
|
107
|
+
* Override. Add some custom logic to super `destroy` method.
|
|
108
|
+
*/
|
|
109
|
+
destroy(options) {
|
|
110
|
+
super.destroy(options);
|
|
111
|
+
// Stop listening to `change`.
|
|
112
|
+
// Set destroyed flag to prevent a last render after destroyed. TODO: Review
|
|
113
|
+
if (this.model) this.model.off('change', this.onChange);
|
|
114
|
+
if (this.state) this.state.off('change', this.onChange);
|
|
115
|
+
this.destroyed = true;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Lifecycle method. Called when the view is created at the end of the constructor.
|
|
120
|
+
* @param options {object} The view options.
|
|
121
|
+
*/
|
|
122
|
+
onCreate() {}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Lifecycle method. Called when model emits `change` event.
|
|
126
|
+
* By default calls render method.
|
|
127
|
+
* This method should be extended with custom logic.
|
|
128
|
+
* Maybe comparing new attributes with previous ones and calling
|
|
129
|
+
* render when needed. Or doing some dom transformation.
|
|
130
|
+
* @param model {Rasti.Model}
|
|
131
|
+
* @param key {string}
|
|
132
|
+
* @param value {any}
|
|
133
|
+
*/
|
|
134
|
+
onChange() {
|
|
135
|
+
this.render();
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Lifecycle method. Called when the view is rendered.
|
|
140
|
+
*/
|
|
141
|
+
onRender() {}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Lifecycle method. Called when the view is destroyed.
|
|
145
|
+
*/
|
|
146
|
+
onDestroy() {}
|
|
147
|
+
|
|
148
|
+
/*
|
|
149
|
+
* Replace expressions.
|
|
150
|
+
*/
|
|
151
|
+
replaceExpressions(string, parseExpressionResult) {
|
|
152
|
+
return string
|
|
153
|
+
.replace(/{(\d+)}/g, (match) => {
|
|
154
|
+
// Get expression index.
|
|
155
|
+
const idx = match.match(/{(\d+)}/)[1];
|
|
156
|
+
// Eval expression. Pass view as argument.
|
|
157
|
+
const result = this.template.expressions[idx].call(this, this);
|
|
158
|
+
// Treat all expressions as arrays.
|
|
159
|
+
const results = result instanceof Array ? result : [result];
|
|
160
|
+
// Replace expression with the result of the evaluation.
|
|
161
|
+
return results.reduce((out, current) => {
|
|
162
|
+
// Concatenate expressions.
|
|
163
|
+
out += parseExpressionResult(current);
|
|
164
|
+
return out;
|
|
165
|
+
}, '');
|
|
166
|
+
|
|
167
|
+
})
|
|
168
|
+
// Replace `attribute="true"` with `attribute`
|
|
169
|
+
.replace(/([a-z]+)=["|']true["|']/g, '$1')
|
|
170
|
+
// Replace `attribute="false"` with ``
|
|
171
|
+
.replace(/([a-z]+)=["|']false["|']/g, '');
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/*
|
|
175
|
+
* Treat the whole view as a HTML string.
|
|
176
|
+
*/
|
|
177
|
+
toString() {
|
|
178
|
+
// Normally there won't be any children, but if there are, destroy them.
|
|
179
|
+
this.destroyChildren();
|
|
180
|
+
// Add id to template including root element (this.el) as part of it.
|
|
181
|
+
const tpl = this.attributes.id ?
|
|
182
|
+
this.template.outer :
|
|
183
|
+
this.template.outer.replace(/^<([a-z]+)/, `<$1 id="${this.id}"`);
|
|
184
|
+
// Replace expressions.
|
|
185
|
+
return this.replaceExpressions(tpl, (result) => {
|
|
186
|
+
let out = result;
|
|
187
|
+
// Return empty string if result is null or undefined.
|
|
188
|
+
if (result === null || typeof result === 'undefined') out = '';
|
|
189
|
+
// If result is a view, add it to children array.
|
|
190
|
+
else if (result && typeof result.render === 'function') {
|
|
191
|
+
// Add child component.
|
|
192
|
+
this.addChild(result);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return out;
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/*
|
|
200
|
+
* View render method.
|
|
201
|
+
*/
|
|
202
|
+
render() {
|
|
203
|
+
// Prevent a last re render if view is already destroyed.
|
|
204
|
+
if (this.destroyed) return this;
|
|
205
|
+
// If "this.el" is not present,
|
|
206
|
+
// create a new element according "this.tag"
|
|
207
|
+
// and "this.attributes".
|
|
208
|
+
if (!this.el) {
|
|
209
|
+
this.el = this.createElement(this.tag);
|
|
210
|
+
this.delegateEvents();
|
|
211
|
+
}
|
|
212
|
+
// Set `this.el` attributes.
|
|
213
|
+
Object.keys(this.attributes).forEach(key => {
|
|
214
|
+
// Omit changing id.
|
|
215
|
+
if (key === 'id') return;
|
|
216
|
+
// Evaluate attribute value.
|
|
217
|
+
let value = evalExpression(this.attributes[key], this, this);
|
|
218
|
+
// Transform bool attribute values
|
|
219
|
+
if (value === false) {
|
|
220
|
+
// Remove false attributes.
|
|
221
|
+
this.el.removeAttribute(key);
|
|
222
|
+
} else {
|
|
223
|
+
if (value === true) value = '';
|
|
224
|
+
this.el.setAttribute(key, value);
|
|
225
|
+
}
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
const previousChildren = this.children;
|
|
229
|
+
this.children = [];
|
|
230
|
+
|
|
231
|
+
const nextChildren = [];
|
|
232
|
+
const recycledChildren = [];
|
|
233
|
+
|
|
234
|
+
// Replace expressions.
|
|
235
|
+
// Set html text inside `this.el`. Make it part of the dom.
|
|
236
|
+
this.el.innerHTML = this.replaceExpressions(this.template.inner, (result) => {
|
|
237
|
+
let out = result;
|
|
238
|
+
// Return empty string if result is null or undefined.
|
|
239
|
+
if (result === null || typeof result === 'undefined') out = '';
|
|
240
|
+
|
|
241
|
+
// If result is a view, add it to children array.
|
|
242
|
+
else if (result && typeof result.render === 'function') {
|
|
243
|
+
const found = result.key && previousChildren.find(
|
|
244
|
+
previousChild => previousChild.key === result.key
|
|
245
|
+
);
|
|
246
|
+
|
|
247
|
+
if (found) {
|
|
248
|
+
out = `<${found.tag} id="${evalExpression(found.id, found, found)}"></${found.tag}>`;
|
|
249
|
+
recycledChildren.push(found);
|
|
250
|
+
result.destroy();
|
|
251
|
+
} else {
|
|
252
|
+
nextChildren.push(result);
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
return out;
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
nextChildren.forEach(nextChild => {
|
|
260
|
+
this.addChild(nextChild).hydrate(this.el);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
recycledChildren.forEach(recycledChild => {
|
|
264
|
+
this.el.replaceChild(this.addChild(recycledChild).el, recycledChild.findElement(this.el));
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
previousChildren.forEach(previousChild => {
|
|
268
|
+
const found = recycledChildren.indexOf(previousChild) > -1;
|
|
269
|
+
if (!found) previousChild.destroy();
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
// Call onRender lifecycle method.
|
|
273
|
+
this.onRender.call(this);
|
|
274
|
+
// Return this for chaining.
|
|
275
|
+
return this;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Helper method used to extend a `Component`, creating a subclass.
|
|
280
|
+
* @static
|
|
281
|
+
* @param {object} object Object containing methods to be added to the new `Component` subclass. Also can be a function that receives the parent prototype and returns an object.
|
|
282
|
+
*/
|
|
283
|
+
static extend(object) {
|
|
284
|
+
const Current = this;
|
|
285
|
+
|
|
286
|
+
class Extended extends Current {}
|
|
287
|
+
|
|
288
|
+
Object.assign(
|
|
289
|
+
Extended.prototype,
|
|
290
|
+
typeof object === 'function' ? object(Current.prototype) : object
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
return Extended;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Mount the component into the dom.
|
|
298
|
+
* It instantiate the Component view using options,
|
|
299
|
+
* appends its element into the DOM (if `el` is provided).
|
|
300
|
+
* And returns the view instance.
|
|
301
|
+
* @static
|
|
302
|
+
* @param {object} options The view options.
|
|
303
|
+
* @param {node} el Dom element to append the view element.
|
|
304
|
+
* @param {boolean} hydrate If true, the view will use existing html.
|
|
305
|
+
* @return {Rasti.View}
|
|
306
|
+
*/
|
|
307
|
+
static mount(options = {}, el, hydrate) {
|
|
308
|
+
// Instantiate view.
|
|
309
|
+
const view = new this(options);
|
|
310
|
+
|
|
311
|
+
// If `el` is passed, mount component.
|
|
312
|
+
if (el) {
|
|
313
|
+
if (hydrate) {
|
|
314
|
+
view.toString();
|
|
315
|
+
} else {
|
|
316
|
+
const fragment = document.createElement('template');
|
|
317
|
+
// Add html text into element inner html.
|
|
318
|
+
fragment.innerHTML = view;
|
|
319
|
+
// Add to dom.
|
|
320
|
+
el.appendChild(fragment.content);
|
|
321
|
+
}
|
|
322
|
+
view.hydrate(el);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
return view;
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Tagged template that receives an HTML string,
|
|
329
|
+
* and returns a `Component`.
|
|
330
|
+
* @static
|
|
331
|
+
*/
|
|
332
|
+
static create(strings, ...expressions) {
|
|
333
|
+
const parts = [];
|
|
334
|
+
// Replace functions and objects interpolations with `{number}`.
|
|
335
|
+
// Where `number` is the index on expressions array.
|
|
336
|
+
strings.forEach((string, i) => {
|
|
337
|
+
// Add string part.
|
|
338
|
+
parts.push(string);
|
|
339
|
+
// Add expression placeholder for later or expression eval.
|
|
340
|
+
if (expressions[i]) {
|
|
341
|
+
parts.push(
|
|
342
|
+
typeof expressions[i] === 'function' || typeof expressions[i] === 'object' ?
|
|
343
|
+
`{${i}}` :
|
|
344
|
+
expressions[i]
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
});
|
|
348
|
+
// Create output text for main template.
|
|
349
|
+
const main = parts.join('').trim().replace(/\n/g, '');
|
|
350
|
+
// Extract outer tag, attributes and inner html.
|
|
351
|
+
const result = main.match(/^<([a-z]+)(.*?)>(.*)<\/\1>$/);
|
|
352
|
+
// Remove events listeners.
|
|
353
|
+
const string = main.replace(/on([A-Z]{1}[a-z]+)+=[^>\s]+/g, '');
|
|
354
|
+
// Parse attributes from html text into an object.
|
|
355
|
+
let attributes = getAttributes(result[2]);
|
|
356
|
+
let events = {};
|
|
357
|
+
// Filter events. Replace placeholders `{number}` with expressions.
|
|
358
|
+
attributes = Object.keys(attributes).reduce((out, key) => {
|
|
359
|
+
const matchKey = key.match(/on(([A-Z]{1}[a-z]+)+)/); // Is Event?
|
|
360
|
+
const matchValue = attributes[key].match(/{(\d+)}/); // Is placeholder for function or object?
|
|
361
|
+
// Get expression or value.
|
|
362
|
+
const value = matchValue && matchValue[1] ? expressions[matchValue[1]] : attributes[key];
|
|
363
|
+
// Is event handler.
|
|
364
|
+
if (matchKey && matchKey[1]) {
|
|
365
|
+
const eventType = matchKey[1].toLowerCase();
|
|
366
|
+
Object.keys(value).forEach(
|
|
367
|
+
selector => events[`${eventType}${selector === '&' ? '' : ` ${selector}`}`] = value[selector]
|
|
368
|
+
);
|
|
369
|
+
return out;
|
|
370
|
+
}
|
|
371
|
+
// Is function expression.
|
|
372
|
+
out[key] = value;
|
|
373
|
+
return out;
|
|
374
|
+
}, {});
|
|
375
|
+
|
|
376
|
+
const Current = this;
|
|
377
|
+
|
|
378
|
+
// Create subclass for this component.
|
|
379
|
+
return Current.extend({
|
|
380
|
+
// Set events.
|
|
381
|
+
events,
|
|
382
|
+
// Set attributes.
|
|
383
|
+
attributes,
|
|
384
|
+
// Set template data.
|
|
385
|
+
template : {
|
|
386
|
+
// Template including root element (this.el) as part of the template.
|
|
387
|
+
outer: string,
|
|
388
|
+
// Template for innerHTML of root element.
|
|
389
|
+
inner: result[3],
|
|
390
|
+
// Template expressions.
|
|
391
|
+
expressions,
|
|
392
|
+
},
|
|
393
|
+
// Set root element tag.
|
|
394
|
+
tag : result[1]
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
}
|
package/src/Emitter.js
CHANGED
|
@@ -1,21 +1,39 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Emitter is a class that
|
|
3
|
-
*
|
|
2
|
+
* `Emitter` is a class that provides an easy way to implement the observer pattern
|
|
3
|
+
* in your applications.<br />
|
|
4
|
+
* It can be extended to create new classes that have the ability to emit and bind custom named events.<br />
|
|
5
|
+
* Emitter is used by `Model` and `View` classes, which inherit from it to implement
|
|
6
|
+
* event-driven functionality.
|
|
7
|
+
*
|
|
4
8
|
* @module
|
|
5
9
|
* @example
|
|
6
10
|
* import { Emitter } from 'rasti';
|
|
7
|
-
* //
|
|
8
|
-
* class
|
|
11
|
+
* // Custom cart
|
|
12
|
+
* class ShoppingCart extends Emitter {
|
|
9
13
|
* constructor() {
|
|
10
|
-
* super()
|
|
11
|
-
*
|
|
12
|
-
*
|
|
14
|
+
* super();
|
|
15
|
+
* this.items = [];
|
|
16
|
+
* }
|
|
17
|
+
*
|
|
18
|
+
* addItem(item) {
|
|
19
|
+
* this.items.push(item);
|
|
20
|
+
* // Emit a custom event called `itemAdded`.
|
|
21
|
+
* // Pass the added item as an argument to the event listener.
|
|
22
|
+
* this.emit('itemAdded', item);
|
|
13
23
|
* }
|
|
14
24
|
* }
|
|
15
|
-
* //
|
|
16
|
-
* const
|
|
17
|
-
* //
|
|
18
|
-
*
|
|
25
|
+
* // Create an instance of ShoppingCart and Logger
|
|
26
|
+
* const cart = new ShoppingCart();
|
|
27
|
+
* // Listen to the `itemAdded` event and log the added item using the logger.
|
|
28
|
+
* cart.on('itemAdded', (item) => {
|
|
29
|
+
* console.log(`Item added to cart: ${item.name} - Price: $${item.price}`);
|
|
30
|
+
* });
|
|
31
|
+
* // Simulate adding items to the cart
|
|
32
|
+
* const item1 = { name : 'Smartphone', price : 1000 };
|
|
33
|
+
* const item2 = { name : 'Headphones', price : 150 };
|
|
34
|
+
*
|
|
35
|
+
* cart.addItem(item1); // Output: "Item added to cart: Smartphone - Price: $1000"
|
|
36
|
+
* cart.addItem(item2); // Output: "Item added to cart: Headphones - Price: $150"
|
|
19
37
|
*/
|
|
20
38
|
export default class Emitter {
|
|
21
39
|
/**
|
package/src/Model.js
CHANGED
|
@@ -3,37 +3,45 @@ import Emitter from './Emitter';
|
|
|
3
3
|
* - Orchestrates data and business logic.
|
|
4
4
|
* - Emits events when data changes.
|
|
5
5
|
*
|
|
6
|
-
* A Model manages an internal table of data attributes
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* And a setter, which sets the model property in `this.attributes` and emits `change` and `change:attribute` events.
|
|
6
|
+
* A `Model` manages an internal table of data attributes and triggers change events when any of its data is modified.<br />
|
|
7
|
+
* Models may handle syncing data with a persistence layer. To design your models, create atomic, reusable objects
|
|
8
|
+
* that contain all the necessary functions for manipulating their specific data.<br />
|
|
9
|
+
* Models should be easily passed throughout your app and used anywhere the corresponding data is needed.<br />
|
|
10
|
+
* Rasti models stores its attributes in `this.attributes`, which is extended from `this.defaults` and the
|
|
11
|
+
* constructor `attrs` parameter. For every attribute, a getter is generated to retrieve the model property
|
|
12
|
+
* from `this.attributes`, and a setter is created to set the model property in `this.attributes` and emit `change`
|
|
13
|
+
* and `change:attribute` events.
|
|
15
14
|
* @module
|
|
16
15
|
* @param {object} attrs Object containing model attributes to extend `this.attributes`. Getters and setters are generated for `this.attributtes`, in order to emit `change` events.
|
|
17
16
|
* @example
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
*
|
|
22
|
-
* //
|
|
23
|
-
*
|
|
17
|
+
* import { Model } from 'rasti';
|
|
18
|
+
* // Product model
|
|
19
|
+
* class ProductModel extends Model {
|
|
20
|
+
* preinitialize() {
|
|
21
|
+
* // The Product model has `name` and `price` default attributes.
|
|
22
|
+
* // `defaults` will extend `this.attributes`.
|
|
23
|
+
* // Getters and setters are generated for `this.attributes`,
|
|
24
|
+
* // in order to emit `change` events.
|
|
25
|
+
* this.defaults = {
|
|
26
|
+
* name: '',
|
|
27
|
+
* price: 0
|
|
28
|
+
* };
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* setDiscount(discountPercentage) {
|
|
32
|
+
* // Apply a discount to the price property.
|
|
33
|
+
* // This will call a setter that will update `price` in `this.attributes`,
|
|
34
|
+
* // and emit `change` and `change:price` events.
|
|
35
|
+
* const discount = this.price * (discountPercentage / 100);
|
|
36
|
+
* this.price -= discount;
|
|
24
37
|
* }
|
|
25
38
|
* }
|
|
26
|
-
* //
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
* const todo = new TodoModel({ title : 'Learn Rasti' });
|
|
33
|
-
* // Listen to `change:completed` event.
|
|
34
|
-
* todo.on('change:completed', () => console.log('Completed:', todo.completed));
|
|
35
|
-
* // Complete todo.
|
|
36
|
-
* todo.toggle(); // Completed: true
|
|
39
|
+
* // Create a product instance with a name and price.
|
|
40
|
+
* const product = new ProductModel({ name: 'Smartphone', price: 1000 });
|
|
41
|
+
* // Listen to the `change:price` event.
|
|
42
|
+
* product.on('change:price', () => console.log('New Price:', product.price));
|
|
43
|
+
* // Apply a 10% discount to the product.
|
|
44
|
+
* product.setDiscount(10); // Output: "New Price: 900"
|
|
37
45
|
*/
|
|
38
46
|
export default class Model extends Emitter {
|
|
39
47
|
constructor(attrs = {}) {
|
package/src/View.js
CHANGED
|
@@ -16,12 +16,13 @@ const viewOptions = {
|
|
|
16
16
|
* - Handles user input and interactivity.
|
|
17
17
|
* - Sends captured input to the model.
|
|
18
18
|
*
|
|
19
|
-
* A View is an atomic
|
|
20
|
-
*
|
|
21
|
-
* Models must be unaware of views.
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
19
|
+
* A `View` is an atomic unit of the user interface that can render the data from a specific model or multiple models.
|
|
20
|
+
* However, views can also be independent and have no associated data.<br />
|
|
21
|
+
* Models must be unaware of views. Views, on the other hand, may render model data and listen to the change events
|
|
22
|
+
* emitted by the models to re-render themselves based on changes.<br />
|
|
23
|
+
* Each `View` has a root element, `this.el`, which is used for event delegation.<br />
|
|
24
|
+
* All element lookups are scoped to this element, and any rendering or DOM manipulations should be done inside it.
|
|
25
|
+
* If `this.el` is not present, an element will be created using `this.tag` (defaulting to div) and `this.attributes`.
|
|
25
26
|
* @module
|
|
26
27
|
* @param {object} options Object containing options. The following keys will be merged to `this`: el, tag, attributes, events, model, template, onDestroy.
|
|
27
28
|
* @property {node} el Every view has a root element, `this.el`. If not present it will be created.
|
|
@@ -31,43 +32,25 @@ const viewOptions = {
|
|
|
31
32
|
* @property {object} model A `Rasti.Model` or any object containing data and business logic.
|
|
32
33
|
* @property {function} template A function that receives data and returns a markup string (html for example).
|
|
33
34
|
* @example
|
|
34
|
-
*
|
|
35
|
-
*
|
|
35
|
+
* import { View } from 'rasti';
|
|
36
|
+
*
|
|
37
|
+
* class Timer extends View {
|
|
36
38
|
* constructor(options) {
|
|
37
39
|
* super(options);
|
|
38
|
-
* //
|
|
39
|
-
* this.
|
|
40
|
-
* // Listen to model
|
|
41
|
-
* this.model.on('change:
|
|
40
|
+
* // Create model to store internal state. Set `seconds` attribute into 0.
|
|
41
|
+
* this.model = new Model({ seconds : 0 });
|
|
42
|
+
* // Listen to changes in model `seconds` attribute and re render.
|
|
43
|
+
* this.model.on('change:seconds', this.render.bind(this));
|
|
44
|
+
* // Increment model `seconds` attribute every 1000 milliseconds.
|
|
45
|
+
* this.interval = setInterval(() => this.model.seconds++, 1000);
|
|
42
46
|
* }
|
|
43
|
-
*
|
|
44
|
-
*
|
|
45
|
-
*
|
|
46
|
-
* }
|
|
47
|
-
* // Listener method. Called when button is clicked.
|
|
48
|
-
* onClickIncrement() {
|
|
49
|
-
* // Increment count on model.
|
|
50
|
-
* this.model.count++;
|
|
47
|
+
*
|
|
48
|
+
* template(model) {
|
|
49
|
+
* return `Seconds: <span>${model.seconds}</span>`;
|
|
51
50
|
* }
|
|
52
51
|
* }
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* // Call `onClickIncrement` when button is clicked.
|
|
56
|
-
* events : {
|
|
57
|
-
* 'click button' : 'onClickIncrement'
|
|
58
|
-
* },
|
|
59
|
-
* // View's template.
|
|
60
|
-
* template : (model) => `
|
|
61
|
-
* <div>The count is: ${model.count}</div>
|
|
62
|
-
* <button>Increment</button>
|
|
63
|
-
* `
|
|
64
|
-
* });
|
|
65
|
-
* // Model.
|
|
66
|
-
* const model = new Model({ count : 0 });
|
|
67
|
-
* // Instantiate CounterView.
|
|
68
|
-
* const counterView = new CounterView({ model });
|
|
69
|
-
* // Add to DOM.
|
|
70
|
-
* document.body.appendChild(counterView.render().el);
|
|
52
|
+
* // Render view and append view's element into body.
|
|
53
|
+
* document.body.appendChild(new Timer().render().el);
|
|
71
54
|
*/
|
|
72
55
|
export default class View extends Emitter {
|
|
73
56
|
constructor(options = {}) {
|
|
@@ -133,7 +116,7 @@ export default class View extends Emitter {
|
|
|
133
116
|
// Remove `this.el` if "options.remove" is true.
|
|
134
117
|
if (remove) this.removeElement();
|
|
135
118
|
// Call onDestroy lifecycle method
|
|
136
|
-
this.onDestroy();
|
|
119
|
+
this.onDestroy.apply(this, arguments);
|
|
137
120
|
}
|
|
138
121
|
|
|
139
122
|
/**
|