UNPKG

can

Version:

MIT-licensed, client-side, JavaScript framework that makes building rich web applications easy.

491 lines (425 loc) 18.5 kB
// # 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; });