12.

ReactJS talks to your API

Share this awesome video!

|

Remove the link. In base.html.twig, we already have a few JavaScript files that are included on every page. But now, I want to include some JavaScript on just this page - I don't need this stuff everywhere.

Page-Specific JavaScript (or CSS)

Remember from earlier that those script tags live in a javascripts block. Hey, that's perfect! In the child template, we can override that block: {% block javascripts %} then {% endblock %}:

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 23
{% block javascripts %}
// ... lines 25 - 36
{% endblock %}

Now, whatever JS we put here will end up at the bottom of the layout. Perfect, right?

No, not perfect! When you override blocks, you override them completely. With this code, it will completely replace the other scripts in the base template. I don't want that! I really want to append content to this block.

The secret awesome solution to this is the parent() function:

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 23
{% block javascripts %}
{{ parent() }}
// ... lines 26 - 36
{% endblock %}

This prints all of the content from the parent block, and then we can put our cool stuff below that.

Including the ReactJS Code

Here's the goal: add some JavaScript that will make an AJAX request to the notes API endpoint and use that to render them with the same markup we had before. We'll use ReactJS to do this. It's powerful... and super fun, but if it's new to you, don't worry. We're not going to learn it now, just preview it to see how to get our API working with a JavaScript frontend.

First, include three external script tags for React itself. Next, I'm going to include one more script tag that points to a file in our project: notes.react.js:

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 23
{% block javascripts %}
{{ parent() }}
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/0.14.3/react-dom.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.8.23/browser.min.js"></script>
<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>
// ... lines 31 - 36
{% endblock %}

Let's check that file out! Remember, it's in web/js/notes.react.js:

var NoteSection = React.createClass({
getInitialState: function() {
return {
notes: []
}
},
componentDidMount: function() {
this.loadNotesFromServer();
setInterval(this.loadNotesFromServer, 2000);
},
loadNotesFromServer: function() {
$.ajax({
url: '/genus/octopus/notes',
success: function (data) {
this.setState({notes: data.notes});
}.bind(this)
});
},
render: function() {
return (
<div>
<div className="notes-container">
<h2 className="notes-header">Notes</h2>
<div><i className="fa fa-plus plus-btn"></i></div>
</div>
<NoteList notes={this.state.notes} />
</div>
);
}
});
var NoteList = React.createClass({
render: function() {
var noteNodes = this.props.notes.map(function(note) {
return (
<NoteBox username={note.username} avatarUri={note.avatarUri} date={note.date} key={note.id}>{note.note}</NoteBox>
);
});
return (
<section id="cd-timeline">
{noteNodes}
</section>
);
}
});
var NoteBox = React.createClass({
render: function() {
return (
<div className="cd-timeline-block">
<div className="cd-timeline-img">
<img src={this.props.avatarUri} className="img-circle" alt="Leanna!" />
</div>
<div className="cd-timeline-content">
<h2><a href="#">{this.props.username}</a></h2>
<p>{this.props.children}</p>
<span className="cd-date">{this.props.date}</span>
</div>
</div>
);
}
});
window.NoteSection = NoteSection;

The ReactJS App

This is a small ReactJS app that uses our API to build all of the same markup that we had on the page before, but dynamically. It uses jQuery to make the AJAX call:

69 lines | web/js/notes.react.js
var NoteSection = React.createClass({
// ... lines 2 - 12
loadNotesFromServer: function() {
$.ajax({
url: '/genus/octopus/notes',
success: function (data) {
this.setState({notes: data.notes});
}.bind(this)
});
},
// ... lines 21 - 32
});
// ... lines 34 - 69

But I have a hardcoded URL right now - /genus/octopus/notes. Obviously, that's a problem, and lame. But ignore it for a second.

Back in the template, we need to start up the ReactJS app. Add a script tag with type="text/babel" - that's a React thing. To boot the app, add ReactDOM.render:

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 23
{% block javascripts %}
// ... lines 25 - 29
<script type="text/babel" src="{{ asset('js/notes.react.js') }}"></script>
<script type="text/babel">
ReactDOM.render(
// ... lines 33 - 34
);
</script>
{% endblock %}

PhpStorm is not going to like how this looks, but ignore it. Render the NoteSection into document.getElementById('js-notes-wrapper'):

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 31
ReactDOM.render(
<NoteSection />,
document.getElementById('js-notes-wrapper')
);
// ... lines 36 - 38

Back in the HTML area, clear things out and add an empty div with this id:

38 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 4
{% block body %}
// ... lines 6 - 20
<div id="js-notes-wrapper"></div>
{% endblock %}
// ... lines 23 - 38

Everything will be rendered here.

Ya know what? I think we should try it. Refresh. It's alive! It happened quickly, but this is loading dynamically. In fact, I added some simple magic so that it checks for new comments every two seconds. Let's see if it'll update without refreshing.

In the controller, remove one of the notes - take out AquaWeaver in the middle. Back to the browser! Boom! It's gone. Now put it back. There it is! So, really cool stuff.

Generating the URL for JavaScript

But... we still have that hardcoded URL. That's still lame, and a problem. How you fix this will depend on if you're using AngularJS, ReactJS or something else. But the idea is the same: we need to pass the dynamic value into JavaScript. Change the URL to this.props.url:

69 lines | web/js/notes.react.js
var NoteSection = React.createClass({
// ... lines 2 - 12
loadNotesFromServer: function() {
$.ajax({
url: this.props.url,
// ... lines 16 - 18
});
},
// ... lines 21 - 32
});
// ... lines 34 - 69

This means that we will pass a url property to NoteSection. Since we create that in the Twig template, we'll pass it in there.

First, we need to get the URL to the API endpoint. Add var notesUrl = ''. Inside, generate the URL with twig using path(). Pass it genus_show_notes and the genusName set to name:

40 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 23
{% block javascripts %}
// ... lines 25 - 30
<script type="text/babel">
var notesUrl = '{{ path('genus_show_notes', {'genusName': name}) }}';
// ... lines 33 - 37
</script>
{% endblock %}

Yes, this is Twig inside of JavaScript. And yes, I know it can feel a little crazy.

Finally, pass this into React as a prop using url={notesUrl}:

40 lines | app/Resources/views/genus/show.html.twig
// ... lines 1 - 33
ReactDOM.render(
<NoteSection url={notesUrl} />,
document.getElementById('js-notes-wrapper')
);
// ... lines 38 - 40

Try that out. It still works very nicely.

Go Deeper!

There is also an open-source bundle called FOSJsRoutingBundle that allows you to generate URLs purely from JavaScript. It's pretty awesome.

Congrats on making it this far: it means you're serious! We've just started, but we've already created a rich HTML page and an API endpoint to fuel some sweet JavaScript. And we're just starting to scratch the surface of Symfony.

What about talking to a database, using forms, setting up security or handling API input and validation? How and why should you register your own services? And what are event listeners? The answers to these will make you truly dangerous not just in Symfony, but as a programmer in general.

See you on the next challenge.