UNPKG

todomvc

Version:

> Helping you select an MV\* framework

705 lines (645 loc) 21.5 kB
/*! * CanJS - 2.0.3 * http://canjs.us/ * Copyright (c) 2013 Bitovi * Tue, 26 Nov 2013 18:21:22 GMT * Licensed MIT * Includes: CanJS default build * Download from: http://canjs.us/ */ define(["can/util/library"], function( can ) { // ## view.js // `can.view` // _Templating abstraction._ var isFunction = can.isFunction, makeArray = can.makeArray, // Used for hookup `id`s. hookupId = 1, /** * @add can.view */ $view = can.view = can.template = function(view, data, helpers, callback){ // If helpers is a `function`, it is actually a callback. if ( isFunction( helpers )) { callback = helpers; helpers = undefined; } var pipe = function(result){ return $view.frag(result); }, // In case we got a callback, we need to convert the can.view.render // result to a document fragment wrapCallback = isFunction(callback) ? function(frag) { callback(pipe(frag)); } : null, // Get the result, if a renderer function is passed in, then we just use that to render the data result = isFunction(view) ? view(data, helpers, wrapCallback) : $view.render(view, data, helpers, wrapCallback), deferred = can.Deferred(); if(isFunction(result)) { return result; } if(can.isDeferred(result)){ result.then(function(result, data) { deferred.resolve.call(deferred, pipe(result), data); }, function() { deferred.fail.apply(deferred, arguments); }); return deferred; } // Convert it into a dom frag. return pipe(result); }; can.extend( $view, { // creates a frag and hooks it up all at once /** * @function can.view.frag frag * @parent can.view.static */ frag: function(result, parentNode ){ return $view.hookup( $view.fragment(result), parentNode ); }, // simply creates a frag // this is used internally to create a frag // insert it // then hook it up fragment: function(result){ var frag = can.buildFragment(result,document.body); // If we have an empty frag... if(!frag.childNodes.length) { frag.appendChild(document.createTextNode('')); } return frag; }, // Convert a path like string into something that's ok for an `element` ID. toId : function( src ) { return can.map(src.toString().split(/\/|\./g), function( part ) { // Dont include empty strings in toId functions if ( part ) { return part; } }).join("_"); }, hookup: function(fragment, parentNode ){ var hookupEls = [], id, func; // Get all `childNodes`. can.each(fragment.childNodes ? can.makeArray(fragment.childNodes) : fragment, function(node){ if(node.nodeType === 1){ hookupEls.push(node); hookupEls.push.apply(hookupEls, can.makeArray( node.getElementsByTagName('*'))); } }); // Filter by `data-view-id` attribute. can.each( hookupEls, function( el ) { if ( el.getAttribute && (id = el.getAttribute('data-view-id')) && (func = $view.hookups[id]) ) { func(el, parentNode, id); delete $view.hookups[id]; el.removeAttribute('data-view-id'); } }); return fragment; }, /** * @function can.view.ejs ejs * @parent can.view.static * * @signature `can.view.ejs( [id,] template )` * * Register an EJS template string and create a renderer function. * * var renderer = can.view.ejs("<h1><%= message %></h1>"); * renderer({message: "Hello"}) //-> docFrag[ <h1>Hello</h1> ] * * @param {String} [id] An optional ID to register the template. * * can.view.ejs("greet","<h1><%= message %></h1>"); * can.view("greet",{message: "Hello"}) //-> docFrag[<h1>Hello</h1>] * * @param {String} template An EJS template in string form. * @return {can.view.renderer} A renderer function that takes data and helpers. * * * @body * `can.view.ejs([id,] template)` registers an EJS template string * for a given id programatically. The following * registers `myViewEJS` and renders it into a documentFragment. * * can.view.ejs('myViewEJS', '<h2><%= message %></h2>'); * * var frag = can.view('myViewEJS', { * message : 'Hello there!' * }); * * frag // -> <h2>Hello there!</h2> * * To convert the template into a render function, just pass * the template. Call the render function with the data * you want to pass to the template and it returns the * documentFragment. * * var renderer = can.view.ejs('<div><%= message %></div>'); * renderer({ * message : 'EJS' * }); // -> <div>EJS</div> */ // auj /** * @function can.view.mustache mustache * @parent can.view.static * * @signature `can.view.mustache( [id,] template )` * * Register a Mustache template string and create a renderer function. * * var renderer = can.view.mustache("<h1>{{message}}</h1>"); * renderer({message: "Hello"}) //-> docFrag[ <h1>Hello</h1> ] * * @param {String} [id] An optional ID for the template. * * can.view.ejs("greet","<h1>{{message}}</h1>"); * can.view("greet",{message: "Hello"}) //-> docFrag[<h1>Hello</h1>] * * @param {String} template A Mustache template in string form. * * @return {can.view.renderer} A renderer function that takes data and helpers. * * @body * * `can.view.mustache([id,] template)` registers an Mustache template string * for a given id programatically. The following * registers `myStache` and renders it into a documentFragment. * * can.viewmustache('myStache', '<h2>{{message}}</h2>'); * * var frag = can.view('myStache', { * message : 'Hello there!' * }); * * frag // -> <h2>Hello there!</h2> * * To convert the template into a render function, just pass * the template. Call the render function with the data * you want to pass to the template and it returns the * documentFragment. * * var renderer = can.view.mustache('<div>{{message}}</div>'); * renderer({ * message : 'Mustache' * }); // -> <div>Mustache</div> */ // heir /** * @property hookups * @hide * A list of pending 'hookups' */ hookups: {}, /** * @description Create a hookup to insert into templates. * @function can.view.hook hook * @parent can.view.static * @signature `can.view.hook(callback)` * @param {Function} callback A callback function to be called with the element. * * @body * Registers a hookup function that can be called back after the html is * put on the page. Typically this is handled by the template engine. Currently * only EJS supports this functionality. * * var id = can.view.hook(function(el){ * //do something with el * }), * html = "<div data-view-id='"+id+"'>" * $('.foo').html(html); */ hook: function( cb ) { $view.hookups[++hookupId] = cb; return " data-view-id='"+hookupId+"'"; }, /** * @hide * @property {Object} can.view.cached view * @parent can.view * Cached are put in this object */ cached: {}, cachedRenderers: {}, /** * @property {Boolean} can.view.cache cache * @parent can.view.static * By default, views are cached on the client. If you'd like the * the views to reload from the server, you can set the `cache` attribute to `false`. * * //- Forces loads from server * can.view.cache = false; * */ cache: true, /** * @function can.view.register register * @parent can.view.static * @description Register a templating language. * @signature `can.view.register(info)` * @param {{}} info Information about the templating language. * @option {String} plugin The location of the templating language's plugin. * @option {String} suffix Files with this suffix will use this templating language's plugin by default. * @option {function} renderer A function that returns a function that, given data, will render the template with that data. * The __renderer__ function receives the id of the template and the text of the template. * @option {function} script A function that returns the string form of the processed template. * * @body * Registers a template engine to be used with * view helpers and compression. * * ## Example * * @codestart * can.View.register({ * suffix : "tmpl", * plugin : "jquery/view/tmpl", * renderer: function( id, text ) { * return function(data){ * return jQuery.render( text, data ); * } * }, * script: function( id, text ) { * var tmpl = can.tmpl(text).toString(); * return "function(data){return ("+ * tmpl+ * ").call(jQuery, jQuery, data); }"; * } * }) * @codeend */ register: function( info ) { this.types["." + info.suffix] = info; }, types: {}, /** * @property {String} can.view.ext ext * @parent can.view.static * The default suffix to use if none is provided in the view's url. * This is set to `.ejs` by default. * * // Changes view ext to 'txt' * can.view.ext = 'txt'; * */ ext: ".ejs", /** * Returns the text that * @hide * @param {Object} type * @param {Object} id * @param {Object} src */ registerScript: function() {}, /** * @hide * Called by a production script to pre-load a renderer function * into the view cache. * @param {String} id * @param {Function} renderer */ preload: function( ) {}, /** * @function can.view.render render * @parent can.view.static * @description Render a template. * @signature `can.view.render(template[, callback])` * @param {String|Object} view The path of the view template or a view object. * @param {Function} [callback] A function executed after the template has been processed. * @return {Function|can.Deferred} A renderer function to be called with data and helpers * or a Deferred that resolves to a renderer function. * * @signature `can.view.render(template, data[, [helpers,] callback])` * @param {String|Object} view The path of the view template or a view object. * @param {Object} [data] The data to populate the template with. * @param {Object.<String, function>} [helpers] Helper methods referenced in the template. * @param {Function} [callback] A function executed after the template has been processed. * @return {String|can.Deferred} The template with interpolated data in string form * or a Deferred that resolves to the template with interpolated data. * * @body * `can.view.render(view, [data], [helpers], callback)` returns the rendered markup produced by the corresponding template * engine as String. If you pass a deferred object in as data, render returns * a deferred resolving to the rendered markup. * * `can.view.render` is commonly used for sub-templates. * * ## Example * * _welcome.ejs_ looks like: * * <h1>Hello <%= hello %></h1> * * Render it to a string like: * * can.view.render("welcome.ejs",{hello: "world"}) * //-> <h1>Hello world</h1> * * ## Use as a Subtemplate * * If you have a template like: * * <ul> * <% list(items, function(item){ %> * <%== can.view.render("item.ejs",item) %> * <% }) %> * </ul> * * ## Using renderer functions * * If you only pass the view path, `can.view will return a renderer function that can be called with * the data to render: * * var renderer = can.view.render("welcome.ejs"); * // Do some more things * renderer({hello: "world"}) // -> Document Fragment * */ render: function( view, data, helpers, callback ) { // If helpers is a `function`, it is actually a callback. if ( isFunction( helpers )) { callback = helpers; helpers = undefined; } // See if we got passed any deferreds. var deferreds = getDeferreds(data); if ( deferreds.length ) { // Does data contain any deferreds? // The deferred that resolves into the rendered content... var deferred = new can.Deferred(), dataCopy = can.extend({}, data); // Add the view request to the list of deferreds. deferreds.push(get(view, true)) // Wait for the view and all deferreds to finish... can.when.apply(can, deferreds).then(function( resolved ) { // Get all the resolved deferreds. var objs = makeArray(arguments), // Renderer is the last index of the data. renderer = objs.pop(), // The result of the template rendering with data. result; // Make data look like the resolved deferreds. if ( can.isDeferred(data) ) { dataCopy = usefulPart(resolved); } else { // Go through each prop in data again and // replace the defferreds with what they resolved to. for ( var prop in data ) { if ( can.isDeferred(data[prop]) ) { dataCopy[prop] = usefulPart(objs.shift()); } } } // Get the rendered result. result = renderer(dataCopy, helpers); // Resolve with the rendered view. deferred.resolve(result, dataCopy); // If there's a `callback`, call it back with the result. callback && callback(result, dataCopy); }, function() { deferred.reject.apply(deferred, arguments) }); // Return the deferred... return deferred; } else { // get is called async but in // ff will be async so we need to temporarily reset if(can.__reading){ var reading = can.__reading; can.__reading = null; } // No deferreds! Render this bad boy. var response, // If there's a `callback` function async = isFunction( callback ), // Get the `view` type deferred = get(view, async); if(can.Map && can.__reading){ can.__reading = reading; } // If we are `async`... if ( async ) { // Return the deferred response = deferred; // And fire callback with the rendered result. deferred.then(function( renderer ) { callback(data ? renderer(data, helpers) : renderer); }) } else { // if the deferred is resolved, call the cached renderer instead // this is because it's possible, with recursive deferreds to // need to render a view while its deferred is _resolving_. A _resolving_ deferred // is a deferred that was just resolved and is calling back it's success callbacks. // If a new success handler is called while resoliving, it does not get fired by // jQuery's deferred system. So instead of adding a new callback // we use the cached renderer. // We also add __view_id on the deferred so we can look up it's cached renderer. // In the future, we might simply store either a deferred or the cached result. if(deferred.state() === "resolved" && deferred.__view_id ){ var currentRenderer = $view.cachedRenderers[ deferred.__view_id ]; return data ? currentRenderer(data, helpers) : currentRenderer; } else { // Otherwise, the deferred is complete, so // set response to the result of the rendering. deferred.then(function( renderer ) { response = data ? renderer(data, helpers) : renderer; }); } } return response; } }, /** * @hide * Registers a view with `cached` object. This is used * internally by this class and Mustache to hookup views. * @param {String} id * @param {String} text * @param {String} type * @param {can.Deferred} def */ registerView: function( id, text, type, def ) { // Get the renderer function. var func = (type || $view.types[$view.ext]).renderer(id, text); def = def || new can.Deferred(); // Cache if we are caching. if ( $view.cache ) { $view.cached[id] = def; def.__view_id = id; $view.cachedRenderers[id] = func; } // Return the objects for the response's `dataTypes` // (in this case view). return def.resolve(func); } }); // Makes sure there's a template, if not, have `steal` provide a warning. var checkText = function( text, url ) { if ( ! text.length ) { throw "can.view: No template or empty template:" + url; } }, // `Returns a `view` renderer deferred. // `url` - The url to the template. // `async` - If the ajax request should be asynchronous. // Returns a deferred. get = function( obj, async ) { var url = typeof obj === 'string' ? obj : obj.url, suffix = obj.engine || url.match(/\.[\w\d]+$/), type, // If we are reading a script element for the content of the template, // `el` will be set to that script element. el, // A unique identifier for the view (used for caching). // This is typically derived from the element id or // the url for the template. id, // The ajax request used to retrieve the template content. jqXHR; //If the url has a #, we assume we want to use an inline template //from a script element and not current page's HTML if( url.match(/^#/) ) { url = url.substr(1); } // If we have an inline template, derive the suffix from the `text/???` part. // This only supports `<script>` tags. if ( el = document.getElementById(url) ) { suffix = "."+el.type.match(/\/(x\-)?(.+)/)[2]; } // If there is no suffix, add one. if (!suffix && !$view.cached[url] ) { url += ( suffix = $view.ext ); } if ( can.isArray( suffix )) { suffix = suffix[0] } // Convert to a unique and valid id. id = $view.toId(url); // If an absolute path, use `steal` to get it. // You should only be using `//` if you are using `steal`. if ( url.match(/^\/\//) ) { var sub = url.substr(2); url = ! window.steal ? sub : steal.config().root.mapJoin(""+steal.id(sub)); } // Set the template engine type. type = $view.types[suffix]; // If it is cached, if ( $view.cached[id] ) { // Return the cached deferred renderer. return $view.cached[id]; // Otherwise if we are getting this from a `<script>` element. } else if ( el ) { // Resolve immediately with the element's `innerHTML`. return $view.registerView(id, el.innerHTML, type); } else { // Make an ajax request for text. var d = new can.Deferred(); can.ajax({ async: async, url: url, dataType: "text", error: function(jqXHR) { checkText("", url); d.reject(jqXHR); }, success: function( text ) { // Make sure we got some text back. checkText(text, url); $view.registerView(id, text, type, d) } }); return d; } }, // Gets an `array` of deferreds from an `object`. // This only goes one level deep. getDeferreds = function( data ) { var deferreds = []; // pull out deferreds if ( can.isDeferred(data) ) { return [data] } else { for ( var prop in data ) { if ( can.isDeferred(data[prop]) ) { deferreds.push(data[prop]); } } } return deferreds; }, // Gets the useful part of a resolved deferred. // This is for `model`s and `can.ajax` that resolve to an `array`. usefulPart = function( resolved ) { return can.isArray(resolved) && resolved[1] === 'success' ? resolved[0] : resolved }; //!steal-pluginify-remove-start if ( window.steal ) { steal.type("view js", function( options, success, error ) { var type = $view.types["." + options.type], id = $view.toId(options.id); /** * @hide * should return something like steal("dependencies",function(EJS){ * return can.view.preload("ID", options.text) * }) */ options.text = "steal('" + (type.plugin || "can/view/" + options.type) + "',function(can){return " + "can.view.preload('" + id + "'," + options.text + ");\n})"; success(); }) } //!steal-pluginify-remove-end can.extend($view, { register: function( info ) { this.types["." + info.suffix] = info; //!steal-pluginify-remove-start if ( window.steal ) { steal.type(info.suffix + " view js", function( options, success, error ) { var type = $view.types["." + options.type], id = $view.toId(options.id+''); options.text = type.script(id, options.text) success(); }) }; //!steal-pluginify-remove-end $view[info.suffix] = function(id, text){ if(!text) { // Return a nameless renderer var renderer = function() { return $view.frag(renderer.render.apply(this, arguments)); } renderer.render = function() { var renderer = info.renderer(null, id); return renderer.apply(renderer, arguments); } return renderer; } return $view.preload(id, info.renderer(id, text)); } }, registerScript: function( type, id, src ) { return "can.view.preload('" + id + "'," + $view.types["." + type].script(id, src) + ");"; }, preload: function( id, renderer ) { var def = $view.cached[id] = new can.Deferred().resolve(function( data, helpers ) { return renderer.call(data, data, helpers); }); function frag(){ return $view.frag(renderer.apply(this,arguments)); } // expose the renderer for mustache frag.render = renderer; // set cache references (otherwise preloaded recursive views won't recurse properly) def.__view_id = id; $view.cachedRenderers[id] = renderer; return frag; } }); return can; });