forked from canjs/canjs
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcomponent.js
491 lines (425 loc) · 18 KB
/
component.js
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
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
// # can/component/component.js
//
// This implements the `can.Component` which allows you to create widgets
// that use a template, a view-model and custom tags.
//
// `can.Component` implements most of it's functionality in the `can.Component.setup`
// and the `can.Component.prototype.setup` functions.
//
// `can.Component.setup` prepares everything needed by the `can.Component.prototype.setup`
// to hookup the component.
steal("can/util", "can/view/callbacks","can/view/elements.js","can/view/bindings","can/control", "can/observe", "can/view/mustache", "can/util/view_model", function (can, viewCallbacks, elements, bindings) {
// ## Helpers
// Attribute names to ignore for setting viewModel values.
var paramReplacer = /\{([^\}]+)\}/g;
/**
* @add can.Component
*/
var Component = can.Component = can.Construct.extend(
// ## Static
/**
* @static
*/
{
// ### setup
//
// When a component is extended, this sets up the component's internal constructor
// functions and templates for later fast initialization.
setup: function () {
can.Construct.setup.apply(this, arguments);
// When `can.Component.setup` function is ran for the first time, `can.Component` doesn't exist yet
// which ensures that the following code is ran only in constructors that extend `can.Component`.
if (can.Component) {
var self = this,
protoViewModel = this.prototype.scope || this.prototype.viewModel;
// Define a control using the `events` prototype property.
this.Control = ComponentControl.extend( this.prototype.events );
// Look to convert `protoViewModel` to a Map constructor function.
if (!protoViewModel || (typeof protoViewModel === "object" && ! (protoViewModel instanceof can.Map) ) ) {
// If protoViewModel is an object, use that object as the prototype of an extended
// Map constructor function.
// A new instance of that Map constructor function will be created and
// set a the constructor instance's viewModel.
this.Map = can.Map.extend(protoViewModel || {});
}
else if (protoViewModel.prototype instanceof can.Map) {
// If viewModel is a can.Map constructor function, just use that.
this.Map = protoViewModel;
}
// Look for default `@` values. If a `@` is found, these
// attributes string values will be set and 2-way bound on the
// component instance's viewModel.
this.attributeScopeMappings = {};
can.each(this.Map ? this.Map.defaults : {}, function (val, prop) {
if (val === "@") {
self.attributeScopeMappings[prop] = prop;
}
});
// Convert the template into a renderer function.
if (this.prototype.template) {
// If `this.prototype.template` is a function create renderer from it by
// wrapping it with the anonymous function that will pass it the arguments,
// otherwise create the render from the string
if (typeof this.prototype.template === "function") {
var temp = this.prototype.template;
this.renderer = function () {
return can.view.frag(temp.apply(null, arguments));
};
} else {
this.renderer = can.view.mustache(this.prototype.template);
}
}
// Register this component to be created when its `tag` is found.
can.view.tag(this.prototype.tag, function (el, options) {
new self(el, options);
});
}
}
}, {
// ## Prototype
/**
* @prototype
*/
// ### setup
// When a new component instance is created, setup bindings, render the template, etc.
setup: function (el, componentTagData) {
// Setup values passed to component
var initialViewModelData = {},
component = this,
// If a template is not provided, we fall back to
// dynamic scoping regardless of settings.
lexicalContent = ((typeof this.leakScope === "undefined" ?
false :
!this.leakScope) &&
!!this.template),
// the object added to the scope
viewModel,
frag,
// an array of teardown stuff that should happen when the element is removed
teardownFunctions = [],
callTeardownFunctions = function(){
for(var i = 0, len = teardownFunctions.length ; i < len; i++) {
teardownFunctions[i]();
}
},
$el = can.$(el),
setupBindings = !can.data($el,"preventDataBindings");
// ## Scope
// Add viewModel prototype properties marked with an "@" to the `initialViewModelData` object
can.each(this.constructor.attributeScopeMappings, function (val, prop) {
initialViewModelData[prop] = el.getAttribute(can.hyphenate(val));
});
if(setupBindings) {
teardownFunctions.push(bindings.behaviors.viewModel(el, componentTagData, function(initialViewModelData){
// Make %root available on the viewModel.
initialViewModelData["%root"] = componentTagData.scope.attr("%root");
// Create the component's viewModel.
var protoViewModel = component.scope || component.viewModel;
if (component.constructor.Map) {
// If `Map` property is set on the constructor use it to wrap the `initialViewModelData`
viewModel = new component.constructor.Map(initialViewModelData);
} else if (protoViewModel instanceof can.Map) {
// If `component.viewModel` is instance of `can.Map` assign it to the `viewModel`
viewModel = protoViewModel;
} else if (can.isFunction(protoViewModel)) {
// If `component.viewModel` is a function, call the function and
var scopeResult = protoViewModel.call(component, initialViewModelData, componentTagData.scope, el);
if (scopeResult instanceof can.Map) {
// If the function returns a can.Map, use that as the viewModel
viewModel = scopeResult;
} else if (scopeResult.prototype instanceof can.Map) {
// If `scopeResult` is of a `can.Map` type, use it to wrap the `initialViewModelData`
viewModel = new scopeResult(initialViewModelData);
} else {
// Otherwise extend `can.Map` with the `scopeResult` and initialize it with the `initialViewModelData`
viewModel = new(can.Map.extend(scopeResult))(initialViewModelData);
}
}
var oldSerialize = viewModel.serialize;
viewModel.serialize = function () {
var result = oldSerialize.apply(this, arguments);
delete result["%root"];
return result;
};
return viewModel;
}, initialViewModelData));
}
// Set `viewModel` to `this.viewModel` and set it to the element's `data` object as a `viewModel` property
this.scope = this.viewModel = viewModel;
can.data($el, "scope", this.viewModel);
can.data($el, "viewModel", this.viewModel);
can.data($el,"preventDataBindings", true);
// Create a real Scope object out of the viewModel property
// The scope used to render the component's template.
// However, if there is no template, the "light" dom is rendered with this anyway.
var shadowScope;
if(lexicalContent) {
shadowScope = can.view.Scope.refsScope().add(this.viewModel,{viewModel: true});
} else {
// if this component has a template,
// render the template with it's own Refs scope
// otherwise, just add this component's viewModel.
shadowScope = ( this.constructor.renderer ?
componentTagData.scope.add( new can.view.Scope.Refs() ) :
componentTagData.scope )
.add(this.viewModel,{viewModel: true});
}
var options = {
helpers: {}
},
addHelper = function(name, fn) {
options.helpers[name] = function() {
return fn.apply(viewModel, arguments);
};
};
// ## Helpers
// Setup helpers to callback with `this` as the component
can.each(this.helpers || {}, function (val, prop) {
if (can.isFunction(val)) {
addHelper(prop, val);
}
});
// Setup simple helpers
can.each(this.simpleHelpers || {}, function(val, prop) {
//!steal-remove-start
if(options.helpers[prop]) {
can.dev.warn('Component ' + component.tag +
' already has a helper called ' + prop);
}
//!steal-remove-end
// Convert the helper
addHelper(prop, can.view.simpleHelper(val));
});
// ## `events` control
// Create a control to listen to events
this._control = new this.constructor.Control(el, {
// Pass the viewModel to the control so we can listen to it's changes from the controller.
scope: this.viewModel,
viewModel: this.viewModel,
destroy: callTeardownFunctions
});
// ## Rendering
// Keep a nodeList so we can kill any directly nested nodeLists within this component
var nodeList = can.view.nodeLists.register([], undefined, componentTagData.parentNodeList || true, false);
nodeList.expression = "<"+this.tag+">";
teardownFunctions.push(function(){
can.view.nodeLists.unregister(nodeList);
});
// If this component has a template (that we've already converted to a renderer)
if (this.constructor.renderer) {
// If `options.tags` doesn't exist set it to an empty object.
if (!options.tags) {
options.tags = {};
}
// We need be alerted to when a <content> element is rendered so we can put the original contents of the widget in its place
options.tags.content = function contentHookup(el, contentTagData) {
// First check if there was content within the custom tag
// otherwise, render what was within <content>, the default code.
// `componentTagData.subtemplate` is the content inside this component
var subtemplate = componentTagData.subtemplate || contentTagData.subtemplate,
renderingLightContent = subtemplate === componentTagData.subtemplate;
if (subtemplate) {
// `contentTagData.options` is a viewModel of helpers where `<content>` was found, so
// the right helpers should already be available.
// However, `_tags.content` is going to point to this current content callback. We need to
// remove that so it will walk up the chain
delete options.tags.content;
// By default, light dom scoping is
// dynamic. This means that any `{{foo}}`
// bindings inside the "light dom" content of
// the component will have access to the
// internal viewModel. This can be overridden to be
// lexical with the leakScope option.
var lightTemplateData;
if( renderingLightContent ) {
if(lexicalContent) {
// render with the same scope the component was found within.
lightTemplateData = componentTagData;
} else {
// render with the component's viewModel mixed in, however
// we still want the outer refs to be used, NOT the component's refs
// <component> {{some value }} </component>
// To fix this, we
// walk down the scope to the component's ref, clone scopes from that point up
// use that as the new scope.
lightTemplateData = {
scope: contentTagData.scope.cloneFromRef(),
options: contentTagData.options
};
}
} else {
// we are rendering default content so this content should
// use the same scope as the <content> tag was found within.
lightTemplateData = contentTagData;
}
if(contentTagData.parentNodeList) {
var frag = subtemplate( lightTemplateData.scope, lightTemplateData.options, contentTagData.parentNodeList );
elements.replace([el], frag);
} else {
can.view.live.replace([el], subtemplate( lightTemplateData.scope, lightTemplateData.options ));
}
// Restore the content tag so it could potentially be used again (as in lists)
options.tags.content = contentHookup;
}
};
// Render the component's template
frag = this.constructor.renderer(shadowScope, componentTagData.options.add(options), nodeList);
} else {
// Otherwise render the contents between the element
if(componentTagData.templateType === "legacy") {
frag = can.view.frag(componentTagData.subtemplate ? componentTagData.subtemplate(shadowScope, componentTagData.options.add(options)) : "");
} else {
// we need to be the parent ... or we need to
frag = componentTagData.subtemplate ?
componentTagData.subtemplate(shadowScope, componentTagData.options.add(options), nodeList) :
document.createDocumentFragment();
}
}
// Append the resulting document fragment to the element
can.appendChild(el, frag, can.document);
// update the nodeList with the new children so the mapping gets applied
can.view.nodeLists.update(nodeList, can.childNodes(el));
}
});
var ComponentControl = can.Control.extend({
// Change lookup to first look in the viewModel.
_lookup: function (options) {
return [options.scope, options, window];
},
_action: function (methodName, options, controlInstance ) {
var hasObjectLookup, readyCompute;
paramReplacer.lastIndex = 0;
hasObjectLookup = paramReplacer.test(methodName);
// If we don't have options (a `control` instance), we'll run this
// later.
if( !controlInstance && hasObjectLookup) {
return;
} else if( !hasObjectLookup ) {
return can.Control._action.apply(this, arguments);
} else {
// We have `hasObjectLookup` and `controlInstance`.
readyCompute = can.compute(function(){
var delegate;
// Set the delegate target and get the name of the event we're listening to.
var name = methodName.replace(paramReplacer, function(matched, key){
var value;
// If we are listening directly on the `viewModel` set it as a delegate target.
if(key === "scope" || key === "viewModel") {
delegate = options.viewModel;
return "";
}
// Remove `viewModel.` from the start of the key and read the value from the `viewModel`.
key = key.replace(/^(scope|^viewModel)\./,"");
value = can.compute.read(options.viewModel, can.compute.read.reads(key), {
// if we find a compute, we should bind on that and not read it
readCompute: false
}).value;
// If `value` is undefined use `can.getObject` to get the value.
if(value === undefined) {
value = can.getObject(key);
}
// If `value` is a string we just return it, otherwise we set it as a delegate target.
if(typeof value === "string") {
return value;
} else {
delegate = value;
return "";
}
});
// Get the name of the `event` we're listening to.
var parts = name.split(/\s+/g),
event = parts.pop();
// Return everything needed to handle the event we're listening to.
return {
processor: this.processors[event] || this.processors.click,
parts: [name, parts.join(" "), event],
delegate: delegate || undefined
};
}, this);
// Create a handler function that we'll use to handle the `change` event on the `readyCompute`.
var handler = function(ev, ready){
// unbinds the old binding
controlInstance._bindings.control[methodName](controlInstance.element);
// binds the new
controlInstance._bindings.control[methodName] = ready.processor(
ready.delegate || controlInstance.element,
ready.parts[2], ready.parts[1], methodName, controlInstance);
};
readyCompute.bind("change", handler);
controlInstance._bindings.readyComputes[methodName] = {
compute: readyCompute,
handler: handler
};
return readyCompute();
}
}
},
// Extend `events` with a setup method that listens to changes in `viewModel` and
// rebinds all templated event handlers.
{
setup: function (el, options) {
this.scope = options.scope;
this.viewModel = options.viewModel;
return can.Control.prototype.setup.call(this, el, options);
},
off: function(){
// If `this._bindings` exists we need to go through it's `readyComputes` and manually
// unbind `change` event listeners set by the controller.
if( this._bindings ) {
can.each(this._bindings.readyComputes || {}, function (value) {
value.compute.unbind("change", value.handler);
});
}
// Call `can.Control.prototype.off` function on this instance to cleanup the bindings.
can.Control.prototype.off.apply(this, arguments);
this._bindings.readyComputes = {};
},
destroy: function() {
can.Control.prototype.destroy.apply( this, arguments );
if(typeof this.options.destroy === 'function') {
this.options.destroy.apply(this, arguments);
}
}
});
/**
* @description Read and write a component element's viewModel.
*
* @function can.viewModel
* @parent can.util
* @signature `can.viewModel(el[, attr[, value]])`
* @param {HTMLElement|NodeList} el can.Component element to get viewModel of.
* @param {String} [attr] Attribute name to access.
* @param {*} [val] Value to write to the viewModel attribute.
*
* @return {*} If only one argument, returns the viewModel itself. If two
* arguments are given, returns the attribute value. If three arguments
* are given, returns the element itself after assigning the value (for
* chaining).
*
* @body
*
* `can.viewModel` can be used to directly access a [can.Component]'s
* viewModel. Depending on how it's called, it can be used to get the
* entire viewModel object, read a specific property from it, or write a
* property. The property read and write features can be seen as a
* shorthand for code such as `$("my-thing").viewModel().attr("foo", val);`
*
* If using jQuery, this function is accessible as a jQuery plugin,
* with one fewer argument to the call. For example,
* `$("my-element").viewModel("name", "Whatnot");`
*
*/
// Define the `can.viewModel` function that can be used to retrieve the
// `viewModel` from the element
var $ = can.$;
// If `$` has an `fn` object create the
// `scope` plugin that returns the scope object.
if ($.fn) {
$.fn.scope = $.fn.viewModel = function () {
// Just use `can.scope` as the base for this function instead
// of repeating ourselves.
return can.viewModel.apply(can, [this].concat(can.makeArray(arguments)));
};
}
return Component;
});