Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

Success Messages + The Style Attribute

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.

Start your All-Access Pass
Buy just this tutorial for $12.00

With a Subscription, click any sentence in the script to jump to that part of the video!

Login Subscribe

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

Adding the "saving" State

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.

Passing & Using the Prop

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

The style Prop

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!

Success Message

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!

Leave a comment!

3
Login or Register to join the conversation
Tim J. Avatar

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.

1 Reply
Graymath technology Avatar
Graymath technology Avatar Graymath technology | posted 4 years ago

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.

Reply

Hey Graymath technology

Yep, you are correct, parenthesis are not needed, in this case the DIV element is rendered as a ReactJS component

Cheers!

Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

While the concepts of this course are still largely applicable, it's built using an older version of Symfony (4) and React (16).

What PHP libraries does this tutorial use?

// 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
    }
}

What JavaScript libraries does this tutorial use?

// 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
    }
}
userVoice