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 SubscribeWe need to load this itemOptions
data dynamically from the server. Copy the options and then find the template for this page: templates/lift/index.html.twig
.
At the bottom, you'll find the script that loads our app. Before this, create a new global variable. So, use the window
object: window.REP_LOG_APP_PROPS =
an object with itemOptions
set to our options.
... lines 1 - 63 | |
{% block javascripts %} | |
... lines 65 - 66 | |
<script> | |
window.REP_LOG_APP_PROPS = { | |
itemOptions: [ | |
{id: 'cat', text: 'Cat'}, | |
{id: 'fat_cat', text: 'Big Fat Cat'}, | |
{id: 'laptop', text: 'My Laptop'}, | |
{id: 'coffee_cup', text: 'Coffee Cup'}, | |
{id: 'invalid_item', text: 'Dark Matter'} | |
] | |
} | |
</script> | |
... lines 78 - 79 | |
{% endblock %} |
Now, go back to rep_log_react.js
delete the old constant and, below, use window.REP_LOG_APP_PROPS.itemOptions
.
... lines 1 - 6 | |
render( | |
<RepLogApp | |
... line 9 | |
itemOptions={window.REP_LOG_APP_PROPS.itemOptions} | |
/>, | |
... line 12 | |
); |
This will work... but... now I have a question. Why couldn't we just copy this code and use it in RepLogCreator
instead of passing the prop down all the levels? You could. But, as a best practice, I don't want any of my React components to use variables on the window
object. The only place where I want you to feel safe using the global window object is inside of your entry point: it should grab all of the stuff you need, and pass it into your React app.
Like everything, don't live and die by this rule. But, the window
object is a global variable. And, just like in PHP, while global variables are easy to use, they make your code harder to debug and understand. Use them in your entry, but that's it.
Back in the template, I built the REP_LOG_APP_PROPS
variable so that we could, in theory, set other props on it. For example, add withHeart: true
.
... lines 1 - 66 | |
<script> | |
window.REP_LOG_APP_PROPS = { | |
... lines 69 - 75 | |
withHeart: true | |
} | |
</script> | |
... lines 79 - 82 |
In the entry file, to read this, we could of course use window.REP_LOG_APP_PROPS.withHeart
. Or... we can be way cooler! Use spread attributes: ...window.REP_LOG_APP_PROPS
.
... lines 1 - 6 | |
render( | |
<RepLogApp | |
... line 9 | |
{...window.REP_LOG_APP_PROPS} | |
/>, | |
... line 12 | |
); |
Suddenly all of the keys on that object will be passed as props! And this is cool: set shouldShowHeart
to false. Hmm: we're now passing withHeart=false
... but thanks to the spread prop, we're passing that prop again as true.
... lines 1 - 4 | |
const shouldShowHeart = false; | |
... lines 6 - 14 |
When you do this, the last prop always wins. Yep, we do see the heart.
This is a cool way to render a component with initial data that comes from the server.
Well, the data isn't quite dynamic yet. Let's finally finish that. Open the form: src/Form/Type/RepLogType.php
. The choices
options is the data that we want to send to React. Copy RepLog::getThingsYouCanLiftChoices()
.
Then, go into the controller that renders this page - LiftController
and find indexAction()
. First, let's dump()
that function to see what it looks like.
... lines 1 - 18 | |
public function indexAction(Request $request, RepLogRepository $replogRepo, UserRepository $userRepo) | |
{ | |
... lines 21 - 22 | |
dump(RepLog::getThingsYouCanLiftChoices());die; | |
... lines 24 - 27 | |
} | |
... lines 29 - 56 |
Move over and refresh! Interesting! It's an array... but it doesn't quite look right. Let's compare this to the structure we want. Ok, each item has an id like cat
or fat_cat
. That is the value on the array. We also need a text
key. My app is using the translator component. The keys on the dumped array need to be run through the translator to be turned into the English text.
Actually, the details aren't important. The point is this: our app does have the data we need... but we need to "tweak" it a little bit to match what our React app is expecting.
To do that, go back to the controller. To save us some tedious work, I'll paste in some code. This code uses the $translator
. To get that, add a new controller argument: TranslatorInterface $translator
.
... lines 1 - 19 | |
public function indexAction(Request $request, RepLogRepository $replogRepo, UserRepository $userRepo, TranslatorInterface $translator) | |
{ | |
... lines 22 - 23 | |
$repLogAppProps = [ | |
'itemOptions' => [], | |
]; | |
foreach (RepLog::getThingsYouCanLiftChoices() as $label => $id) { | |
$repLogAppProps['itemOptions'][] = [ | |
'id' => $id, | |
'text' => $translator->trans($label), | |
]; | |
} | |
... lines 33 - 37 | |
} | |
... lines 39 - 66 |
Cool! This code builds the structure we need: it has an itemOptions
key, we loop over each, and create the id
and text
keys. Now when we refresh, Yep! The dumped code looks exactly like our REP_LOG_APP_PROPS
JavaScript structure! Heck, we can add withHeart => true
... because I like the heart.
... lines 1 - 19 | |
public function indexAction(Request $request, RepLogRepository $replogRepo, UserRepository $userRepo, TranslatorInterface $translator) | |
{ | |
... lines 22 - 23 | |
$repLogAppProps = [ | |
'withHeart' => true, | |
... line 26 | |
]; | |
... lines 28 - 38 | |
} | |
... lines 40 - 67 |
Remove the die
and pass this into twig as a new repLogAppProps
variable.
... lines 1 - 34 | |
return $this->render('lift/index.html.twig', array( | |
... line 36 | |
'repLogAppProps' => $repLogAppProps, | |
)); | |
... lines 39 - 67 |
Ready for the last piece? Delete the old JavaScript object and replace it with: {{ repLogAppProps|json_encode|raw }}
.
... lines 1 - 63 | |
{% block javascripts %} | |
... lines 65 - 66 | |
<script> | |
window.REP_LOG_APP_PROPS = {{ repLogAppProps|json_encode|raw }}; | |
</script> | |
... lines 70 - 71 | |
{% endblock %} |
That will print that array as JSON... which of course, is the same as JavaScript.
Ah, do you love it? It's now very easy to pass dynamic values or initial state into your app. Try it: refresh!
And... woh! Our app is now basically working! Yea, we're going to look at a few more things, but I think it's time to delete our old code and put this app into the right spot. In other words, it's time to celebrate!
Start by deleting this entire old Components
directory: all of that code was used by the old app. Delete the old entry file - rep_log.js
and inside of the template, we can remove a ton of old markup. The new lift-stuff-app
div now lives right next to the leaderboard.
... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div id="lift-stuff-app"></div> | |
<div class="col-md-5"> | |
<div class="leaderboard"> | |
... lines 9 - 16 | |
</div> | |
</div> | |
</div> | |
{% endblock %} | |
... lines 21 - 36 |
Oh, and delete _form.html.twig
too - more old markup. At the bottom, remove the original script tag.
And in webpack.config.js
, delete the old entry. Wow! Webpack is angry because of the missing entry file. Stop and restart it:
yarn run encore dev-server
It builds! Go back and refresh! It's alive! And it works! Except... it's jumpy when it loads. The leaderboard starts on the left, then moves over once our app renders.
This is caused by a mistake I made. Look inside RepLogs
. This is our main presentation component: it gives us all the markup. And, it has a col-md-7
class on it. Now, it's not wrong to put grid classes like this inside React. But, this top-level grid class is a bit weird: if we tried to use this component in a different place in our site, it would always have that col-md-7
. It makes more sense sense to remove that class and, instead, in index.html.twig
, add the class there. Now, our React app will just fit inside this.
... lines 1 - 2 | |
{% block body %} | |
<div class="row"> | |
<div id="lift-stuff-app" class="col-md-7"></div> | |
... lines 6 - 18 | |
</div> | |
{% endblock %} | |
... lines 21 - 36 |
And when you reload the page, yes: no annoying jumping!
Next: we know that React can be used to create re-usable components... but we haven't really done this yet. Time to change that!
Hey Mouerr,
No problem! Sometimes we may miss something, so thank you for keeping your eyes on it ;)
Cheers!
Hey Mouerr,
Thank you for reporting this! Though I downloaded the course code and in both start/ and finish/ directories that line is uncommented. Could you give me more context? Where exactly you see that line commented out?
Cheers!
Hey bartek ,
Images are just HTML tags, so you need to pass a proper path to the image, to generate a proper path you need to use asset() Twig function. Then, just receive it and put into src attribute of img tag.
Translations - good question. You can take a look at https://github.com/willdura... - it will give you a Translator object in JS with similar to Symfony Translator Component's API. Use it to translate messages on site. Or, as an alternative solution, if you don't want to use this bundle, you will need to pass already translated text, for it use trans() or transchoice() Twig functions.
Cheers!
Hey victor
Thanks for reply!
images are loaded through require and translations are provided as ready to use props but I'll try to use this bundle!
Thank you very much for your advice!
Have a nice day ;)
Hi again! ;) Do you know how to set up BazingaJsTranslationBundle to work with webpack encore correctly? I don't want to use it globally but through import statement
Hey bartek ,
IIRC, you can install it via NPM (or Yarn) and import it in your JS files: https://github.com/willdura...
Did you try this?
Cheers!
Sorry for late reply victor ;)
yes, know it works fine but installed it by composer and yarn.
I use webpack and import Translator works but to deliver translations to Translator i used dump method but not loaded by xhr like it was written in doc but I wanted to get the same way as in jsrouting-bundle so I did it like that
const locale = document.querySelector('.js-request').dataset.locale;
import Translator from 'bazinga-translator';
const myRetrievedJSONString = require(`../../../public/js/translations/messages/${locale}.json`);
Translator.fromJSON(myRetrievedJSONString);
if you think that this solution is wrong please tell me ;)
thnkas for your time!
Hey bartek ,
Ah, ok, sounds reasonable. Though if I understand you right - I'd recommend you to use "dump" command in production to dump all the translations you needed to a static file, see: https://github.com/willdura... . And then just include/use that file directly in production instead of "{{ url('bazinga_jstranslation_js', { '_format': 'json' }) }}" or custom "Translator.fromJSON(myRetrievedJSONString);". And every time you add/update translations - you would need to re-dump them again. But in development, probably not a big deal to use the recommended "{{ url('bazinga_jstranslation_js', { '_format': 'json' }) }}".
Cheers!
Hey Narong Pattanayanon
You have to declare the window.REP_LOG_APP_PROPS
object in your template
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
}
}
Hello, please fix RepLogController ====> newRepLogAction it not returning the correct response content:
//$response = $this->createApiResponse($apiModel);
this line must be uncommented