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 SubscribeOur app is looking great! But I know, we're missing one big piece: actually making AJAX requests so that all of this saves to the server. That is coming very soon. But, we have one more piece of homework first: adding the ability to delete rep logs.
Open up RepLogList
. This is where we have a little "..." TODO. Turn this into an anchor tag with a span inside: className="fa fa-trash"
.
... lines 1 - 17 | |
<td> | |
<a href="#"> | |
<span className="fa fa-trash"></span> | |
</a> | |
</td> | |
... lines 23 - 34 |
Cool! That should get us a fancy new trash icon. Awesome.
To hook this up, we're going to go through a process that should be starting to feel familiar... hopefully boring! Here it is: when the user clicks this link in RepLogList
, we ultimately need to update the state that lives in RepLogApp
. That means we need to pass a handler callback from RepLogApp
into RepLogs
and again into RepLogList
.
In RepLogApp
, create that new function: handleDeleteRepLog
, which is a great name, because this component doesn't know and doesn't care that a link will be used to delete rep logs. Nope, it's all about the data. Give this an id
argument so we know which rep log to delete. Be lazy and log a todo.
... lines 1 - 50 | |
handleDeleteRepLog(id) { | |
console.log('todo'); | |
} | |
... lines 54 - 71 |
Next, because we have a new handler method, make sure to bind it to this
.
... lines 1 - 6 | |
constructor(props) { | |
... lines 8 - 22 | |
this.handleDeleteRepLog = this.handleDeleteRepLog.bind(this); | |
} | |
... lines 25 - 71 |
And finally, pass this as a new prop: onDeleteRepLog={this.handleDeleteRepLog}
.
... lines 1 - 54 | |
render() { | |
return ( | |
<RepLogs | |
... lines 58 - 62 | |
onDeleteRepLog={this.handleDeleteRepLog} | |
/> | |
) | |
} | |
... lines 67 - 71 |
Our work here is done. Now, move to RepLogs
. First, at the bottom, add this to propTypes
: onDeleteRepLog
is PropTypes.func.isRequired
.
... lines 1 - 82 | |
highlightedRowId: PropTypes.any, | |
... lines 84 - 90 |
Above in the function, destructure onDeleteRepLog
, find RepLogList
, and pass this again as a prop: onDeleteRepLog={onDeleteRepLog}
.
... lines 1 - 18 | |
const { | |
... lines 20 - 26 | |
} = props; | |
... lines 29 - 34 | |
<div className="col-md-7"> | |
... lines 36 - 55 | |
highlightedRowId={highlightedRowId} | |
... lines 57 - 58 | |
/> | |
... line 60 | |
<tr> | |
... lines 62 - 79 | |
... lines 81 - 90 |
Finally, move to RepLogList
. Start the same: add the new prop to propTypes
and destructure the variable.
... lines 1 - 3 | |
export default function RepLogList(props) { | |
const { highlightedRowId, onRowClick, onDeleteRepLog, repLogs } = props; | |
... lines 6 - 32 | |
} | |
... line 34 | |
RepLogList.propTypes = { | |
... lines 36 - 37 | |
onDeleteRepLog: PropTypes.func.isRequired, | |
... line 39 | |
}; |
Ultimately, we need to execute this callback onClick()
of the link. We have a choice here: create an inline arrow function, or add a function above render. If the logic is simple, both are fine. Add a new handleDeleteClick
function with two arguments: the event
and repLogId
. Start with event.preventDefault()
so the browser doesn't try to follow the link. Then, yep, just onDeleteRepLog(repLogId)
.
... lines 1 - 6 | |
const handleDeleteClick = function(event, repLogId) { | |
event.preventDefault(); | |
onDeleteRepLog(repLogId); | |
}; | |
... lines 12 - 41 |
Scroll down to hook this up: onClick={}
. Hmm, we can't call handleDeleteClick
directly... because we also need to pass it the id. No worries: use an arrow function with (event) => handleDeleteClick()
passing it event
and - because we're inside the loop, repLog.id
.
... lines 1 - 12 | |
return ( | |
... line 14 | |
{repLogs.map((repLog) => ( | |
... lines 16 - 24 | |
<a href="#" onClick={(event) => handleDeleteClick(event, repLog.id) }> | |
... lines 26 - 29 | |
))} | |
... line 31 | |
); | |
... lines 33 - 41 |
Let's try it! Refresh! It looks good... and click delete. Nothing happens, but check the console. Got it! There is our todo.
Now for the fun part! Go back to RepLogApp
. Inside the handler, we need to remove one of the repLog
objects from the repLogs
state. But... we do not want to modify the state. So, the question is: how can we remove an item from an array without changing that array?
Here's one great way: call this.setState()
and pass it the key we want to set: repLogs
. Assign this to this.state.repLogs.filter()
, passing this a callback with a repLog
argument. For the body, because I didn't add curly braces, we are returning repLog.id !== id
.
... lines 1 - 50 | |
handleDeleteRepLog(id) { | |
// remove the rep log without mutating state | |
// filter returns a new array | |
this.setState({ | |
repLogs: this.state.repLogs.filter(repLog => repLog.id !== id)} | |
); | |
} | |
... lines 58 - 75 |
The filter
function loops over each repLog
, calls our function, and if it returns true, that repLog
is added to the new array. This will give us a new, identical array... except without the one item.
This will work... but! You might also notice another, familiar problem. Because the new state depends on the existing state, we should pass setState()
a callback to avoid a possible race condition with state being set at almost the same moment.
Call, this.setState()
again, but with a callback that receives a prevState
argument. Copy the object from below, delete all of that code, and return this from our callback.
... lines 1 - 53 | |
this.setState((prevState) => { | |
return { | |
repLogs: prevState.repLogs.filter(repLog => repLog.id !== id) | |
}; | |
}); | |
... lines 59 - 77 |
That's it! Let's try it! Refresh and... click that trash! It's gone! We got it! And because React is awesome, there is no doubt that if I add a new item and try to delete it... yep - it works too. Because everything is based on state, there are no surprises.
Ok - it's finally time to start using AJAX to communicate with the server.
"Houston: no signs of life"
Start the conversation!
// 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
}
}