Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Await tag #312

Merged
merged 9 commits into from
Jun 24, 2016
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
99 changes: 50 additions & 49 deletions docs/async-taglib.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Marko Async Taglib
Async Taglib
=====================

The `marko-async` taglib provides support for the more efficient and simpler "Pull Model "approach to providing templates with view model data.
Marko includes a taglib that supports the more efficient and simpler "Pull Model "approach to providing templates with view model data.

* __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.
* __Pull Model:__ Pass asynchronous data provider functions to template immediately start rendering the template. Let the template _pull_ the data needed during rendering.
Expand All @@ -12,7 +12,7 @@ The Pull Model approach to template rendering requires the use of a templating e

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.

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.
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 `await` only on the associated data provider to complete. The template rendering will only be delayed for data that the template actually needs.

# Example

Expand All @@ -30,8 +30,7 @@ module.exports = function(req, res) {
```

```html
<async-fragment data-provider=data.userProfileDataProvider
var="userProfile">
<await(userProfile from data.userProfileDataProvider)>

<ul>
<li>
Expand All @@ -45,19 +44,17 @@ module.exports = function(req, res) {
</li>
</ul>

</async-fragment>
</await>
```

# Out-of-order Flushing

The marko-async taglib also supports out-of-order flushing. Enabling out-of-order flushing requires two steps:

1. Add the `client-reorder` attribute to the `<async-fragment>` tag:<br>
1. Add the `client-reorder` attribute to the `<await>` tag:<br>

```html
<async-fragment data-provider=data.userProfileDataProvider
var="userProfile"
client-reorder=true>
<await(userProfile from data.userProfileDataProvider) client-reorder=true>

<ul>
<li>
Expand All @@ -71,98 +68,104 @@ The marko-async taglib also supports out-of-order flushing. Enabling out-of-orde
</li>
</ul>

</async-fragment>
</await>
```

2. Add the `<async-fragments>` to the end of the page.
2. Add the `<await-reorderer>` to the end of the page.

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 `<async-fragments>` 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.
If `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 `<await-reorderer>` 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.

# Taglib API

## `<async-fragment>`
## `<await>`

Supported Attributes:
**Required Argument:**
```js
<await(varName from data.provider)>
```

* __`arg`__ (expression): The argument object to provide to the data provider function.
* __`arg-<arg_name>`__ (string): An argument to add to the `arg` object provided to the data provider function.
* __`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`.
* __`data-provider`__ (expression, required): The source of data for the async fragment. Must be a reference to one of the following:
* __`var`__: Variable name to use when consuming the data provided by the data provider
* __`data provider`__: The source of data to await. Must be a reference to one of the following:
- `Function(callback)`
- `Function(args, callback)`
- `Promise`
- Data
* __`error-message`__ (string): Message to output if the fragment errors out. Specifying this will prevent the rendering from aborting.


**Supported Attributes:**

* __`arg`__ (expression): The argument object to provide to the data provider function.
* __`arg-<arg_name>`__ (string): An argument to add to the `arg` object provided to the data provider function.
* __`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`.
* __`error-message`__ (string): Message to output if the data provider errors out.
Specifying this will prevent the rendering from aborting.
* __`name`__ (string): Name to assign to this async fragment. Used for debugging purposes as well as by the `show-after` attribute (see below).
* __`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`.
* __`placeholder`__ (string): Placeholder text to show while waiting for a data provider to complete. Only applicable if `client-reorder` is set to `true`.
* __`show-after`__ (string): When `client-reorder` is set to `true` then displaying this fragment will be delayed until the referenced async fragment is shown.
* __`timeout`__ (integer): Override the default timeout of 10 seconds with this param. Units are in
milliseconds so `timeout="40000"` would give a 40 second timeout.
* __`timeout-message`__ (string): Message to output if the fragment times out. Specifying this
will prevent the rendering from aborting.
* __`var`__: Variable name to use when consuming the data provided by the data provider
* __`timeout`__ (integer): Override the default timeout of 10 seconds with this param. Units are inmilliseconds so `timeout="40000"` would give a 40 second timeout.
* __`timeout-message`__ (string): Message to output if the data provider times out. Specifying this will prevent the rendering from aborting.

## `<async-fragment-placeholder>`
## `<await-placeholder>`

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`.

Example:

```html
<async-fragment data-provider=data.userDataProvider var="user" client-reorder>
<async-fragment-placeholder>
<await(user from data.userDataProvider) client-reorder>
<await-placeholder>
Loading user data...
</async-fragment-placeholder>
</await-placeholder>

<ul>
<li>First name: ${user.firstName}</li>
<li>Last name: ${user.lastName}</li>
</ul>

</async-fragment>
</await>
```

## `<async-fragment-error>`
## `<await-error>`

This tag can be used to control what text is shown when an async fragment errors out.
This tag can be used to control what text is shown when a data provider errors out.

Example:

```html
<async-fragment data-provider=data.userDataProvider var="user">
<async-fragment-error>
<await(user from data.userDataProvider)>
<await-error>
An error occurred!
</async-fragment-error>
</await-error>

<ul>
<li>First name: ${user.firstName}</li>
<li>Last name: ${user.lastName}</li>
</ul>
</async-fragment>
</await>
```

## `<async-fragment-timeout>`
## `<await-timeout>`

This tag can be used to control what text is shown when an async fragment times out.
This tag can be used to control what text is shown when a data provider times out.

Example:

```html
<async-fragment data-provider=data.userDataProvider var="user">
<async-fragment-timeout>
A timeout occurred!
</async-fragment-timeout>
<await(user from data.userDataProvider)>
<await-timeout>
A timeout occurred!
</await-timeout>

<ul>
<li>First name: ${user.firstName}</li>
<li>Last name: ${user.lastName}</li>
</ul>
</async-fragment>
</await>
```

## `<async-fragments>`
## `<await-reorderer>`

Container for all out-of-order async fragments. If any `<async-fragment>` have `client-reorder` set to true then this tag needs to be included in the page template (typically, right before the closing `</body>` tag).
Container for all out-of-order async fragments. If any `<await>` tags have `client-reorder` set to true then this tag needs to be included in the page template (typically, right before the closing `</body>` tag).

Example:

Expand All @@ -172,9 +175,7 @@ Example:
...
<body>
...
<async-fragment ... client-reorder/>
...
<async-fragments/>
<await-reorderer/>
</body>
</html>
```
53 changes: 53 additions & 0 deletions taglibs/async/async-fragment-to-await-transformer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
'use strict';

var newTags = {
'async-fragment':'await',
'async-fragments':'await-reorderer',
'async-fragment-placeholder':'await-placeholder',
'async-fragment-timeout':'await-timeout',
'async-fragment-error':'await-error'
};

module.exports = function transform(oldNode, context) {
var oldTag = oldNode.tagName;
var newTag = newTags[oldTag];
var provider;
var varName;
var argument;

context.data = context.data || {};
context.data.warnings = context.data.warnings || {};

if(!context.data.warnings[oldTag]) {
console.warn('The <'+oldTag+'> tag is deprecated. Please use <'+newTag+'> instead.');
context.data.warnings[oldTag] = true;
}

if(oldTag == 'async-fragment'/* new: <await> */) {
// need to convert data-provider and var attributes
// to an argument: <await(var from dataProvider)>
varName = oldNode.getAttributeValue('var').value;
provider = oldNode.getAttributeValue('data-provider').toString();
argument = varName + ' from ' + provider;

// now remove the attributes
oldNode.removeAttribute('var');
oldNode.removeAttribute('data-provider');
}

if(oldTag == 'async-fragments'/* new: <await-reorderer> */) {
// all this tag ever did was handling of client reordering
// we'll remove the attribute as that's all this new tag does
oldNode.removeAttribute('client-reorder');
}

var newNode = context.createNodeForEl(
newTag,
oldNode.getAttributes(),
argument
);

newNode.body = oldNode.body;
newNode.body.node = newNode;
oldNode.replaceWith(newNode);
};
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,22 @@
module.exports = function transform(el, context) {
var parentNode = el.parentNode;

if (parentNode.tagName !== 'async-fragment') {
context.addError(el, 'The <' + el.tagName + '> should be nested within an <async-fragment> tag.');
if (parentNode.tagName === 'async-fragment') {
console.log(parentNode);
}

if (parentNode.tagName !== 'await') {
context.addError(el, 'The <' + el.tagName + '> should be nested within an <await> tag.');
return;
}

var targetProp;

if (el.tagName === 'async-fragment-error') {
if (el.tagName === 'await-error') {
targetProp = 'renderError';
} else if (el.tagName === 'async-fragment-timeout') {
} else if (el.tagName === 'await-timeout') {
targetProp = 'renderTimeout';
} else if (el.tagName === 'async-fragment-placeholder') {
} else if (el.tagName === 'await-placeholder') {
targetProp = 'renderPlaceholder';
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,27 +3,36 @@
var isObjectEmpty = require('raptor-util/isObjectEmpty');

module.exports = function transform(el, context) {
var varName = el.getAttributeValue('var');
if (varName) {
if (varName.type !== 'Literal' || typeof varName.value !== 'string') {
context.addError(el, 'The "var" attribute value should be a string');
return;
}
if(!el.argument) {
context.addError(el, 'Invalid <await> tag. Argument is missing. Example: <await(user from data.userProvider)>');
return;
}

varName = varName.value;
var parts = el.argument.split(' from ');
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using split is not a good idea since there is very small chance from might appear in the expression. I would use a regular expression that is rooted on the side of the variable name to separate out the variable name from the potentially complex expression:

var matches = / from ([$A-Z_][0-9A-Z_$])$/i.exec(argument);

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason I was thinking that split with a string argument would only split on the first instance it found, but that's definitely not the case. Once we figure out as vs from I'll change it to use a rooted regex.


if (!context.util.isValidJavaScriptIdentifier(varName)) {
context.addError(el, 'The "var" attribute value should be a valid JavaScript identifier');
return;
}
} else {
context.addError(el, 'The "var" attribute is required');
if(parts.length !== 2) {
context.addError(el, 'Invalid <await> tag. Argument is malformed. Example: <await(user from data.userProvider)>');
return;
}

var varName = parts[0];
var dataProviderAttr = parts[1];

if (!context.util.isValidJavaScriptIdentifier(varName)) {
context.addError(el, 'Invalid <await> tag. Argument\'s variable name should be a valid JavaScript identifier. Example: user, as in <await(user from data.userProvider)>');
return;
}

var builder = context.builder;

el.setAttributeValue('var', builder.literal(varName));
el.setAttributeValue('data-provider', builder.parseExpression(dataProviderAttr));
el.argument = null;

////////////////////

var attrs = el.getAttributes().concat([]);
var arg = {};
var builder = context.builder;

attrs.forEach((attr) => {
var attrName = attr.name;
Expand All @@ -34,25 +43,9 @@ module.exports = function transform(el, context) {
}
});

var dataProviderAttr = el.getAttribute('data-provider');
if (!dataProviderAttr) {
context.addError(el, 'The "data-provider" attribute is required');
return;
}

if (dataProviderAttr.value == null) {
context.addError(el, 'A value is required for the "data-provider" attribute');
return;
}

if (dataProviderAttr.value.type == 'Literal') {
context.addError(el, 'The "data-provider" attribute value should not be a literal ' + (typeof dataProviderAttr.value.value));
return;
}

var name = el.getAttributeValue('name');
if (name == null) {
el.setAttributeValue('_name', builder.literal(dataProviderAttr.rawValue));
el.setAttributeValue('_name', builder.literal(dataProviderAttr));
}

if (el.hasAttribute('arg')) {
Expand Down
File renamed without changes.
Loading