UNPKG

done-autorender

Version:
578 lines (494 loc) 15.7 kB
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; });