├── .gitignore ├── .jshintignore ├── .jshintrc ├── AsyncFragmentErrorNode.js ├── AsyncFragmentPlaceholderNode.js ├── AsyncFragmentTimeoutNode.js ├── LICENSE ├── README.md ├── async-fragment-error-tag-transformer.js ├── async-fragment-placeholder-tag-transformer.js ├── async-fragment-tag-transformer.js ├── async-fragment-tag.js ├── async-fragment-timeout-tag-transformer.js ├── async-fragments-tag.js ├── browser.json ├── client-reorder-browser.js ├── client-reorder-runtime.js ├── client-reorder-runtime.min.js ├── client-reorder.js ├── dust └── index.js ├── marko-taglib.json └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | /work 2 | /build 3 | /.idea/ 4 | /npm-debug.log 5 | /node_modules 6 | /*.sublime-workspace 7 | *.orig 8 | .DS_Store 9 | .vscode 10 | coverage 11 | -------------------------------------------------------------------------------- /.jshintignore: -------------------------------------------------------------------------------- 1 | /client-reorder-runtime.js 2 | /client-reorder-runtime.min.js -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | 4 | ], 5 | 6 | "globals": { 7 | "define": true, 8 | "require": true 9 | }, 10 | 11 | "node" : true, 12 | "es5" : false, 13 | "browser" : true, 14 | "boss" : false, 15 | "curly": false, 16 | "debug": false, 17 | "devel": false, 18 | "eqeqeq": true, 19 | "evil": true, 20 | "forin": false, 21 | "immed": true, 22 | "laxbreak": false, 23 | "newcap": true, 24 | "noarg": true, 25 | "noempty": false, 26 | "nonew": true, 27 | "nomen": false, 28 | "onevar": false, 29 | "plusplus": false, 30 | "regexp": false, 31 | "undef": true, 32 | "sub": false, 33 | "white": false, 34 | "eqeqeq": false, 35 | "latedef": true, 36 | "unused": "vars", 37 | "strict": false, 38 | 39 | /* Relaxing options: */ 40 | "eqnull": true 41 | } 42 | -------------------------------------------------------------------------------- /AsyncFragmentErrorNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 eBay Software Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | function AsyncFragmentErrorNode(props) { 18 | AsyncFragmentErrorNode.$super.call(this, 'async-fragment-error'); 19 | if (props) { 20 | this.setProperties(props); 21 | } 22 | } 23 | 24 | AsyncFragmentErrorNode.nodeType = 'element'; 25 | 26 | AsyncFragmentErrorNode.prototype = { 27 | doGenerateCode: function (template) { 28 | throw new Error('Illegal State. This node should have been removed'); 29 | } 30 | }; 31 | 32 | module.exports = AsyncFragmentErrorNode; 33 | -------------------------------------------------------------------------------- /AsyncFragmentPlaceholderNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 eBay Software Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | function AsyncFragmentPlaceholderNode(props) { 18 | AsyncFragmentPlaceholderNode.$super.call(this, 'async-fragment-placeholder'); 19 | if (props) { 20 | this.setProperties(props); 21 | } 22 | } 23 | 24 | AsyncFragmentPlaceholderNode.nodeType = 'element'; 25 | 26 | AsyncFragmentPlaceholderNode.prototype = { 27 | doGenerateCode: function (template) { 28 | throw new Error('Illegal State. This node should have been removed'); 29 | } 30 | }; 31 | 32 | module.exports = AsyncFragmentPlaceholderNode; -------------------------------------------------------------------------------- /AsyncFragmentTimeoutNode.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2011 eBay Software Foundation 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 'use strict'; 17 | function AsyncFragmentTimeoutNode(props) { 18 | AsyncFragmentTimeoutNode.$super.call(this, 'async-fragment-timeout'); 19 | if (props) { 20 | this.setProperties(props); 21 | } 22 | } 23 | 24 | AsyncFragmentTimeoutNode.nodeType = 'element'; 25 | 26 | AsyncFragmentTimeoutNode.prototype = { 27 | doGenerateCode: function (template) { 28 | throw new Error('Illegal State. This node should have been removed'); 29 | } 30 | }; 31 | 32 | module.exports = AsyncFragmentTimeoutNode; 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 JS Foundation and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | marko-async 2 | ===================== 3 | 4 | ---------------- 5 | 6 | :heavy_exclamation_mark: The source code and documentation in this repo are only relevant to Marko v2. For Marko v3, this repo has been merged into the main [marko](https://github.com/marko-js/marko) repo. 7 | 8 | - Marko v3 async taglib docs: http://markojs.com/docs/marko/language-guide/#async-taglib 9 | - Marko v3 async taglib source code: [marko/taglibs/async](https://github.com/marko-js/marko/tree/master/taglibs/async) 10 | 11 | ---------------- 12 | 13 | The `marko-async` taglib provides support for the more efficient and simpler "Pull Model "approach to providing templates with view model data. 14 | 15 | * __Push Model:__ Request all needed data upfront and wait for all of the data to be received before building the view model and then rendering the template. 16 | * __Pull Model:__ Pass asynchronous data provider functions to template immediately start rendering the template. Let the template _pull_ the data needed during rendering. 17 | 18 | The Pull Model approach to template rendering requires the use of a templating engine that supports asynchronous template rendering (e.g. [marko](https://github.com/marko-js/marko) and [dust](https://github.com/linkedin/dustjs)). This is because before rendering the template begins not all of data may have been fully retrieved. Parts of a template that depend on data that is not yet available are rendered asynchronously with the Pull Model approach. 19 | 20 | # Push Model versus Pull Model 21 | 22 | The problem with the traditional Push Model approach is that template rendering is delayed until _all_ data has been fully received. This reduces the time to first byte, and it also may result in the server sitting idle while waiting for data to be loaded from remote services. In addition, if certain data is no longer needed by a template then only the template needs to be modified and not the controller. 23 | 24 | With the new Pull Model approach, template rendering begins immediately. In addition, fragments of the template that depend on data from data providers are rendered asynchronously and wait only on the associated data provider to complete. The template rendering will only be delayed for data that the template actually needs. 25 | 26 | # Example 27 | 28 | ```javascript 29 | var template = require('./template.marko'); 30 | 31 | module.exports = function(req, res) { 32 | var userId = req.query.userId; 33 | template.render({ 34 | userProfileDataProvider: function(callback) { 35 | userProfileService.getUserProfile(userId, callback); 36 | } 37 | }, res); 38 | } 39 | ``` 40 | 41 | ```html 42 | 44 | 45 | 56 | 57 | 58 | ``` 59 | 60 | # Out-of-order Flushing 61 | 62 | The marko-async taglib also supports out-of-order flushing. Enabling out-of-order flushing requires two steps: 63 | 64 | 1. Add the `client-reorder` attribute to the `` tag:
65 | 66 | ```html 67 | 70 | 71 | 82 | 83 | 84 | ``` 85 | 86 | 2. Add the `` to the end of the page. 87 | 88 | If the `client-reorder` is `true` then a placeholder element will be rendered to the output instead of the final HTML for the async fragment. The async fragment will be instead rendered at the end of the page and client-side JavaScript code will be used to move the async fragment into the proper place in the DOM. The `` will be where the out-of-order fragments are rendered before they are moved into place. If there are any out-of-order fragments then inline JavaScript code will be injected into the page at this location to move the DOM nodes into the proper place in the DOM. 89 | 90 | # Taglib API 91 | 92 | ## `` 93 | 94 | Supported Attributes: 95 | 96 | * __`arg`__ (expression): The argument object to provide to the data provider function. 97 | * __`arg-`__ (string): An argument to add to the `arg` object provided to the data provider function. 98 | * __`client-reorder`__ (boolean): If `true`, then the async fragments will be flushed in the order they complete and JavaScript running on the client will be used to move the async fragments into the proper HTML order in the DOM. Defaults to `false`. 99 | * __`data-provider`__ (expression, required): The source of data for the async fragment. Must be a reference to one of the following: 100 | - `Function(callback)` 101 | - `Function(args, callback)` 102 | - `Promise` 103 | - Data 104 | * __`error-message`__ (string): Message to output if the fragment errors out. Specifying this will prevent the rendering from aborting. 105 | * __`name`__ (string): Name to assign to this async fragment. Used for debugging purposes as well as by the `show-after` attribute (see below). 106 | * __`placeholder`__ (string): Placeholder text to show while waiting for an out-of-order fragment to complete. Only applicable if `client-reorder` is set to `true`. 107 | * __`show-after`__ (string): When `client-reorder` is set to `true` then displaying this fragment will be delayed until the referenced async fragment is shown. 108 | * __`timeout`__ (integer): Override the default timeout of 10 seconds with this param. Units are in 109 | milliseconds so `timeout="40000"` would give a 40 second timeout. 110 | * __`timeout-message`__ (string): Message to output if the fragment times out. Specifying this 111 | will prevent the rendering from aborting. 112 | * __`var`__: Variable name to use when consuming the data provided by the data provider 113 | 114 | ## `` 115 | 116 | This tag can be used to control what text is shown while an out-of-order async fragment is waiting to be loaded. Only applicable if `client-reorder` is set to `true`. 117 | 118 | Example: 119 | 120 | ```html 121 | 122 | 123 | Loading user data... 124 | 125 | 126 |
    127 |
  • First name: ${user.firstName}
  • 128 |
  • Last name: ${user.lastName}
  • 129 |
130 | 131 |
132 | ``` 133 | 134 | ## `` 135 | 136 | This tag can be used to control what text is shown when an async fragment errors out. 137 | 138 | Example: 139 | 140 | ```html 141 | 142 | 143 | An error occurred! 144 | 145 | 146 |
    147 |
  • First name: ${user.firstName}
  • 148 |
  • Last name: ${user.lastName}
  • 149 |
150 |
151 | ``` 152 | 153 | ## `` 154 | 155 | This tag can be used to control what text is shown when an async fragment times out. 156 | 157 | Example: 158 | 159 | ```html 160 | 161 | 162 | A timeout occurred! 163 | 164 | 165 |
    166 |
  • First name: ${user.firstName}
  • 167 |
  • Last name: ${user.lastName}
  • 168 |
169 |
170 | ``` 171 | 172 | ## `` 173 | 174 | Container for all out-of-order async fragments. If any `` have `client-reorder` set to true then this tag needs to be included in the page template (typically, right before the closing `` tag). 175 | 176 | Example: 177 | 178 | ```html 179 | 180 | 181 | ... 182 | 183 | ... 184 | 185 | ... 186 | 187 | 188 | 189 | ``` 190 | -------------------------------------------------------------------------------- /async-fragment-error-tag-transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function transform(node, compiler, template) { 4 | 5 | var asyncFragmentNode = node.parentNode; 6 | 7 | if (!asyncFragmentNode) { 8 | template.addError(' should be nested directly below an tag.'); 9 | return; 10 | } 11 | 12 | // Remove the node from the tree 13 | node.detach(); 14 | 15 | asyncFragmentNode.setProperty('errorMessage', node.getBodyContentExpression(template)); 16 | }; 17 | -------------------------------------------------------------------------------- /async-fragment-placeholder-tag-transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function transform(node, compiler, template) { 4 | 5 | var asyncFragmentNode = node.parentNode; 6 | 7 | // Remove the node from the tree 8 | node.detach(); 9 | 10 | asyncFragmentNode.setProperty('placeholder', node.getBodyContentExpression(template)); 11 | }; 12 | -------------------------------------------------------------------------------- /async-fragment-tag-transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var varNameRegExp = /^[A-Za-z_][A-Za-z0-9_]*$/; 3 | module.exports = function transform(node, compiler, template) { 4 | var varName = node.getAttribute('var') || node.getAttribute('data-provider') || node.getAttribute('dependency'); 5 | if (varName) { 6 | if (!varNameRegExp.test(varName)) { 7 | node.addError('Invalid variable name of "' + varName + '"'); 8 | return; 9 | } 10 | } else { 11 | node.addError('Either "var" or "data-provider" is required'); 12 | return; 13 | } 14 | 15 | 16 | var argProps = []; 17 | var propsToRemove = []; 18 | 19 | var hasNameProp = false; 20 | node.forEachProperty(function (name, value) { 21 | if (name.startsWith('arg-')) { 22 | var argName = name.substring('arg-'.length); 23 | argProps.push(JSON.stringify(argName) + ': ' + value); 24 | propsToRemove.push(name); 25 | } else if (name === 'name') { 26 | hasNameProp = true; 27 | } 28 | }); 29 | 30 | if (!hasNameProp) { 31 | var name = node.getAttribute('data-provider'); 32 | node.setProperty('_name', name); 33 | } 34 | 35 | propsToRemove.forEach(function (propName) { 36 | node.removeProperty(propName); 37 | }); 38 | var argString; 39 | if (argProps.length) { 40 | argString = '{' + argProps.join(', ') + '}'; 41 | } 42 | var arg = node.getProperty('arg'); 43 | if (arg) { 44 | var extendFuncName = template.getStaticHelperFunction('extend', 'xt'); 45 | argString = extendFuncName + '(' + arg + ', ' + argString + ')'; 46 | } 47 | if (argString) { 48 | node.setProperty('arg', template.makeExpression(argString)); 49 | } 50 | }; 51 | -------------------------------------------------------------------------------- /async-fragment-tag.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var logger = require('raptor-logging').logger(module); 4 | var asyncWriter = require('async-writer'); 5 | var AsyncValue = require('raptor-async/AsyncValue'); 6 | var isClientReorderSupported = require('./client-reorder').isSupported; 7 | 8 | function isPromise(o) { 9 | return o && typeof o.then === 'function'; 10 | } 11 | 12 | function promiseToCallback(promise, callback, thisObj) { 13 | if (callback) { 14 | var finalPromise = promise 15 | .then(function(data) { 16 | callback(null, data); 17 | }); 18 | 19 | if (typeof promise.catch === 'function') { 20 | finalPromise = finalPromise.catch(function(err) { 21 | callback(err); 22 | }); 23 | } else if (typeof promise.fail === 'function') { 24 | finalPromise = finalPromise.fail(function(err) { 25 | callback(err); 26 | }); 27 | } 28 | 29 | if (finalPromise.done) { 30 | finalPromise.done(); 31 | } 32 | } 33 | 34 | return promise; 35 | } 36 | 37 | function requestData(provider, args, callback, thisObj) { 38 | if (isPromise(provider)) { 39 | // promises don't support a scope so we can ignore thisObj 40 | promiseToCallback(provider, callback); 41 | return; 42 | } 43 | 44 | if (typeof provider === 'function') { 45 | var data = (provider.length === 1) ? 46 | // one argument so only provide callback to function call 47 | provider.call(thisObj, callback) : 48 | // two arguments so provide args and callback to function call 49 | provider.call(thisObj, args, callback); 50 | 51 | if (data !== undefined) { 52 | if (isPromise(data)) { 53 | promiseToCallback(data, callback); 54 | } 55 | else { 56 | callback(null, data); 57 | } 58 | } 59 | } else { 60 | // Assume the provider is a data object... 61 | callback(null, provider); 62 | } 63 | } 64 | 65 | module.exports = function render(input, out) { 66 | var dataProvider = input.dataProvider; 67 | var arg = input.arg || {}; 68 | arg.out = out; 69 | 70 | var clientReorder = isClientReorderSupported && input.clientReorder === true; 71 | var asyncOut; 72 | var done = false; 73 | var timeoutId = null; 74 | var name = input.name || input._name; 75 | var scope = input.scope || this; 76 | var method = input.method; 77 | if (method) { 78 | dataProvider = dataProvider[method].bind(dataProvider); 79 | } 80 | 81 | var fragmentInfo = { 82 | name: name, 83 | clientReorder: clientReorder, 84 | dataProvider: dataProvider 85 | }; 86 | 87 | var beforeRenderEmitted = false; 88 | 89 | out.emit('asyncFragmentBegin', fragmentInfo); 90 | 91 | function renderBody(err, data, timeoutMessage) { 92 | if (fragmentInfo.finished) { 93 | return; 94 | } 95 | 96 | if (timeoutId) { 97 | clearTimeout(timeoutId); 98 | timeoutId = null; 99 | } 100 | 101 | done = true; 102 | 103 | var targetOut = fragmentInfo.out = asyncOut || out; 104 | 105 | if (!beforeRenderEmitted) { 106 | beforeRenderEmitted = true; 107 | out.emit('asyncFragmentBeforeRender', fragmentInfo); 108 | } 109 | 110 | if (err) { 111 | if (input.errorMessage) { 112 | console.error('Async fragment (' + name + ') failed. Error:', (err.stack || err)); 113 | targetOut.write(input.errorMessage); 114 | } else { 115 | targetOut.error(err); 116 | } 117 | } else if (timeoutMessage) { 118 | asyncOut.write(timeoutMessage); 119 | } else { 120 | if (input.renderBody) { 121 | input.renderBody(targetOut, data); 122 | } 123 | } 124 | 125 | fragmentInfo.finished = true; 126 | 127 | if (!clientReorder) { 128 | out.emit('asyncFragmentFinish', fragmentInfo); 129 | } 130 | 131 | if (asyncOut) { 132 | asyncOut.end(); 133 | 134 | // Only flush if we rendered asynchronously and we aren't using 135 | // client-reordering 136 | if (!clientReorder) { 137 | out.flush(); 138 | } 139 | } 140 | } 141 | 142 | requestData(dataProvider, arg, renderBody, scope); 143 | 144 | if (!fragmentInfo.finished) { 145 | var timeout = input.timeout; 146 | var timeoutMessage = input.timeoutMessage; 147 | 148 | if (timeout == null) { 149 | timeout = 10000; 150 | } else if (timeout <= 0) { 151 | timeout = null; 152 | } 153 | 154 | if (timeout != null) { 155 | timeoutId = setTimeout(function() { 156 | var message = 'Async fragment (' + name + ') timed out after ' + timeout + 'ms'; 157 | 158 | fragmentInfo.timedout = true; 159 | 160 | if (timeoutMessage) { 161 | logger.error(message); 162 | renderBody(null, null, timeoutMessage); 163 | } else { 164 | renderBody(new Error(message)); 165 | } 166 | }, timeout); 167 | } 168 | 169 | if (clientReorder) { 170 | var asyncFragmentContext = out.global.__asyncFragments || (asyncFragmentContext = out.global.__asyncFragments = { 171 | fragments: [], 172 | nextId: 0 173 | }); 174 | 175 | var id = fragmentInfo.id = input.name || (asyncFragmentContext.nextId++); 176 | 177 | if (input.placeholder) { 178 | out.write('' + (input.placeholder || '') + ''); 179 | } else { 180 | out.write(''); 181 | } 182 | 183 | var asyncValue = fragmentInfo.asyncValue = new AsyncValue(); 184 | 185 | // If `client-reorder` is enabled then we asynchronously render the async fragment to a new 186 | // AsyncWriter instance so that we can Write to a temporary in-memory buffer. 187 | asyncOut = fragmentInfo.out = asyncWriter.create(null, {global: out.global}); 188 | 189 | fragmentInfo.after = input.showAfter; 190 | 191 | var oldEmit = asyncOut.emit; 192 | 193 | // Since we are rendering the async fragment to a new and separate AsyncWriter instance, 194 | // we want to proxy any child events to the main AsyncWriter in case anyone is interested 195 | // in those events. This is also needed for the following events to be handled correctly: 196 | // 197 | // - asyncFragmentBegin 198 | // - asyncFragmentBeforeRender 199 | // - asyncFragmentFinish 200 | // 201 | asyncOut.emit = function(event) { 202 | if (event !== 'finish' && event !== 'error') { 203 | // We don't want to proxy the finish and error events since those are 204 | // very specific to the AsyncWriter associated with the async fragment 205 | out.emit.apply(out, arguments); 206 | } 207 | 208 | oldEmit.apply(asyncOut, arguments); 209 | }; 210 | 211 | asyncOut 212 | .on('finish', function() { 213 | asyncValue.resolve(asyncOut.getOutput()); 214 | }) 215 | .on('error', function(err) { 216 | asyncValue.reject(err); 217 | }); 218 | 219 | if (asyncFragmentContext.fragments) { 220 | asyncFragmentContext.fragments.push(fragmentInfo); 221 | } 222 | 223 | out.emit('asyncFragmentClientReorder', fragmentInfo); 224 | } else { 225 | out.flush(); // Flush everything up to this async fragment 226 | asyncOut = fragmentInfo.out = out.beginAsync({ 227 | timeout: 0, // We will use our code for controlling timeout 228 | name: name 229 | }); 230 | } 231 | } 232 | }; 233 | -------------------------------------------------------------------------------- /async-fragment-timeout-tag-transformer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function transform(node, compiler, template) { 4 | 5 | var asyncFragmentNode = node.parentNode; 6 | 7 | // Remove the node from the tree 8 | node.detach(); 9 | 10 | asyncFragmentNode.setProperty('timeoutMessage', node.getBodyContentExpression(template)); 11 | }; 12 | -------------------------------------------------------------------------------- /async-fragments-tag.js: -------------------------------------------------------------------------------- 1 | var clientReorder = require('./client-reorder'); 2 | 3 | module.exports = function(input, out) { 4 | var global = out.global; 5 | 6 | out.flush(); 7 | 8 | var asyncOut = out.beginAsync({ last: true, timeout: -1, name: 'async-fragments' }); 9 | out.onLast(function(next) { 10 | var asyncFragmentsContext = global.__asyncFragments; 11 | 12 | if (!asyncFragmentsContext || !asyncFragmentsContext.fragments.length) { 13 | asyncOut.end(); 14 | next(); 15 | return; 16 | } 17 | 18 | var remaining = asyncFragmentsContext.fragments.length; 19 | 20 | var done = false; 21 | 22 | function handleAsyncFragment(fragmentInfo) { 23 | fragmentInfo.asyncValue.done(function(err, html) { 24 | if (done) { 25 | return; 26 | } 27 | 28 | if (err) { 29 | done = true; 30 | return asyncOut.error(err); 31 | } 32 | 33 | if (!global._afRuntime) { 34 | asyncOut.write(clientReorder.getCode()); 35 | global._afRuntime = true; 36 | } 37 | 38 | asyncOut.write('' + 41 | ''); 45 | 46 | fragmentInfo.out.writer = asyncOut.writer; 47 | 48 | out.emit('asyncFragmentFinish', fragmentInfo); 49 | 50 | out.flush(); 51 | 52 | if (--remaining === 0) { 53 | done = true; 54 | asyncOut.end(); 55 | next(); 56 | } 57 | }); 58 | } 59 | 60 | asyncFragmentsContext.fragments.forEach(handleAsyncFragment); 61 | 62 | out.on('asyncFragmentClientReorder', function(fragmentInfo) { 63 | remaining++; 64 | handleAsyncFragment(fragmentInfo); 65 | }); 66 | 67 | // Now that we have a listener attached, we want to receive any additional 68 | // out-of-sync fragments via an event 69 | delete asyncFragmentsContext.fragments; 70 | }); 71 | }; -------------------------------------------------------------------------------- /browser.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | "require: *.js" 4 | ] 5 | } -------------------------------------------------------------------------------- /client-reorder-browser.js: -------------------------------------------------------------------------------- 1 | exports.isSupported = false; -------------------------------------------------------------------------------- /client-reorder-runtime.js: -------------------------------------------------------------------------------- 1 | function $af(id, after, doc, sourceEl, targetEl, docFragment, childNodes, i, len, af) { 2 | af = $af; 3 | 4 | if (after && !af[after]) { 5 | (af[(after = after + '$')] || (af[after] = [])).push(id); 6 | } else { 7 | doc = document; 8 | sourceEl = doc.getElementById('af' + id); 9 | targetEl = doc.getElementById('afph' + id); 10 | docFragment = doc.createDocumentFragment(); 11 | childNodes = sourceEl.childNodes; 12 | i = 0; 13 | len=childNodes.length; 14 | 15 | for (; i'; 10 | } 11 | return code; 12 | }; -------------------------------------------------------------------------------- /dust/index.js: -------------------------------------------------------------------------------- 1 | var raptorDust = require('raptor-dust'); 2 | 3 | exports.registerHelpers = function(dust) { 4 | raptorDust.registerHelpers({ 5 | 'async-fragment': { 6 | buildInput: function(chunk, context, bodies, params, renderContext) { 7 | var arg = params.arg = {}; 8 | 9 | for (var k in params) { 10 | if (params.hasOwnProperty(k)) { 11 | if (k.startsWith('arg-')) { 12 | arg[k.substring(4)] = params[k]; 13 | delete params[k]; 14 | } 15 | } 16 | } 17 | 18 | var dataProvider = params.dataProvider; 19 | if (typeof dataProvider === 'string') { 20 | var dataProviderFunc = context.get(dataProvider); 21 | if (dataProviderFunc) { 22 | params.dataProvider = dataProviderFunc; 23 | } 24 | } 25 | 26 | params.renderBody = function(out, data) { 27 | var varName = params['var']; 28 | var newContextObj = {}; 29 | newContextObj[varName] = data; 30 | var newContext = context.push(newContextObj); 31 | out.renderDustBody(bodies.block, newContext); 32 | }; 33 | 34 | return params; 35 | }, 36 | renderer: require('../async-fragment-tag') 37 | } 38 | }, dust); 39 | }; -------------------------------------------------------------------------------- /marko-taglib.json: -------------------------------------------------------------------------------- 1 | { 2 | "tags": { 3 | "async-fragment": { 4 | "renderer": "./async-fragment-tag", 5 | "attributes": { 6 | "data-provider": { 7 | "type": "expression" 8 | }, 9 | "arg": { 10 | "type": "expression", 11 | "preserve-name": true 12 | }, 13 | "arg-*": { 14 | "pattern": true, 15 | "type": "string", 16 | "preserve-name": true 17 | }, 18 | "var": { 19 | "type": "identifier" 20 | }, 21 | "timeout": { 22 | "type": "integer" 23 | }, 24 | "method": { 25 | "type": "string" 26 | }, 27 | "timeout-message": { 28 | "type": "string" 29 | }, 30 | "error-message": { 31 | "type": "string" 32 | }, 33 | "name": { 34 | "type": "string", 35 | "description": "Name of async fragment (for debugging purposes only)" 36 | }, 37 | "client-reorder": { 38 | "type": "boolean", 39 | "description": "Use JavaScript on client to move async fragment into the proper place." 40 | }, 41 | "scope": { 42 | "type": "expression" 43 | }, 44 | "show-after": { 45 | "type": "string" 46 | }, 47 | "placeholder": { 48 | "type": "string" 49 | } 50 | }, 51 | "vars": [ 52 | { 53 | "name-from-attribute": "var" 54 | } 55 | ], 56 | "transformer": "./async-fragment-tag-transformer" 57 | }, 58 | "async-fragments": { 59 | "renderer": "./async-fragments-tag", 60 | "attributes": { 61 | } 62 | }, 63 | "async-fragment-placeholder": { 64 | "node-class": "./AsyncFragmentPlaceholderNode", 65 | "transformer": "./async-fragment-placeholder-tag-transformer" 66 | }, 67 | "async-fragment-timeout": { 68 | "node-class": "./AsyncFragmentTimeoutNode", 69 | "transformer": "./async-fragment-timeout-tag-transformer" 70 | }, 71 | "async-fragment-error": { 72 | "node-class": "./AsyncFragmentErrorNode", 73 | "transformer": "./async-fragment-error-tag-transformer" 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "marko-async", 3 | "description": "Async taglib for Marko", 4 | "keywords": [ 5 | "marko-taglib", 6 | "marko", 7 | "async" 8 | ], 9 | "repository": { 10 | "type": "git", 11 | "url": "https://github.com/marko-js/marko-async.git" 12 | }, 13 | "scripts": { 14 | "test": "node_modules/.bin/jshint *.js dust/" 15 | }, 16 | "author": "Patrick Steele-Idem ", 17 | "maintainers": [ 18 | "Patrick Steele-Idem " 19 | ], 20 | "dependencies": { 21 | "async-writer": "^1.2.4", 22 | "raptor-async": "^1.0.1", 23 | "raptor-dust": "^1.1.2", 24 | "raptor-logging": "^1.0.1" 25 | }, 26 | "devDependencies": { 27 | "jshint": "^2.5.0" 28 | }, 29 | "license": "Apache License v2.0", 30 | "publishConfig": { 31 | "registry": "https://registry.npmjs.org/" 32 | }, 33 | "browser": { 34 | "./client-reorder.js": "./client-reorder-browser.js" 35 | }, 36 | "version": "2.2.2" 37 | } 38 | --------------------------------------------------------------------------------