steal('can/util', function (can) {
var isFunction = can.isFunction,
makeArray = can.makeArray,can.view
Templating abstraction.
can.view loads templates based on a registered type, and given a set of data, returns a document fragment
from the template engine’s rendering method
steal('can/util', function (can) {
var isFunction = can.isFunction,
makeArray = can.makeArray,Used for hookup ids.
hookupId = 1; /**
* @hide
* Rendering function factory method
* @param textRenderer
* @return {renderer}
*/
var makeRenderer = function(textRenderer) {
var renderer = function() {
return $view.frag(textRenderer.apply(this, arguments));
};
renderer.render = function() {
return textRenderer.apply(textRenderer, arguments);
};
return renderer;
}; var checkText = function (text, url) {
if (!text.length) {removed if not used as a steal module
!steal-remove-start
can.dev.log("can/view/view.js: There is no template or an empty template at " + url);!steal-remove-end
throw new Error("can.view: No template or empty template:" + url);
}
}; /**
* @hide
* @function get
* @param {String | Object} obj url string or object with url property
* @param {Boolean} async If the ajax request should be asynchronous.
* @return {can.Deferred} a `view` renderer deferred.
*/
var getRenderer = function (obj, async) {If obj already is a renderer function just resolve a Deferred with it
if(isFunction(obj)) {
var def = can.Deferred();
return def.resolve(obj);
}
var url = typeof obj === 'string' ? obj : obj.url,
suffix = (obj.engine && '.' + 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;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 the suffix was derived from the .match() operation, pluck out the first value
if (can.isArray(suffix)) {
suffix = suffix[0];
}Convert to a unique and valid id.
id = $view.toId(url);If an absolute path, use steal/require to get it.
You should only be using // if you are using an AMD loader like steal or require (not almond).
if (url.match(/^\/\//)) {
url = url.substr(2);
url = !window.steal ?
url :
steal.config()
.root.mapJoin("" + steal.id(url));
}Localize for require (not almond)
if (window.require) {
if (require.toUrl) {
url = require.toUrl(url);
}
}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;
}
}; /**
* @hide
* @param {Object|can.Deferred} data
* @return {Array} deferred objects
*/
var getDeferreds = function (data) {
var deferreds = [];pull out deferreds
if (can.isPromise(data)) {
return [data];
} else {
for (var prop in data) {
if (can.isPromise(data[prop])) {
deferreds.push(data[prop]);
}
}
}
return deferreds;
};Gets the useful part of a resolved deferred.
When a jQuery.when is resolved, it returns an array to each argument.
Reference ($.when)[https://api.jquery.com/jQuery.when/]
This is for models and can.ajax that resolve to an array.
/**
* @hide
* @function usefulPart
* @param {Array|*} resolved
* @return {*}
*/
var usefulPart = function (resolved) {
return can.isArray(resolved) && resolved[1] === 'success' ? resolved[0] : resolved;
}; /**
* @add can.view
*/
var $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;
}Render the view as a fragment
return $view.renderAs("fragment",view, data, helpers, callback);
}; can.extend($view, { /**
* @function can.view.frag frag
* @parent can.view.static
*/
frag: function (result, parentNode) {
return $view.hookup($view.fragment(result), parentNode);
}, fragment: function (result) {
return can.frag(result, document);
}, toId: function (src) {
return can.map(src.toString()
.split(/\/|\./g), function (part) {Dont include empty strings in toId functions
if (part) {
return part;
}
})
.join('_');
}, toStr: function(txt){
return txt == null ? "" : ""+txt;
}, /**
* @hide
* hook up a fragment to its parent node
* @param fragment
* @param parentNode
* @return {*}
*/
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;
},hookups keeps list of pending hookups, ie fragments to attach to a parent node
/**
* @property hookups
* @hide
* A list of pending 'hookups'
*/
hookups: {},hook factory method for hookup function inserted into templates
hookup functions are called after the html is rendered to the page
only implemented by EJS templates.
/**
* @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: {},cache view templates resolved via XHR on the client
/**
* @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,given an info object, register a template type different templating solutions produce strings or document fragments via their renderer function
/**
* @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
*
* ```
* 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); }";
* }
* })
* ```
*/
register: function (info) {
this.types['.' + info.suffix] = info;removed if not used as a steal module
!steal-remove-start
if ( typeof window !== "undefined" && window.steal && steal.type ) {
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-remove-end
can[info.suffix] = $view[info.suffix] = function (id, text) {
var renderer,
renderFunc;If there is no text, assume id is the template text, so return a nameless renderer.
if (!text) {
renderFunc = function(){
if(!renderer){if the template has a fragRenderer already, just return that.
if(info.fragRenderer) {
renderer = info.fragRenderer(null, id);
} else {
renderer = makeRenderer(info.renderer(null, id));
}
}
return renderer.apply(this, arguments);
};
renderFunc.render = function() {
var textRenderer = info.renderer(null, id);
return textRenderer.apply(textRenderer, arguments);
};
return renderFunc;
}
var registeredRenderer = function(){
if(!renderer){
if(info.fragRenderer) {
renderer = info.fragRenderer(id, text);
} else {
renderer = info.renderer(id, text);
}
}
return renderer.apply(this, arguments);
};
if(info.fragRenderer) {
return $view.preload( id, registeredRenderer );
} else {
return $view.preloadStringRenderer(id, registeredRenderer);
}
};
},registered view types
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 from a script tag
* @hide
* @param {Object} type
* @param {Object} id
* @param {Object} src
*/
registerScript: function (type, id, src) {
return 'can.view.preloadStringRenderer(\'' + id + '\',' + $view.types['.' + type].script(id, src) + ');';
},
/**
* @hide
* Called by a production script to pre-load a fragment renderer function
* into the view cache.
* @param {String} id
* @param {Function} renderer
*/
preload: function (id, renderer) {
var def = $view.cached[id] = new can.Deferred()
.resolve(function (data, helpers) {
return renderer.call(data, data, helpers);
});set cache references (otherwise preloaded recursive views won’t recurse properly)
def.__view_id = id;
$view.cachedRenderers[id] = renderer;
return renderer;
},
/**
* @hide
* Called by a production script to pre-load a string renderer function
* into the view cache.
* @param id
* @param stringRenderer
* @return {*}
*/
preloadStringRenderer: function(id, stringRenderer) {
return this.preload(id, makeRenderer(stringRenderer) );
},can.view’s primary purpose is to load templates (from strings or filesystem) and render them
can.view supports two different forms of rendering systems
mustache templates return a string based rendering function
stache (or other fragment based templating systems) return a document fragment, so ‘hookup’ steps are not required
/**
* @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.
* @param {NodeList} nodelist parent nodelist to register partial template contents with
* @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
*
*/call renderAs with a hardcoded string, as view.render
always operates against resolved template files or hardcoded strings
render: function (view, data, helpers, callback, nodelist) {
return can.view.renderAs("string",view, data, helpers, callback, nodelist);
}, /**
* @hide
* @function renderTo
* @param {String} format
* @param {Function} renderer
* @param data
* @param {Object} helpers helper methods for this template
* @param {NodeList} nodelist parent nodelist to register partial template contents with
* @return {*}
*/
renderTo: function(format, renderer, data, helpers, nodelist){
return (format === "string" && renderer.render ? renderer.render : renderer)(data, helpers, nodelist);
},
/**
* @hide
*
* @param format
* @param view
* @param data
* @param helpers
* @param callback
* @param nodelist
* @return {*}
*/
renderAs: function (format, view, data, helpers, callback, nodelist) {if callback has expression prop its actually the nodelist
if (callback !== undefined && typeof callback.expression === 'string') {
nodelist = callback;
callback = undefined;
}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);
var deferred, dataCopy, async, response;
if (deferreds.length) {Does data contain any deferreds? The deferred that resolves into the rendered content…
deferred = new can.Deferred();
dataCopy = can.extend({}, data);Add the view request to the list of deferreds.
deferreds.push(getRenderer(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.isPromise(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.isPromise(data[prop])) {
dataCopy[prop] = usefulPart(objs.shift());
}
}
}Get the rendered result.
result = can.view.renderTo(format, renderer, dataCopy, helpers, nodelist);Resolve with the rendered view.
deferred.resolve(result, dataCopy);If there’s a callback, call it back with the result.
if (callback) {
callback(result, dataCopy);
}
}, function () {
deferred.reject.apply(deferred, arguments);
});Return the deferred…
return deferred;
} else {If there’s a callback function
async = isFunction(callback);get is called async but in ff will be async so we need to temporarily reset
deferred = can.__notObserve(getRenderer)(view, async);If we are async…
if (async) {Return the deferred
response = deferred;And fire callback with the rendered result.
deferred.then(function (renderer) {
callback(data ? can.view.renderTo(format, renderer, data, helpers, nodelist) : 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 ? can.view.renderTo(format, currentRenderer, data, helpers, nodelist) : currentRenderer;
} else {Otherwise, the deferred is complete, so set response to the result of the rendering.
deferred.then(function (renderer) {
response = data ? can.view.renderTo(format, renderer, data, helpers, nodelist) : 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 info = (typeof type === "object" ? type : $view.types[type || $view.ext]),
renderer;
if(info.fragRenderer) {
renderer = info.fragRenderer(id, text);
} else {
renderer = makeRenderer( info.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] = renderer;
}Return the objects for the response’s dataTypes
(in this case view).
return def.resolve(renderer);
},Returns a function that automatically converts all computes passed to it
simpleHelper: function(fn) {
return function() {
var realArgs = [];
var fnArgs = arguments;
can.each(fnArgs, function(val, i) {
if (i <= fnArgs.length) {
while (val && val.isComputed) {
val = val();
}
realArgs.push(val);
}
});
return fn.apply(this, realArgs);
};
}
});removed if not used as a steal module
!steal-remove-start
if ( typeof window !== "undefined" && window.steal && steal.type) {when being used as a steal module, add a new type for ‘view’ that runs
can.view.preloadStringRenderer with the loaded string/text for the dependency.
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)
* })
*/
var dependency = type.plugin || 'can/view/' + options.type,
preload = type.fragRenderer ? "preload" : "preloadStringRenderer";
options.text = 'steal(\'can/view\',\'' + dependency + '\',function(can){return ' + 'can.view.'+preload+'(\'' + id + '\',' + options.text + ');\n})';
success();
});
}!steal-remove-end
return can;
});