Merge branch 'master' of github.com:sveltejs/svelte

pull/970/head
Rich Harris 7 years ago
commit d6814d76e4

@ -728,6 +728,16 @@ export default class Generator {
}
}
if (node.type === 'AwaitBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
contextDependencies = new Map(contextDependencies);
contextDependencies.set(node.value, node.metadata.dependencies);
contextDependencies.set(node.error, node.metadata.dependencies);
contextDependenciesStack.push(contextDependencies);
}
if (node.type === 'IfBlock') {
node.metadata = contextualise(node.expression, contextDependencies, indexes);
}

@ -108,6 +108,59 @@ const preprocessors = {
node.var = block.getUniqueName(`text`);
},
AwaitBlock: (
generator: DomGenerator,
block: Block,
state: State,
node: Node,
inEachBlock: boolean,
elementStack: Node[],
componentStack: Node[],
stripWhitespace: boolean,
nextSibling: Node
) => {
cannotUseInnerHTML(node);
node.var = block.getUniqueName('await_block');
block.addDependencies(node.metadata.dependencies);
let dynamic = false;
[
['pending', null],
['then', node.value],
['catch', node.error]
].forEach(([status, arg]) => {
const child = node[status];
const context = block.getUniqueName(arg || '_');
const contexts = new Map(block.contexts);
contexts.set(arg, context);
child._block = block.child({
comment: createDebuggingComment(child, generator),
name: generator.getUniqueName(`create_${status}_block`),
params: block.params.concat(context),
context,
contexts
});
child._state = getChildState(state);
preprocessChildren(generator, child._block, child._state, child, inEachBlock, elementStack, componentStack, stripWhitespace, nextSibling);
generator.blocks.push(child._block);
if (child._block.dependencies.size > 0) {
dynamic = true;
block.addDependencies(child._block.dependencies);
}
});
node.pending._block.hasUpdateMethod = dynamic;
node.then._block.hasUpdateMethod = dynamic;
node.catch._block.hasUpdateMethod = dynamic;
},
IfBlock: (
generator: DomGenerator,
block: Block,

@ -0,0 +1,155 @@
import deindent from '../../../utils/deindent';
import visit from '../visit';
import { DomGenerator } from '../index';
import Block from '../Block';
import isDomNode from './shared/isDomNode';
import { Node } from '../../../interfaces';
import { State } from '../interfaces';
export default function visitAwaitBlock(
generator: DomGenerator,
block: Block,
state: State,
node: Node,
elementStack: Node[],
componentStack: Node[]
) {
const name = node.var;
const needsAnchor = node.next ? !isDomNode(node.next, generator) : !state.parentNode || !isDomNode(node.parent, generator);
const anchor = needsAnchor
? block.getUniqueName(`${name}_anchor`)
: (node.next && node.next.var) || 'null';
const params = block.params.join(', ');
block.contextualise(node.expression);
const { snippet } = node.metadata;
if (needsAnchor) {
block.addElement(
anchor,
`@createComment()`,
`@createComment()`,
state.parentNode
);
}
const promise = block.getUniqueName(`promise`);
const resolved = block.getUniqueName(`resolved`);
const await_block = block.getUniqueName(`await_block`);
const await_block_type = block.getUniqueName(`await_block_type`);
const token = block.getUniqueName(`token`);
const await_token = block.getUniqueName(`await_token`);
const handle_promise = block.getUniqueName(`handle_promise`);
const replace_await_block = block.getUniqueName(`replace_await_block`);
const old_block = block.getUniqueName(`old_block`);
const value = block.getUniqueName(`value`);
const error = block.getUniqueName(`error`);
const create_pending_block = node.pending._block.name;
const create_then_block = node.then._block.name;
const create_catch_block = node.catch._block.name;
block.addVariable(await_block);
block.addVariable(await_block_type);
block.addVariable(await_token);
block.addVariable(promise);
block.addVariable(resolved);
block.builders.init.addBlock(deindent`
function ${replace_await_block}(${token}, type, ${value}, ${params}) {
if (${token} !== ${await_token}) return;
var ${old_block} = ${await_block};
${await_block} = (${await_block_type} = type)(${params}, ${resolved} = ${value}, #component);
if (${old_block}) {
${old_block}.u();
${old_block}.d();
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
}
function ${handle_promise}(${promise}, ${params}) {
var ${token} = ${await_token} = {};
if (@isPromise(${promise})) {
${promise}.then(function(${value}) {
${replace_await_block}(${token}, ${create_then_block}, ${value}, ${params});
}, function (${error}) {
${replace_await_block}(${token}, ${create_catch_block}, ${error}, ${params});
});
// if we previously had a then/catch block, destroy it
if (${await_block_type} !== ${create_pending_block}) {
${replace_await_block}(${token}, ${create_pending_block}, null, ${params});
return true;
}
} else {
${resolved} = ${promise};
if (${await_block_type} !== ${create_then_block}) {
${replace_await_block}(${token}, ${create_then_block}, ${resolved}, ${params});
return true;
}
}
}
${handle_promise}(${promise} = ${snippet}, ${params});
`);
block.builders.create.addBlock(deindent`
${await_block}.c();
`);
block.builders.claim.addBlock(deindent`
${await_block}.l(${state.parentNodes});
`);
const targetNode = state.parentNode || '#target';
const anchorNode = state.parentNode ? 'null' : 'anchor';
block.builders.mount.addBlock(deindent`
${await_block}.m(${targetNode}, ${anchorNode});
`);
const conditions = [];
if (node.metadata.dependencies) {
conditions.push(
`(${node.metadata.dependencies.map(dep => `'${dep}' in changed`).join(' || ')})`
);
}
conditions.push(
`${promise} !== (${promise} = ${snippet})`,
`${handle_promise}(${promise}, ${params})`
);
if (node.pending._block.hasUpdateMethod) {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
// nothing
} else {
${await_block}.p(changed, ${params}, ${resolved});
}
`);
} else {
block.builders.update.addBlock(deindent`
if (${conditions.join(' && ')}) {
${await_block}.c();
${await_block}.m(${anchor}.parentNode, ${anchor});
}
`);
}
block.builders.destroy.addBlock(deindent`
${await_token} = null;
${await_block}.d();
`);
[node.pending, node.then, node.catch].forEach(status => {
status.children.forEach(child => {
visit(generator, status._block, status._state, child, elementStack, componentStack);
});
});
}

@ -1,3 +1,4 @@
import AwaitBlock from './AwaitBlock';
import EachBlock from './EachBlock';
import Element from './Element/Element';
import IfBlock from './IfBlock';
@ -7,6 +8,7 @@ import Text from './Text';
import { Visitor } from '../interfaces';
const visitors: Record<string, Visitor> = {
AwaitBlock,
EachBlock,
Element,
IfBlock,

@ -165,16 +165,29 @@ export default function ssr(
};
};
var escaped = {
'"': '&quot;',
"'": '&##39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
${
// TODO this is a bit hacky
/__escape/.test(generator.renderCode) && deindent`
var escaped = {
'"': '&quot;',
"'": '&##39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
`
}
function __escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
${
/__isPromise/.test(generator.renderCode) && deindent`
function __isPromise(value) {
return value && typeof value.then === 'function';
}
`
}
`.replace(/(@+|#+|%+)(\w*(?:-\w*)?)/g, (match: string, sigil: string, name: string) => {
if (sigil === '@') return generator.alias(name);

@ -10,6 +10,16 @@ const preprocessors = {
RawMustacheTag: noop,
Text: noop,
AwaitBlock: (
generator: SsrGenerator,
node: Node,
elementStack: Node[]
) => {
preprocessChildren(generator, node.pending, elementStack);
preprocessChildren(generator, node.then, elementStack);
preprocessChildren(generator, node.catch, elementStack);
},
IfBlock: (
generator: SsrGenerator,
node: Node,

@ -0,0 +1,40 @@
import visit from '../visit';
import { SsrGenerator } from '../index';
import Block from '../Block';
import { Node } from '../../../interfaces';
export default function visitAwaitBlock(
generator: SsrGenerator,
block: Block,
node: Node
) {
block.contextualise(node.expression);
const { dependencies, snippet } = node.metadata;
// TODO should this be the generator's job? It's duplicated between
// here and the equivalent DOM compiler visitor
const contexts = new Map(block.contexts);
contexts.set(node.value, '__value');
const contextDependencies = new Map(block.contextDependencies);
contextDependencies.set(node.value, dependencies);
const childBlock = block.child({
contextDependencies,
contexts
});
generator.append('${(function(__value) { if(__isPromise(__value)) return `');
node.pending.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append('`; return `');
node.then.children.forEach((child: Node) => {
visit(generator, childBlock, child);
});
generator.append(`\`;}(${snippet})) }`);
}

@ -1,3 +1,4 @@
import AwaitBlock from './AwaitBlock';
import Comment from './Comment';
import EachBlock from './EachBlock';
import Element from './Element';
@ -7,6 +8,7 @@ import RawMustacheTag from './RawMustacheTag';
import Text from './Text';
export default {
AwaitBlock,
Comment,
EachBlock,
Element,

@ -3,6 +3,7 @@ import fragment from './state/fragment';
import { whitespace } from '../utils/patterns';
import { trimStart, trimEnd } from '../utils/trim';
import getCodeFrame from '../utils/getCodeFrame';
import reservedNames from '../utils/reservedNames';
import hash from './utils/hash';
import { Node, Parsed } from '../interfaces';
import CompileError from '../utils/CompileError';
@ -139,6 +140,17 @@ export class Parser {
return match[0];
}
readIdentifier() {
const start = this.index;
const identifier = this.read(/[a-zA-Z_$][a-zA-Z0-9_$]*/);
if (reservedNames.has(identifier)) {
this.error(`'${identifier}' is a reserved word in JavaScript and cannot be used here`, start);
}
return identifier;
}
readUntil(pattern: RegExp) {
if (this.index >= this.template.length)
this.error('Unexpected end of input');

@ -5,9 +5,9 @@ import reservedNames from '../../utils/reservedNames';
import { Parser } from '../index';
import { Node } from '../../interfaces';
const validIdentifier = /[a-zA-Z_$][a-zA-Z0-9_$]*/;
function trimWhitespace(block: Node, trimBefore: boolean, trimAfter: boolean) {
if (!block.children) return; // AwaitBlock
const firstChild = block.children[0];
const lastChild = block.children[block.children.length - 1];
@ -41,16 +41,20 @@ export default function mustache(parser: Parser) {
let block = parser.current();
let expected;
if (block.type === 'ElseBlock') {
if (block.type === 'ElseBlock' || block.type === 'PendingBlock' || block.type === 'ThenBlock' || block.type === 'CatchBlock') {
block.end = start;
parser.stack.pop();
block = parser.current();
expected = 'await';
}
if (block.type === 'IfBlock') {
expected = 'if';
} else if (block.type === 'EachBlock') {
expected = 'each';
} else if (block.type === 'AwaitBlock') {
expected = 'await';
} else {
parser.error(`Unexpected block closing tag`);
}
@ -70,7 +74,7 @@ export default function mustache(parser: Parser) {
}
// strip leading/trailing whitespace as necessary
if (!block.children.length) parser.error(`Empty block`, block.start);
if (block.children && !block.children.length) parser.error(`Empty block`, block.start);
const charBefore = parser.template[block.start - 1];
const charAfter = parser.template[parser.index];
@ -131,6 +135,53 @@ export default function mustache(parser: Parser) {
};
parser.stack.push(block.else);
} else if (parser.eat('then')) {
// TODO DRY out this and the next section
const pendingBlock = parser.current();
if (pendingBlock.type === 'PendingBlock') {
pendingBlock.end = start;
parser.stack.pop();
const awaitBlock = parser.current();
parser.requireWhitespace();
awaitBlock.value = parser.readIdentifier();
parser.allowWhitespace();
parser.eat('}}', true);
const thenBlock: Node = {
start,
end: null,
type: 'ThenBlock',
children: []
};
awaitBlock.then = thenBlock;
parser.stack.push(thenBlock);
}
} else if (parser.eat('catch')) {
const thenBlock = parser.current();
if (thenBlock.type === 'ThenBlock') {
thenBlock.end = start;
parser.stack.pop();
const awaitBlock = parser.current();
parser.requireWhitespace();
awaitBlock.error = parser.readIdentifier();
parser.allowWhitespace();
parser.eat('}}', true);
const catchBlock: Node = {
start,
end: null,
type: 'CatchBlock',
children: []
};
awaitBlock.catch = catchBlock;
parser.stack.push(catchBlock);
}
} else if (parser.eat('#')) {
// {{#if foo}} or {{#each foo}}
let type;
@ -139,21 +190,50 @@ export default function mustache(parser: Parser) {
type = 'IfBlock';
} else if (parser.eat('each')) {
type = 'EachBlock';
} else if (parser.eat('await')) {
type = 'AwaitBlock';
} else {
parser.error(`Expected if or each`);
parser.error(`Expected if, each or await`);
}
parser.requireWhitespace();
const expression = readExpression(parser);
const block: Node = {
start,
end: null,
type,
expression,
children: [],
};
const block: Node = type === 'AwaitBlock' ?
{
start,
end: null,
type,
expression,
value: null,
error: null,
pending: {
start: null,
end: null,
type: 'PendingBlock',
children: []
},
then: {
start: null,
end: null,
type: 'ThenBlock',
children: []
},
catch: {
start: null,
end: null,
type: 'CatchBlock',
children: []
},
} :
{
start,
end: null,
type,
expression,
children: [],
};
parser.allowWhitespace();
@ -170,13 +250,8 @@ export default function mustache(parser: Parser) {
do {
parser.allowWhitespace();
const start = parser.index;
const destructuredContext = parser.read(validIdentifier);
const destructuredContext = parser.readIdentifier();
if (!destructuredContext) parser.error(`Expected name`);
if (reservedNames.has(destructuredContext)) {
parser.error(`'${destructuredContext}' is a reserved word in JavaScript and cannot be used here`, start);
}
block.destructuredContexts.push(destructuredContext);
parser.allowWhitespace();
@ -188,12 +263,7 @@ export default function mustache(parser: Parser) {
parser.allowWhitespace();
parser.eat(']', true);
} else {
const start = parser.index;
block.context = parser.read(validIdentifier);
if (reservedNames.has(block.context)) {
parser.error(`'${block.context}' is a reserved word in JavaScript and cannot be used here`, start);
}
block.context = parser.readIdentifier();
if (!block.context) parser.error(`Expected name`);
}
@ -201,13 +271,13 @@ export default function mustache(parser: Parser) {
if (parser.eat(',')) {
parser.allowWhitespace();
block.index = parser.read(validIdentifier);
block.index = parser.readIdentifier();
if (!block.index) parser.error(`Expected name`);
parser.allowWhitespace();
}
if (parser.eat('@')) {
block.key = parser.read(validIdentifier);
block.key = parser.readIdentifier();
if (!block.key) parser.error(`Expected name`);
parser.allowWhitespace();
}
@ -217,6 +287,11 @@ export default function mustache(parser: Parser) {
parser.current().children.push(block);
parser.stack.push(block);
if (type === 'AwaitBlock') {
block.pending.start = parser.index;
parser.stack.push(block.pending);
}
} else if (parser.eat('yield')) {
// {{yield}}
// TODO deprecate

@ -188,6 +188,14 @@ export function _unmount() {
this._fragment.u();
}
export function isPromise(value) {
return value && typeof value.then === 'function';
}
export var PENDING = {};
export var SUCCESS = {};
export var FAILURE = {};
export function removeFromStore() {
this.store._remove(this);
}

@ -22,16 +22,4 @@ SvelteComponent.renderCss = function() {
};
};
var escaped = {
'"': '&quot;',
"'": '&#39;',
'&': '&amp;',
'<': '&lt;',
'>': '&gt;'
};
function __escape(html) {
return String(html).replace(/["'&<>]/g, match => escaped[match]);
}
module.exports = SvelteComponent;

@ -41,7 +41,8 @@ describe('parse', () => {
assert.deepEqual(err.loc, expected.loc);
assert.equal(err.pos, expected.pos);
} catch (err2) {
throw err2.code === 'MODULE_NOT_FOUND' ? err : err2;
const e = err2.code === 'MODULE_NOT_FOUND' ? err : err2;
throw e;
}
}
});

@ -0,0 +1,7 @@
{{#await thePromise}}
<p>loading...</p>
{{then theValue}}
<p>the value is {{theValue}}</p>
{{catch theError}}
<p>oh no! {{theError.message}}</p>
{{/await}}

@ -0,0 +1,161 @@
{
"hash": 1040536517,
"html": {
"start": 0,
"end": 158,
"type": "Fragment",
"children": [
{
"start": 0,
"end": 158,
"type": "AwaitBlock",
"expression": {
"type": "Identifier",
"start": 9,
"end": 19,
"name": "thePromise"
},
"value": "theValue",
"error": "theError",
"pending": {
"start": 21,
"end": 41,
"type": "PendingBlock",
"children": [
{
"start": 21,
"end": 23,
"type": "Text",
"data": "\n\t"
},
{
"start": 23,
"end": 40,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 26,
"end": 36,
"type": "Text",
"data": "loading..."
}
]
},
{
"start": 40,
"end": 41,
"type": "Text",
"data": "\n"
}
]
},
"then": {
"start": 41,
"end": 93,
"type": "ThenBlock",
"children": [
{
"start": 58,
"end": 60,
"type": "Text",
"data": "\n\t"
},
{
"start": 60,
"end": 92,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 63,
"end": 76,
"type": "Text",
"data": "the value is "
},
{
"start": 76,
"end": 88,
"type": "MustacheTag",
"expression": {
"type": "Identifier",
"start": 78,
"end": 86,
"name": "theValue"
}
}
]
},
{
"start": 92,
"end": 93,
"type": "Text",
"data": "\n"
}
]
},
"catch": {
"start": 93,
"end": 148,
"type": "CatchBlock",
"children": [
{
"start": 111,
"end": 113,
"type": "Text",
"data": "\n\t"
},
{
"start": 113,
"end": 147,
"type": "Element",
"name": "p",
"attributes": [],
"children": [
{
"start": 116,
"end": 123,
"type": "Text",
"data": "oh no! "
},
{
"start": 123,
"end": 143,
"type": "MustacheTag",
"expression": {
"type": "MemberExpression",
"start": 125,
"end": 141,
"object": {
"type": "Identifier",
"start": 125,
"end": 133,
"name": "theError"
},
"property": {
"type": "Identifier",
"start": 134,
"end": 141,
"name": "message"
},
"computed": false
}
}
]
},
{
"start": 147,
"end": 148,
"type": "Text",
"data": "\n"
}
]
}
}
]
},
"css": null,
"js": null
}

@ -89,7 +89,7 @@ describe("runtime", () => {
}
} catch (err) {
failed.add(dir);
showOutput(cwd, { shared, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
showOutput(cwd, { shared, format: 'cjs', store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
}
@ -106,96 +106,97 @@ describe("runtime", () => {
const window = env();
try {
// set of hacks to support transition tests
transitionManager.running = false;
transitionManager.transitions = [];
const raf = {
time: 0,
callback: null,
tick: now => {
raf.time = now;
if (raf.callback) raf.callback();
}
};
window.performance = { now: () => raf.time };
global.requestAnimationFrame = cb => {
let called = false;
raf.callback = () => {
if (!called) {
called = true;
cb();
return Promise.resolve()
.then(() => {
// set of hacks to support transition tests
transitionManager.running = false;
transitionManager.transitions = [];
const raf = {
time: 0,
callback: null,
tick: now => {
raf.time = now;
if (raf.callback) raf.callback();
}
};
};
global.window = window;
try {
SvelteComponent = require(`./samples/${dir}/main.html`);
} catch (err) {
showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
window.performance = { now: () => raf.time };
global.requestAnimationFrame = cb => {
let called = false;
raf.callback = () => {
if (!called) {
called = true;
cb();
}
};
};
global.window = window;
try {
SvelteComponent = require(`./samples/${dir}/main.html`);
} catch (err) {
showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
// Put the constructor on window for testing
window.SvelteComponent = SvelteComponent;
global.window = window;
const target = window.document.querySelector("main");
// Put the constructor on window for testing
window.SvelteComponent = SvelteComponent;
const warnings = [];
const warn = console.warn;
console.warn = warning => {
warnings.push(warning);
};
const target = window.document.querySelector("main");
const options = Object.assign({}, {
target,
hydrate,
data: config.data,
store: config.store
}, config.options || {});
const warnings = [];
const warn = console.warn;
console.warn = warning => {
warnings.push(warning);
};
const component = new SvelteComponent(options);
const options = Object.assign({}, {
target,
hydrate,
data: config.data,
store: config.store
}, config.options || {});
console.warn = warn;
const component = new SvelteComponent(options);
if (config.error) {
unintendedError = true;
throw new Error("Expected a runtime error");
}
console.warn = warn;
if (config.warnings) {
assert.deepEqual(warnings, config.warnings);
} else if (warnings.length) {
unintendedError = true;
throw new Error("Received unexpected warnings");
}
if (config.error) {
unintendedError = true;
throw new Error("Expected a runtime error");
}
if (config.html) {
assert.htmlEqual(target.innerHTML, config.html);
}
if (config.warnings) {
assert.deepEqual(warnings, config.warnings);
} else if (warnings.length) {
unintendedError = true;
throw new Error("Received unexpected warnings");
}
if (config.test) {
config.test(assert, component, target, window, raf);
} else {
component.destroy();
assert.equal(target.innerHTML, "");
}
} catch (err) {
if (config.error && !unintendedError) {
config.error(assert, err);
} else {
failed.add(dir);
showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
}
if (config.html) {
assert.htmlEqual(target.innerHTML, config.html);
}
if (config.show) showOutput(cwd, { shared, hydratable: hydrate, store: !!compileOptions.store }, svelte);
if (config.test) {
return config.test(assert, component, target, window, raf);
} else {
component.destroy();
assert.equal(target.innerHTML, "");
}
})
.catch(err => {
if (config.error && !unintendedError) {
config.error(assert, err);
} else {
failed.add(dir);
showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte); // eslint-disable-line no-console
throw err;
}
})
.then(() => {
if (config.show) showOutput(cwd, { shared, format: 'cjs', hydratable: hydrate, store: !!compileOptions.store }, svelte);
});
});
}

@ -0,0 +1,53 @@
let fulfil;
let thePromise = new Promise(f => {
fulfil = f;
});
export default {
data: {
thePromise
},
html: `
<p>loading...</p>
<p>loading...</p>
`,
test(assert, component, target) {
fulfil(42);
return thePromise
.then(() => {
assert.htmlEqual(target.innerHTML, `
<p>the value is 42</p>
<p>the value is 42</p>
`);
let reject;
thePromise = new Promise((f, r) => {
reject = r;
});
component.set({
thePromise
});
assert.htmlEqual(target.innerHTML, `
<p>loading...</p>
<p>loading...</p>
`);
reject(new Error('something broke'));
return thePromise.catch(() => {});
})
.then(() => {
assert.htmlEqual(target.innerHTML, `
<p>oh no! something broke</p>
<p>oh no! something broke</p>
`);
});
}
};

@ -0,0 +1,15 @@
{{#await thePromise}}
<p>loading...</p>
{{then theValue}}
<p>the value is {{theValue}}</p>
{{catch theError}}
<p>oh no! {{theError.message}}</p>
{{/await}}
{{#await thePromise}}
<p>loading...</p>
{{then theValue}}
<p>the value is {{theValue}}</p>
{{catch theError}}
<p>oh no! {{theError.message}}</p>
{{/await}}

@ -0,0 +1,19 @@
export default {
data: {
thePromise: 'not actually a promise'
},
html: `
<p>the value is not actually a promise</p>
`,
test(assert, component, target) {
component.set({
thePromise: 'still not a promise'
});
assert.htmlEqual(target.innerHTML, `
<p>the value is still not a promise</p>
`);
}
};

@ -0,0 +1,7 @@
{{#await thePromise}}
<p>loading...</p>
{{then theValue}}
<p>the value is {{theValue}}</p>
{{catch theError}}
<p>oh no! {{theError.message}}</p>
{{/await}}

@ -0,0 +1,49 @@
let fulfil;
let thePromise = new Promise(f => {
fulfil = f;
});
export default {
data: {
thePromise
},
html: `
<p>loading...</p>
`,
test(assert, component, target) {
fulfil(42);
return thePromise
.then(() => {
assert.htmlEqual(target.innerHTML, `
<p>the value is 42</p>
`);
let reject;
thePromise = new Promise((f, r) => {
reject = r;
});
component.set({
thePromise
});
assert.htmlEqual(target.innerHTML, `
<p>loading...</p>
`);
reject(new Error('something broke'));
return thePromise.catch(() => {});
})
.then(() => {
assert.htmlEqual(target.innerHTML, `
<p>oh no! something broke</p>
`);
});
}
};

@ -0,0 +1,7 @@
{{#await thePromise}}
<p>loading...</p>
{{then theValue}}
<p>the value is {{theValue}}</p>
{{catch theError}}
<p>oh no! {{theError.message}}</p>
{{/await}}
Loading…
Cancel
Save