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 id
s.
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 model
s 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;
});