done-autorender
Version:
Autorender CanJS projects
578 lines (494 loc) • 15.7 kB
Plain Text
define(<%= imports %>, function(<%= args %>){
var zoneOpts = <%= zoneOpts %>;
var useZones = zoneOpts.useZones;
var tokens = <%= intermediate %>;
var renderer = stache("<%= filename %>", tokens);
var routeDataProp = "<%= routeData %>";
var isDevelopment = <%= isDevelopment %>;
var isNode = typeof process === "object" &&
{}.toString.call(process) === "[object process]";
var isNW = (function(){
try{var nr = loader._nodeRequire; return nr && nr('nw.gui') !== 'undefined';}catch(e){return false;}
})();
var isElectron = isNode && !!process.versions.electron;
/**
* @function render
* @hide
* @description Call the stache renderer function with Scope and Options.
* @signature `render(scope, options)`
* @param {can-view-scope} scope A can-view-scope object.
* @param {can-view-scope.Options} options An option object.
* @return {DocumentFragment} The result of calling a can-stache renderer,
* a document fragment.
*/
function render(viewModel, options){
var moduleOptions = { module: module };
options = (options && options.add) ? options.add(moduleOptions) :
moduleOptions;
var nodeList = canNodeList.register([], null, true);
nodeList.expression = "DONE-AUTO-RENDER";
var fragment = renderer(new Scope(viewModel, null, { viewModel: true }), options, nodeList);
canNodeList.update( nodeList, childNodes(fragment) );
// Provide the nodeList for external use.
if(useZones && typeof CanZone !== "undefined" && CanZone.current) {
CanZone.current.data.nodeList = nodeList;
}
return {fragment: fragment, nodeList: nodeList};
}
/**
* @function connectViewModel
* @description Create a new instance of the provided ViewModel, set it
* as the route’s data, and call route.start().
* @signature `connectViewModel()`
* @return {Map} an instance of some map type.
*/
function connectViewModel() {
var ctx = getContext(this);
var ViewModel = ctx.ViewModel;
if(!ViewModel) {
var message = "done-autorender cannot start without a ViewModel. " +
"Please ensure your template contains an export for your " +
"application's ViewModel. https://github.com/donejs/autorender#viewmodel";
console.error(message);
return;
}
// Make sure the common index.stache props are defined.
setupDefaultViewModelProps(ctx, ViewModel);
var viewModel = ctx.state = new ViewModel();
canReflect.setKeyValue(document.documentElement,
canSymbol.for("can.viewModel"), viewModel);
setRouteData(route, viewModel);
route.start();
return viewModel;
}
/**
* @function connectViewModelAndAttach
* @description Render the stache template, then update the
* DOM to reflect these changes. Save the state of the ViewModel instance
* so that it can be reused to do rerenders in case of live-reload. This is
* the main entry point of rendering, and happens upon page load.
* @signature `connectViewModelAndAttach()`
**/
function connectViewModelAndAttach() {
connectViewModel();
return renderAndAttach.call(this);
}
/**
* @function reattachWithZone
* @description Create a Zone for reattach.
* @signature `reattachWithZone()`
**/
function reattachWithZone() {
new Zone({
plugins: [xhrZone]
}).run(function(){
var viewModel = connectViewModel();
var result = renderInZone(viewModel);
var incremental = document.documentElement.dataset.incrementallyRendered === "";
// If incrementally rendering, attach right away. IR hydration will
// handle reattachment.
if(incremental) {
attach(result);
} else {
result.promise.then(attach);
}
});
}
var tagsToIgnore = {
"SCRIPT": true,
"STYLE": true,
"LINK": true,
"BASE": true
};
function hasDataKeepAttr(node) {
return node.dataset && node.dataset.keep === "";
}
function isKeepComment(node) {
return node.nodeType === 8 && node.nodeValue.indexOf("autorender-keep") === 0;
}
var keepNodeSymbol = canSymbol.for("done.keepNode");
function hasKeepSymbol(node) {
return node[keepNodeSymbol];
}
function isKeepNode(node) {
return hasDataKeepAttr(node) || isKeepComment(node) || hasKeepSymbol(node);
}
/**
* Call a callback for each child Node within a parent, skipping
* elements that should not be touched because of their side-effects.
*/
function eachChild(parent, callback, noSkipping){
var nodes = Array.prototype.slice.call(childNodes(parent)),
i = 0, len = nodes.length,
node, ignoreTag;
for(; i < len; i++) {
node = nodes[i];
ignoreTag = tagsToIgnore[node.nodeName];
if(noSkipping || (!ignoreTag && !isKeepNode(node))) {
// Returning false breaks the loop
if(callback(node) === false) {
break;
}
}
}
}
/**
* Remove an element
*/
function remove(el) {
domMutateNode.removeChild.call(el.parentNode, el);
}
/**
* Creates a function that will append to a parent Element.
*/
function appendTo(parent){
return function(el){
domMutateNode.appendChild.call(parent, el);
}
}
/**
* Sets the route.data property
*/
function setRouteData(route, appViewModel) {
if(routeDataProp) {
var obs = canReflect.getKeyValue(appViewModel, routeDataProp);
if(obs.constructor) {
route.data = new obs.constructor();
canReflect.setKeyValue(appViewModel, routeDataProp, route.data);
} else {
route.data = obs;
}
} else {
route.data = appViewModel;
}
}
/**
* @function attach
* @hide
* @description Receives the completely rendered DocumentFragment and
* attaches the parts from the head into the document.head, the body into
* document.body.
* @signature `attach(result)`
* @param {RenderResult} The result of rendering within a Zone.
*/
function attach(result){
var document = getContext(this).ownerDocument;
var frag = result.fragment;
var viewModel = result.viewModel;
// If already attached skip this part.
if(document.documentElement.hasAttribute("data-attached")) {
return;
}
moveToDocument(frag, document, true);
document.documentElement.setAttribute("data-attached", "");
if(viewModel && viewModel.connectedCallback) {
var dispose = viewModel.connectedCallback(document.documentElement);
if(typeof dispose === "function") {
domMutate.onNodeRemoval(document.documentElement, dispose);
}
}
return result;
}
/**
* Move content from a fragment into a document.
**/
function moveToDocument(frag, document, removeExistingNodes) {
var head = document.head;
var body = document.body;
// Move elements from the fragment's head to the document head.
if(removeExistingNodes) {
eachChild(head, remove);
}
var top = frag.firstChild;
// Find the <html> element.
if(top.nodeType !== 1) {
top = top.nextSibling;
}
var fragHead = top;
var fragBody = top.firstChild;
while(fragHead.tagName !== "HEAD") {
fragHead = fragHead.firstChild || fragHead.nextSibling;
}
while(fragBody && fragBody.tagName !== "BODY") {
fragBody = fragBody.nextSibling;
}
if(head && fragHead) {
eachChild(fragHead, appendTo(head), !removeExistingNodes);
}
// Move elements from the fragment's body to the document body.
if(removeExistingNodes) {
eachChild(body, remove);
}
if(body && fragBody) {
eachChild(fragBody, appendTo(body), !removeExistingNodes);
}
}
/**
* @function renderAndAttach
* @hide
* @description Render the template with a Zone, wait for all asynchronous
* events to complete, and then attach the DocumentFragment to the page.
* @signature `renderAndAttach()`
* @return {Promise} A Promise that resolves after the template has been
* attached to the DOM.
*/
function renderAndAttach(){
var boundAttach = attach.bind(this);
var viewModel = context.state;
return useZones
? renderInZone(viewModel).promise.then(boundAttach)
: renderNoZone(viewModel).then(boundAttach);
}
/**
* @function renderIntoZone
* @hide
* @description Render a viewModel in a Zone context, returning the
* Zone promise.
* @signature `renderIntoZone(viewModel)`
* @param {Object} viewModel
* @return {RenderResult} the promise that resolves when asynchronousity
* within the Zone is complete, and the fragment generated.
*/
function renderInZone(viewModel){
function getZonePlugins() {
var plugins = [xhrZone];
if(zoneOpts.useDebug) {
var timeout = zoneOpts.timeout;
var opts = {
break: zoneOpts.debugBrk
};
plugins.push(debugZone(timeout, opts));
}
return plugins;
}
function logDebugInfo() {
var warn = Function.prototype.bind.call(console.warn, console);
var zoneData = zone.data;
if(zoneData.debugInfo) {
zoneData.debugInfo.forEach(function(info){
warn(info.task, info.stack);
});
}
}
var renderData;
var zone = new Zone({
plugins: getZonePlugins()
});
var zonePromise = zone.run(function(){
renderData = render(viewModel, {});
}).then(function(zoneData){
return {
nodeList: renderData.nodeList,
fragment: renderData.fragment,
zoneData: zoneData,
viewModel: viewModel
};
})
.then(null, function(err){
if(err.timeout) {
logDebugInfo();
var error = new Error("Timeout of " + err.timeout + " exceeded");
throw error;
} else {
throw err;
}
});
return {
fragment: renderData.fragment,
promise: zonePromise,
viewModel: viewModel,
zoneData: zone.data
};
}
/**
* @function renderNoZone
* @hide
* @description Render a viewModel without a Zone.
* @signature `renderIntoZone(viewModel)`
* @param {Object} viewModel
* @return {RenderResult} the promise that resolves immediately with a fragment.
*/
function renderNoZone(viewModel){
var renderData = render(viewModel, {});
return Promise.resolve(renderData);
}
/**
* @function renderIntoDocument
* @description This is used in SSR, it provides a fresh document
* and viewModel, and this function calls the stache renderer and updates
* the document with the result.
* @signature `renderIntoDocument(document, viewModel)`
* @param {Document} document
* @param {Object} viewModel
**/
function renderIntoDocument(document, viewModel) {
var renderData = render.call(this, viewModel, {});;
var frag = renderData.fragment;
var firstChild = frag.firstChild;
var documentElement = document.documentElement;
var head = document.head;
// If there is an <html> element, which there usually is,
// replace the existing documentElement, otherwise just append the fragment
if(firstChild && firstChild.nodeName === "HTML") {
domMutateNode.replaceChild.call(document, firstChild, documentElement);
} else {
domMutateNode.appendChild.call(documentElement, frag);
}
// Move anything from the original head back to the new documentElement.
if(head && head.firstChild) {
var fragHead = document.createDocumentFragment();
fragHead.appendChild(head);
moveToDocument(fragHead, document, false);
}
// Set the can-automount=false attribute
document.documentElement.setAttribute("data-can-automount", "false");
}
/**
* @function teardownRouting
* @hide
* @description Teardown the connection to the route, for live-reload.
* @signature `teardownRouting()`
**/
function teardownRouting() {
route.stop();
}
if(typeof steal !== 'undefined' && (isNW || isElectron || !isNode)) {
steal.done().then(function() {
if(steal.loader.autorenderAutostart !== false) {
if (useZones){
reattachWithZone();
} else {
connectViewModelAndAttach();
}
}
});
}
var context = Object.create(Function.prototype, {
<%= ases %>
connectViewModelAndAttach: {
value: connectViewModelAndAttach
},
ownerDocument: {
get: function(){
return document;
}
},
renderAndAttach: {
value: renderAndAttach
},
renderInZone: {
value: renderInZone
},
teardownRouting: {
value: teardownRouting
},
ViewModel: {
get: function(){
return this.viewModel;
}
}
});
var defaultProperties = ["env", "request", "statusCode", "statusMessage"];
/**
* @function setupDefaultViewModelProps
* @hide
* @description Given a ViewModel constructor, ensure that the
* default properties like `env` and `request` are not serialized
*/
function setupDefaultViewModelProps(ctx, ViewModel) {
var sym = canSymbol.for("autorender.props");
if(!canReflect.getKeyValue(ctx, sym) &&
canReflect.isConstructorLike(ViewModel)) {
var proto = ViewModel.prototype;
defaultProperties.forEach(function(prop){
if(!canReflect.hasOwnKey(proto, prop)) {
canReflect.defineInstanceKey(ViewModel, prop, {
enumerable: false
});
}
});
canReflect.setKeyValue(ctx, sym, true);
}
}
/**
* @function getContext
* @hide
* @description Gets the context (an object containing information about the render).
* This is used mostly as a DI mechanism for testing.
*/
function getContext(thisArg) {
return context.isPrototypeOf(thisArg) ? thisArg : context;
}
/**
* @function createViewModelAndRender
* @hide
* @description Create an instance of the ViewModel and render it with
* the renderIntoDocument function.
* @param {node.IncomingMessage|Object} requestOrHeaders A request object, from Node.js
*/
function createViewModelAndRender(requestOrHeaders) {
// Check if we were called with .call/apply, otherwise use the
// context within the scope. This is for testing.
var ctx = getContext(this);
var document = ctx.ownerDocument;
var ViewModel = ctx.ViewModel;
if(!ViewModel) {
var msg = "done-autorender cannot render your application " +
"without a viewModel defined. " +
"See the guide for information. " +
"http://donejs.com/Guide.html#section_Createatemplateandmainfile";
throw new Error(msg);
}
//!steal-remove-start
if (isDevelopment) {
if(canReflect.size(route.routes) === 0) {
var msg = "done-autorender didn't receive route definitions." +
"For Server-Side-Rendering you have to provide an initialized can-route." +
"See the guide for information. " +
"https://canjs.com/doc/can-route.html and https://donejs.com/Apis.html#can-route"
canDev.warn(msg);
}
}
//!steal-remove-end
setupDefaultViewModelProps(ctx, ViewModel);
var urlObj;
// Test if this is http/2 headers object
var isH2 = !!requestOrHeaders[":authority"];
if(isH2) {
var h = requestOrHeaders;
urlObj = new URL(h[":scheme"] + ":" + h[":authority"] + h[":path"]);
} else {
urlObj = new URL(fullUrl(requestOrHeaders));
}
// Get the route params
var pathWithoutSlash = urlObj.pathname.substr(1);
var routeStr = pathWithoutSlash + urlObj.search;
var hasMatchingRoute = !!route.rule(routeStr);
var routeParams = route.deparam(routeStr);
var viewModelParams = {
env: Object.assign({}, process.env),
request: requestOrHeaders
};
if(!routeDataProp) {
viewModelParams = Object.assign(routeParams, viewModelParams);
}
var viewModel = new ViewModel(viewModelParams);
if(!canReflect.getKeyValue(viewModel, "statusCode") && canReflect.size(route.routes) !== 0) {
if(hasMatchingRoute) {
canReflect.setKeyValue(viewModel, "statusCode", 200);
} else {
canReflect.setKeyValue(viewModel, "statusCode", 404);
canReflect.setKeyValue(viewModel, "statusMessage", "Not found");
}
}
setRouteData(route, viewModel);
if(routeDataProp) {
canReflect.update(route.data, routeParams);
}
route.start();
renderIntoDocument.call(ctx, document, viewModel);
return createViewModelAndRender;
}
Object.setPrototypeOf(createViewModelAndRender, context);
return createViewModelAndRender;
});