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 SubscribeWhen you a have a super-fancy, AJAX-powered app like we do, success messages and loading animations are essential to having a beautiful user experience. Of course, you will choose how fancy you want to get: more fancy just means more complexity.
Let's look at one rough spot. Watch carefully: there's a delay between when we submit the form and when the new row appears. Sure, that was pretty quick - but if it is ever any slower, it's going to look broken.
The delay is because we are not doing an optimistic UI update: we don't set the state until after the AJAX called finishes. Let's smooth this out: let's add a "loading" row at the bottom of the table while we're saving.
To do this, our app needs to know whether or not a new rep log is currently being saved. So, we need new state! And this state will definitely live in RepLogApp
, because it's the only component that is even aware that AJAX is happening. Call it isSavingNewRepLog
and initialize it to false.
... lines 1 - 7 | |
constructor(props) { | |
... lines 9 - 10 | |
this.state = { | |
... lines 12 - 15 | |
isSavingNewRepLog: false | |
}; | |
... lines 18 - 22 | |
} | |
... lines 24 - 97 |
Down below, before we call createRepLog()
, add this.setState()
to change isSavingNewRepLog
to true
. And after the AJAX call finishes, let's break this onto multiple lines and then set this same key back to false.
... lines 1 - 38 | |
handleAddRepLog(item, reps) { | |
... lines 40 - 44 | |
this.setState({ | |
isSavingNewRepLog: true | |
}); | |
... line 48 | |
createRepLog(newRep) | |
.then(repLog => { | |
this.setState(prevState => { | |
... lines 52 - 53 | |
return { | |
repLogs: newRepLogs, | |
isSavingNewRepLog: false | |
}; | |
}) | |
}) | |
; | |
} | |
... lines 62 - 97 |
That felt great! Adding and managing new state in our smart component continues to be very simple.
Next: where do we need to use this value? The tbody
lives in RepLogList
: this is where we'll add a temporary row. So, we need to, once again, do the fancy prop-passing dance. First, all state is already passed from RepLogApp
to RepLogs
. Inside that component, define a new prop type: isSavingNewRepLog
as a required bool. Above, destructure it and then, find RepLogList
and pass it down.
... lines 1 - 17 | |
export default function RepLogs(props) { | |
const { | |
... lines 20 - 28 | |
isSavingNewRepLog | |
} = props; | |
... lines 31 - 36 | |
return ( | |
... lines 38 - 48 | |
<table className="table table-striped"> | |
... lines 50 - 57 | |
<RepLogList | |
... lines 59 - 63 | |
isSavingNewRepLog={isSavingNewRepLog} | |
/> | |
... lines 66 - 73 | |
</table> | |
... lines 75 - 83 | |
); | |
} | |
... line 86 | |
RepLogs.propTypes = { | |
... lines 88 - 96 | |
isSavingNewRepLog: PropTypes.bool.isRequired, | |
}; |
Copy the new prop type and also put it into RpeLogList
. In render()
, destructure the new variable.
... lines 1 - 3 | |
export default function RepLogList(props) { | |
const { highlightedRowId, onRowClick, onDeleteRepLog, repLogs, isLoaded, isSavingNewRepLog } = props; | |
... lines 6 - 53 | |
} | |
... line 55 | |
RepLogList.propTypes = { | |
... lines 57 - 61 | |
isSavingNewRepLog: PropTypes.bool.isRequired | |
}; |
Ok! Now we're ready. Move down to after the map function so that our new tr appears at the bottom of the table. To print the new row only when we need it, use the trick we learned earlier: isSavingNewRepLog &&
, then open a set of parentheses. Now, just add the tr
and td
: "Lifting to the database...". Give that a colSpan=4
and className="text-center"
.
... lines 1 - 22 | |
return ( | |
... lines 24 - 40 | |
{isSavingNewRepLog && ( | |
<tr> | |
<td | |
colSpan="4" | |
className="text-center" | |
... lines 46 - 48 | |
>Lifting to the database ...</td> | |
</tr> | |
)} | |
... line 52 | |
); | |
... lines 54 - 64 |
And, hmm... it might look better if we lower the opacity a bit. Do that with a style
prop. But, the style
prop works a bit different than the style HTML attribute: instead of being a string of styles, React expects an object of the styles we want. This is actually easier, but the syntax looks a bit nuts. First, we use {}
to move into JavaScript mode. Then, we add another set of {}
to define an object, with opacity: .5
.
... lines 1 - 45 | |
style={{ | |
opacity: .5 | |
}} | |
... lines 49 - 64 |
The double {{
almost looks like Twig code. But really, we're doing two separate things: entering JavaScript and then creating an object.
Try it! Move over, refresh, fill out the form and... watch closely. There it was! It was beautiful!
While we're adding some little "touches" to make the UI better, let's add a new success message when the new rep log API call finishes.
Once again, in RepLogApp
, we need new state for this message. Give it a generic name - successMessage
. We may be able to use this in a few other places, like when deleting a rep log.
... lines 1 - 7 | |
constructor(props) { | |
... lines 9 - 10 | |
this.state = { | |
... lines 12 - 16 | |
successMessage: '' | |
}; | |
... lines 19 - 23 | |
} | |
... lines 25 - 99 |
Below, after createRepLog()
finishes, update this state: successMessage
set to "Rep Log Saved!".
... lines 1 - 49 | |
createRepLog(newRep) | |
.then(repLog => { | |
this.setState(prevState => { | |
... lines 53 - 54 | |
return { | |
... lines 56 - 57 | |
successMessage: 'Rep Log Saved!' | |
}; | |
}) | |
}) | |
... lines 62 - 99 |
Cool! This time, I want to print the message right on top of the app, above the table. That markup lives in RepLogs
. Go straight into that component and define the new prop type: successMessage
as a string that's required.
... lines 1 - 95 | |
RepLogs.propTypes = { | |
... lines 97 - 106 | |
successMessage: PropTypes.string.isRequired | |
}; |
Destructure that variable... then, after the input
, use our trick: successMessage &&
open parentheses. Render a div with a few bootstrap classes: alert alert-success text-center
. Inside, print the text!
... lines 1 - 17 | |
export default function RepLogs(props) { | |
const { | |
... lines 20 - 29 | |
successMessage | |
} = props; | |
... lines 32 - 37 | |
return ( | |
... lines 39 - 51 | |
{successMessage && ( | |
<div className="alert alert-success text-center"> | |
{successMessage} | |
</div> | |
)} | |
... lines 57 - 92 | |
); | |
} | |
... lines 95 - 109 |
I love it! Head back to your browser and refresh! Let's delete a few rep logs to clean things up. Then, lift your coffee cup 12 times and, submit! Boom! There is our new message.
The only problem is that the message stays up there... forever! That should probably disappear after a few seconds. Let's do that next!
Sorry for such dumb questions but just wanted to double check. The brackets we are adding after && to encapsulate the state driven messages they are just for giving code some clarity ? I did remove and everything worked as expected.
Hey Graymath technology
Yep, you are correct, parenthesis are not needed, in this case the DIV element is rendered as a ReactJS component
Cheers!
// composer.json
{
"require": {
"php": "^7.2.0",
"ext-iconv": "*",
"composer/package-versions-deprecated": "^1.11", // 1.11.99
"doctrine/doctrine-bundle": "^1.6", // 1.9.1
"doctrine/doctrine-cache-bundle": "^1.2", // 1.3.3
"doctrine/doctrine-fixtures-bundle": "~3.0", // 3.0.2
"doctrine/doctrine-migrations-bundle": "^1.2", // v1.3.1
"doctrine/orm": "^2.5", // v2.7.2
"friendsofsymfony/jsrouting-bundle": "^2.2", // 2.2.0
"friendsofsymfony/user-bundle": "dev-master#4125505ba6eba82ddf944378a3d636081c06da0c", // dev-master
"sensio/framework-extra-bundle": "^5.1", // v5.2.0
"symfony/asset": "^4.0", // v4.1.4
"symfony/console": "^4.0", // v4.1.4
"symfony/flex": "^1.0", // v1.17.6
"symfony/form": "^4.0", // v4.1.4
"symfony/framework-bundle": "^4.0", // v4.1.4
"symfony/lts": "^4@dev", // dev-master
"symfony/monolog-bundle": "^3.1", // v3.3.0
"symfony/polyfill-apcu": "^1.0", // v1.9.0
"symfony/serializer-pack": "^1.0", // v1.0.1
"symfony/swiftmailer-bundle": "^3.1", // v3.2.3
"symfony/twig-bundle": "^4.0", // v4.1.4
"symfony/validator": "^4.0", // v4.1.4
"symfony/yaml": "^4.0", // v4.1.4
"twig/twig": "2.10.*" // v2.10.0
},
"require-dev": {
"symfony/debug-pack": "^1.0", // v1.0.6
"symfony/dotenv": "^4.0", // v4.1.4
"symfony/maker-bundle": "^1.5", // v1.5.0
"symfony/phpunit-bridge": "^4.0", // v4.1.4
"symfony/web-server-bundle": "^4.0" // v4.1.4
}
}
// package.json
{
"dependencies": {
"@babel/plugin-proposal-object-rest-spread": "^7.12.1" // 7.12.1
},
"devDependencies": {
"@babel/preset-react": "^7.0.0", // 7.12.5
"@symfony/webpack-encore": "^0.26.0", // 0.26.0
"babel-plugin-transform-object-rest-spread": "^6.26.0", // 6.26.0
"babel-plugin-transform-react-remove-prop-types": "^0.4.13", // 0.4.13
"bootstrap": "3", // 3.3.7
"copy-webpack-plugin": "^4.4.1", // 4.5.1
"core-js": "2", // 1.2.7
"eslint": "^4.19.1", // 4.19.1
"eslint-plugin-react": "^7.8.2", // 7.8.2
"font-awesome": "4", // 4.7.0
"jquery": "^3.3.1", // 3.3.1
"promise-polyfill": "^8.0.0", // 8.0.0
"prop-types": "^15.6.1", // 15.6.1
"react": "^16.3.2", // 16.4.0
"react-dom": "^16.3.2", // 16.4.0
"sass": "^1.29.0", // 1.29.0
"sass-loader": "^7.0.0", // 7.3.1
"sweetalert2": "^7.11.0", // 7.22.0
"uuid": "^3.2.1", // 3.4.0
"webpack-notifier": "^1.5.1", // 1.6.0
"whatwg-fetch": "^2.0.4" // 2.0.4
}
}
Add a "sleep(3)" into your post Action the RepLogController::newRepLogAction if you maschine is actually to fast to see the loading animation :D. This will cause your php response to idle for 3 seconds.