Making (and Keeping) a Promise
Keep on Learning!
If you liked what you've learned so far, dive in! Subscribe to get access to this tutorial plus video, code and script downloads.
With a Subscription, click any sentence in the script to jump to that part of the video!
Login SubscribeIgnore the error for a second and go down to the AJAX call. We know that this method returns a Promise
, and then we call .then()
on it:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 77 | |
handleNewFormSubmit: function(e) { | |
// ... lines 79 - 86 | |
this._saveRepLog(formData) | |
.then(function(data) { | |
// ... lines 89 - 98 | |
}); | |
}, | |
_saveRepLog: function(data) { | |
return $.ajax({ | |
url: Routing.generate('rep_log_new'), | |
method: 'POST', | |
data: JSON.stringify(data) | |
}); | |
}, | |
// ... lines 109 - 150 | |
}); | |
// ... lines 152 - 169 | |
})(window, jQuery, Routing); |
But, our handler expects that the Promise's value will be the RepLog
data. But now, it's null
because that's what the server is returning!
// ... lines 1 - 13 | |
class RepLogController extends BaseController | |
{ | |
// ... lines 16 - 64 | |
public function newRepLogAction(Request $request) | |
{ | |
// ... lines 67 - 93 | |
//$response = $this->createApiResponse($apiModel); | |
$response = new Response(null, 204); | |
// ... lines 96 - 102 | |
} | |
// ... lines 104 - 130 | |
} |
Somehow, I want to fix this method so that it once again returns a Promise whose value is the RepLog
data.
How? Well first, we're going to read the Location
header that's sent back in the response - which is the URL we can use to fetch that RepLog's data:
// ... lines 1 - 13 | |
class RepLogController extends BaseController | |
{ | |
// ... lines 16 - 64 | |
public function newRepLogAction(Request $request) | |
{ | |
// ... lines 67 - 95 | |
// setting the Location header... it's a best-practice | |
$response->headers->set( | |
'Location', | |
$this->generateUrl('rep_log_get', ['id' => $repLog->getId()]) | |
); | |
// ... lines 101 - 102 | |
} | |
// ... lines 104 - 130 | |
} |
We'll use that to make a second AJAX call to get the data we need.
Making the Second AJAX Call
Start simple: add another .then()
to this, with 3 arguments: data
, textStatus
and jqXHR
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return $.ajax({ | |
// ... lines 99 - 101 | |
}).then(function(data, textStatus, jqXHR) { | |
// ... line 103 | |
}); | |
}, | |
// ... lines 106 - 147 | |
}); | |
// ... lines 149 - 166 | |
})(window, jQuery, Routing); |
Normally, promise handlers are only passed 1 argument, but in this case jQuery cheats and passes us 3. To fetch the Location
header, say console.log(jqXHR.getResponseHeader('Location'))
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return $.ajax({ | |
// ... lines 99 - 101 | |
}).then(function(data, textStatus, jqXHR) { | |
console.log(jqXHR.getResponseHeader('Location')); | |
}); | |
}, | |
// ... lines 106 - 147 | |
}); | |
// ... lines 149 - 166 | |
})(window, jQuery, Routing); |
Go see if that works: we still get the errors, but hey! It prints /reps/76
! Cool! Let's make an AJAX call to that: copy the jqXHR
line. Then, add our favorite $.ajax()
and set the URL to that header. Add a .then()
to this Promise
with a data
argument:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return $.ajax({ | |
// ... lines 99 - 101 | |
}).then(function(data, textStatus, jqXHR) { | |
$.ajax({ | |
url: jqXHR.getResponseHeader('Location') | |
}).then(function(data) { | |
// ... lines 106 - 107 | |
}); | |
}); | |
}, | |
// ... lines 111 - 152 | |
}); | |
// ... lines 154 - 171 | |
})(window, jQuery, Routing); |
Finally, this should be the RepLog data.
To check things, add console.log('now we are REALLY done')
and also console.log(data)
to make sure it looks right:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return $.ajax({ | |
// ... lines 99 - 101 | |
}).then(function(data, textStatus, jqXHR) { | |
$.ajax({ | |
url: jqXHR.getResponseHeader('Location') | |
}).then(function(data) { | |
console.log('now we are REALLY done'); | |
console.log(data); | |
}); | |
}); | |
}, | |
// ... lines 111 - 152 | |
}); | |
// ... lines 154 - 171 | |
})(window, jQuery, Routing); |
Ok, refresh and fill out the form. Ignore the errors, because there's our message and the correct data!
Ok, now we can just return this somehow, right? Wait, that's not going to work... When we return the main $.ajax()
, that Promise
is resolved - meaning finished - the moment that the first AJAX call is made. You can see that because the errors from the handlers happen first, and then the second AJAX call finishes.
Somehow, we need to return a Promise
that isn't resolved until that second AJAX call finishes.
There are two ways to do this - we'll do the harder way... because it's a lot more interesting - but I'll mention the other way at the end.
Could we use a Promise?
What we need to do is create our own Promise
object, and take control of exactly when it's resolved and what value is passed back.
If you look at the Promise
documentation, you'll find an example of how to do this: new Promise()
with one argument: a function that has resolve
and reject
arguments. I know, it looks a little weird.
Inside of that function, you'll put your asynchronous code. And as soon as it's done, you'll call the resolve()
function and pass it whatever value should be passed to the handlers. If something goes wrong, call the reject()
function. This is effectively what jQuery is doing right now inside of its $.ajax()
function.
Browser Compatability!? Polyfill
There's one quick gotcha: not all browsers support the Promise
object. But, no worries! Google for "JavaScript Promise polyfill CDN".
A polyfill is a library that gives you functionality that's normally only available in a newer version of your language, JavaScript in this case. PHP also has polyfills: small PHP libraries that backport newer PHP functionality.
This polyfill guarantees that the Promise
object will exist in JavaScript. If it's already supported by the browser it uses that. But if not, it adds it.
Copy the es6-promise.auto.min.js
path. In the next tutorial, we'll talk all about what that es6 part means. Next, go into app/Resources/views/base.html.twig
and add a script
tag with src=""
and this path:
// ... lines 1 - 90 | |
{% block javascripts %} | |
// ... lines 92 - 96 | |
<script src="https://cdnjs.cloudflare.com/ajax/libs/es6-promise/4.0.5/es6-promise.auto.min.js"></script> | |
{% endblock %} | |
// ... lines 99 - 102 |
Now our Promise
object is guaranteed!
Creating a Promise
In _saveRepLog
, create and return a new Promise
, passing it the 1 argument it needs: a function with resolve
and reject
arguments:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return new Promise(function(resolve, reject) { | |
// ... lines 99 - 112 | |
}); | |
}, | |
// ... lines 115 - 156 | |
}); | |
// ... lines 158 - 175 | |
})(window, jQuery, Routing); |
Move all of our AJAX code inside:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return new Promise(function(resolve, reject) { | |
$.ajax({ | |
url: Routing.generate('rep_log_new'), | |
method: 'POST', | |
data: JSON.stringify(data) | |
}).then(function(data, textStatus, jqXHR) { | |
$.ajax({ | |
url: jqXHR.getResponseHeader('Location') | |
}).then(function(data) { | |
// ... lines 107 - 108 | |
}); | |
// ... lines 110 - 111 | |
}); | |
}); | |
}, | |
// ... lines 115 - 156 | |
}); | |
// ... lines 158 - 175 | |
})(window, jQuery, Routing); |
Now, all we need to do is call resolve()
when our asynchronous work is finally resolved. This happens after the second AJAX call. Great! Just call resolve()
and pass it data
:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return new Promise(function(resolve, reject) { | |
$.ajax({ | |
url: Routing.generate('rep_log_new'), | |
method: 'POST', | |
data: JSON.stringify(data) | |
}).then(function(data, textStatus, jqXHR) { | |
$.ajax({ | |
url: jqXHR.getResponseHeader('Location') | |
}).then(function(data) { | |
// we're finally done! | |
resolve(data); | |
}); | |
// ... lines 110 - 111 | |
}); | |
}); | |
}, | |
// ... lines 115 - 156 | |
}); | |
// ... lines 158 - 175 | |
})(window, jQuery, Routing); |
Finally, the RepLog
data should once again be passed to the success handlers!
Go back now and refresh. Watch the total at the bottom: lift the big fat cat 10 times and... boom! The new row was added and the total was updated. It worked!
This is huge! Our _saveRepLog
function previously returned a jqXHR
object, which implements the Promise
interface. Now, we've changed that to a real Promise
, and our code that calls this function didn't need to change at all. The .then()
and .catch()
work exactly like before. Ultimately, before and after this change, _saveRepLog()
returns a promise whose value is the RepLog
data.
Handling the Reject
Of course, we also need to call reject
, which should happen if the original AJAX call has a validation error. If you fill out the form blank now, we can see the 400 error, but it doesn't call our .catch()
handler.
No problem: after .then()
, add a .catch()
to handle the AJAX failure. Inside that, call reject()
and pass it jqXHR
: the value that our other .catch()
expects:
// ... lines 1 - 2 | |
(function(window, $, Routing) { | |
// ... lines 4 - 26 | |
$.extend(window.RepLogApp.prototype, { | |
// ... lines 28 - 96 | |
_saveRepLog: function(data) { | |
return new Promise(function(resolve, reject) { | |
$.ajax({ | |
url: Routing.generate('rep_log_new'), | |
method: 'POST', | |
data: JSON.stringify(data) | |
}).then(function(data, textStatus, jqXHR) { | |
// ... lines 104 - 109 | |
}).catch(function(jqXHR) { | |
reject(jqXHR); | |
}); | |
}); | |
}, | |
// ... lines 115 - 156 | |
}); | |
// ... lines 158 - 175 | |
})(window, jQuery, Routing); |
We could also add a .catch()
to the second AJAX call, but this should never fail under normal circumstances, so I think that's overkill.
Refresh again! And try the form blank. Perfect! But, we can get a little bit fancier.
Hi
Thanks for this greate course!
You said, there are two ways to do this. The more interesting way was, to create an own Promise but what is the second way.
I'm just curious.
Regards,
Arber
// EDIT
You show the other way in the next video. Sorry, I hadn't seen it first.