Skip to content

Commit d485a72

Browse files
samuelms1sethkinast
authored andcommitted
{?exists} and {^exists} resolve Promises and check if the result exists (#753)
Closes #752
1 parent 43c0831 commit d485a72

File tree

3 files changed

+93
-28
lines changed

3 files changed

+93
-28
lines changed

lib/dust.js

+71-25
Original file line numberDiff line numberDiff line change
@@ -725,6 +725,11 @@
725725
return this;
726726
};
727727

728+
/**
729+
* Inserts a new chunk that can be used to asynchronously render or write to it
730+
* @param callback {Function} The function that will be called with the new chunk
731+
* @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk
732+
*/
728733
Chunk.prototype.map = function(callback) {
729734
var cursor = new Chunk(this.root, this.next, this.taps),
730735
branch = new Chunk(this.root, cursor, this.taps);
@@ -740,6 +745,35 @@
740745
return cursor;
741746
};
742747

748+
/**
749+
* Like Chunk#map but additionally resolves a thenable. If the thenable succeeds the callback is invoked with
750+
* a new chunk that can be used to asynchronously render or write to it, otherwise if the thenable is rejected
751+
* then the error body is rendered if available, an error is logged, and the callback is never invoked.
752+
* @param {Chunk} The current chunk to insert a new chunk
753+
* @param thenable {Thenable} the target thenable to await
754+
* @param context {Context} context to use to render the deferred chunk
755+
* @param bodies {Object} may optionally contain an "error" for when the thenable is rejected
756+
* @param callback {Function} The function that will be called with the new chunk
757+
* @returns {Chunk} A copy of this chunk instance in order to further chain function calls on the chunk
758+
*/
759+
function mapThenable(chunk, thenable, context, bodies, callback) {
760+
return chunk.map(function(asyncChunk) {
761+
thenable.then(function(data) {
762+
try {
763+
callback(asyncChunk, data);
764+
} catch (err) {
765+
// handle errors the same way Chunk#map would. This logic is only here since the thenable defers
766+
// logic such that the try / catch in Chunk#map would not capture it.
767+
dust.log(err, ERROR);
768+
asyncChunk.setError(err);
769+
}
770+
}, function(err) {
771+
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
772+
asyncChunk.renderError(err, context, bodies).end();
773+
});
774+
});
775+
}
776+
743777
Chunk.prototype.tap = function(tap) {
744778
var taps = this.taps;
745779

@@ -861,6 +895,12 @@
861895
var body = bodies.block,
862896
skip = bodies['else'];
863897

898+
if (dust.isThenable(elem)) {
899+
return mapThenable(this, elem, context, bodies, function(chunk, data) {
900+
chunk.exists(data, context, bodies).end();
901+
});
902+
}
903+
864904
if (!dust.isEmpty(elem)) {
865905
if (body) {
866906
return body(this, context);
@@ -876,6 +916,12 @@
876916
var body = bodies.block,
877917
skip = bodies['else'];
878918

919+
if (dust.isThenable(elem)) {
920+
return mapThenable(this, elem, context, bodies, function(chunk, data) {
921+
chunk.notexists(data, context, bodies).end();
922+
});
923+
}
924+
879925
if (dust.isEmpty(elem)) {
880926
if (body) {
881927
return body(this, context);
@@ -970,27 +1016,31 @@
9701016
* @return {Chunk}
9711017
*/
9721018
Chunk.prototype.await = function(thenable, context, bodies, auto, filters) {
973-
return this.map(function(chunk) {
974-
thenable.then(function(data) {
975-
if (bodies) {
976-
chunk = chunk.section(data, context, bodies);
977-
} else {
978-
// Actually a reference. Self-closing sections don't render
979-
chunk = chunk.reference(data, context, auto, filters);
980-
}
981-
chunk.end();
982-
}, function(err) {
983-
var errorBody = bodies && bodies.error;
984-
if(errorBody) {
985-
chunk.render(errorBody, context.push(err)).end();
986-
} else {
987-
dust.log('Unhandled promise rejection in `' + context.getTemplateName() + '`', INFO);
988-
chunk.end();
989-
}
990-
});
1019+
return mapThenable(this, thenable, context, bodies, function(chunk, data) {
1020+
if (bodies) {
1021+
chunk.section(data, context, bodies).end();
1022+
} else {
1023+
// Actually a reference. Self-closing sections don't render
1024+
chunk.reference(data, context, auto, filters).end();
1025+
}
9911026
});
9921027
};
9931028

1029+
/**
1030+
* Render an error body if available
1031+
* @param err {Error} error that occurred
1032+
* @param context {Context} context to use to render the error
1033+
* @param bodies {Object} may optionally contain an "error" which will be rendered
1034+
* @return {Chunk}
1035+
*/
1036+
Chunk.prototype.renderError = function(err, context, bodies) {
1037+
var errorBody = bodies && bodies.error;
1038+
if (errorBody) {
1039+
return this.render(errorBody, context.push(err));
1040+
}
1041+
return this;
1042+
};
1043+
9941044
/**
9951045
* Reserve a chunk to be evaluated with the contents of a streamable.
9961046
* Currently an error event will bomb out the stream. Once an error
@@ -1002,8 +1052,7 @@
10021052
* @return {Chunk}
10031053
*/
10041054
Chunk.prototype.stream = function(stream, context, bodies, auto, filters) {
1005-
var body = bodies && bodies.block,
1006-
errorBody = bodies && bodies.error;
1055+
var body = bodies && bodies.block;
10071056
return this.map(function(chunk) {
10081057
var ended = false;
10091058
stream
@@ -1025,11 +1074,8 @@
10251074
if(ended) {
10261075
return;
10271076
}
1028-
if(errorBody) {
1029-
chunk.render(errorBody, context.push(err));
1030-
} else {
1031-
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
1032-
}
1077+
chunk.renderError(err, context, bodies);
1078+
dust.log('Unhandled stream error in `' + context.getTemplateName() + '`', INFO);
10331079
if(!ended) {
10341080
ended = true;
10351081
chunk.end();

test/templates.spec.js

+3-3
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ function render(test, dust) {
9393
expect(messageInLog(dust.logQueue, test.log)).toEqual(true);
9494
}
9595
if (typeof test.expected !== 'undefined') {
96-
expect(test.expected).toEqual(output);
96+
expect(output).toEqual(test.expected);
9797
}
9898
done();
9999
};
@@ -124,7 +124,7 @@ function stream(test, dust) {
124124
expect(messageInLog(dust.logQueue, test.log)).toEqual(true);
125125
}
126126
if (typeof test.expected !== 'undefined') {
127-
expect(test.expected).toEqual(result.output);
127+
expect(result.output).toEqual(test.expected);
128128
}
129129
done();
130130
};
@@ -196,7 +196,7 @@ function pipe(test, dust) {
196196
expect(messageInLog(dust.logQueue, test.log)).toEqual(true);
197197
}
198198
if (typeof test.expected !== 'undefined') {
199-
expect(test.expected).toEqual(result.data);
199+
expect(result.data).toEqual(test.expected);
200200
}
201201
if(calls === 2) {
202202
done();

test/templates/all.js

+19
Original file line numberDiff line numberDiff line change
@@ -262,6 +262,18 @@ return [
262262
message: "should setup base template for next test. hi should not be part of base block name"
263263

264264
},
265+
{
266+
name: "{?exists} supports promises and uses correct context",
267+
source: "{#a}{?b}{test}{/b}{/a}",
268+
context: {
269+
a: {
270+
b: FalsePromise(null, { test: "BAD" }),
271+
test: "GOOD"
272+
}
273+
},
274+
expected: "GOOD",
275+
message: "{?exists} supports promises and uses correct context",
276+
},
265277
{
266278
name: "issue322 use base template picks up prefix chunk data",
267279
source: '{>issue322 name="abc"/}' +
@@ -584,6 +596,13 @@ return [
584596
expected: "false",
585597
message: "empty array is treated as empty in exists"
586598
},
599+
{
600+
name: "empty array resolved from a Promise is treated as empty in exists",
601+
source: "{?emptyArrayFromPromise}true{:else}false{/emptyArrayFromPromise}",
602+
context: {"emptyArrayFromPromise": FalsePromise(null, [])},
603+
expected: "false",
604+
message: "empty array resolved from a Promise is treated as empty in exists"
605+
},
587606
{
588607
name: "empty {} is treated as non empty in exists",
589608
source: "{?object}true{:else}false{/object}",

0 commit comments

Comments
 (0)