Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Turbo: Give you Traditional App the "Single-page-app" Feel

F5dfeeef276fcfd4751f4063487a5a3f?s=47 weaverryan
December 10, 2021

Turbo: Give you Traditional App the "Single-page-app" Feel

One of the biggest draws to building a "single page app" (SPA) is that full page refreshes are gone, giving users a quick and responsive experience. And, until recently, it seemed like you needed to choose between a "responsive SPA" *or* a "traditional" app... which are often much simpler to build.

But... that's a lie! Thanks to Turbo, we can now build traditional applications that return HTML *and* have the "no-refresh" SPA experience.

In this talk, we'll look how we can incrementally convert an existing application into a Turbo-powered app with zero full page refreshes. This includes tips for how you write your JavaScript, cleaning up Turbo "previews" and other pitfalls.

We'll also get a primer on Turbo Frames and the *super* cool Turbo Streams, which allow you to (with Mercure) update any part of any user's page in real time!

So lets go build a traditional app *and* give our users the quick experience they deserve!

F5dfeeef276fcfd4751f4063487a5a3f?s=128

weaverryan

December 10, 2021
Tweet

More Decks by weaverryan

Other Decks in Programming

Transcript

  1. Turbo: Give Your Traditional App the "single-page-app" Feel by your

    friend Ryan Weaver @weaverryan
  2. > Member of the Symfony docs team 
 > Some

    Dude at SymfonyCasts.com > Husband of the talented and beloved @leannapelham symfonycasts.com twitter.com/weaverryan Hallo there!! I’m Ryan! > Father to my much more charming son, Beckett
  3. Follow along with the code: @weaverryan https://github.com/weaverryan/symfony-world-2021-turbo

  4. A Symfony UX Update Part 1 @weaverryan

  5. Symfony UX? A Movement Towards Stimulus* *a JavaScript library @weaverryan

  6. <div id="flock" data-animal="🐑">🐑</div> <button class="js-grow-flock">Grow my flock!</button> Existing Solution: const

    flockEl = document.getElementById('flock'); const growFlockLink = document.querySelector('.js-grow-flock'); growFlockLink.addEventListener('click', (event) => { event.preventDefault(); flockEl.innerHTML = flockEl.innerHTML + flockEl.dataset.animal; });
  7. Full Stimulus Tutorial: … in 5 (3?) minutes! @weaverryan

  8. On your marks… @weaverryan

  9. Get set… @weaverryan

  10. Go! @weaverryan

  11. Stimulus: create a controller // assets/controllers/grow-flock-controller.js import { Controller }

    from 'stimulus'; export default class extends Controller { connect() { // I will be called each time a matching // element appears on the page, even via Ajax! } }
  12. Attach to an Element <div data-controller="grow-flock"> <div>🐑</div> <button> Grow my

    flock! </button> </div>
  13. Add Actions: <div data-controller="grow-flock"> <div>🐑</div> <button data-action="grow-flock#grow"> Grow my flock!

    </button> </div>
  14. Actions are Simple Methods // assets/controllers/grow-flock-controller.js import { Controller }

    from 'stimulus'; export default class extends Controller { grow(event) { event.preventDefault(); console.log('I am called on click!'); } }
  15. Allow a Value to be Passed // assets/controllers/grow-flock-controller.js import {

    Controller } from 'stimulus'; export default class extends Controller { static values = { animal: String } grow(event) { event.preventDefault(); console.log('I will grow ' + this.animalValue); } }
  16. Pass in the values <div data-controller="grow-flock" data-grow-flock-animal-value="🐑"> <div>🐑</div> <button data-action="grow-flock#grow">

    Grow my flock! </button> </div>
  17. Add a Target to Find Elements <div data-controller="grow-flock" data-grow-flock-animal-value="🐑"> <div

    data-grow-flock-target="flock">🐑</div> <button data-action="grow-flock#grow"> Grow my flock! </button> </div>
  18. Access the Target // assets/controllers/grow-flock-controller.js import { Controller } from

    'stimulus'; export default class extends Controller { static values = { animal: String } static targets = ['flock']; grow(event) { event.preventDefault(); this.flockTarget.innerHTML = this.flockTarget.innerHTML + this.animalValue; } }
  19. Congrats on Learning Stimulus! 🎉 @weaverryan

  20. <div {{ stimulus_controller('grow-flock', { animal: '🐑' }) }}> <div {{

    stimulus_target(‘grow-flock', 'flock') }}>🐑</div> <button {{ stimulus_action('grow-flock', 'grow') }}> Grow my flock! </button> </div> Twig Helper Methods <div data-controller="grow-flock" data-grow-flock-animal-value="🐑"> <div data-grow-flock-target="flock">🐑</div> <button data-action="grow-flock#grow"> Grow my flock! </button> </div>
  21. The Huge Bene fi t? Your JavaScript "just works" …

    always … even if HTML is loaded later @weaverryan
  22. So how do I get this JavaScript Library? composer require

    encore @weaverryan
  23. package.json { "devDependencies": { "@symfony/stimulus-bridge": "^2.0.0", "@symfony/webpack-encore": "^1.0.0", "stimulus": "^2.0.0",

    // ... } } @weaverryan
  24. assets/bootstrap.js import { startStimulusApp } from '@symfony/stimulus-bridge'; // Registers Stimulus

    controllers from controllers.json // and the controllers/ directory export const app = startStimulusApp(require.context( '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', true, /\.(j|t)sx?$/ )); // register any custom, 3rd party controllers here // app.register('some_controller_name', SomeImportedController);
  25. assets/bootstrap.js import { startStimulusApp } from '@symfony/stimulus-bridge'; // Registers Stimulus

    controllers from controllers.json // and the controllers/ directory export const app = startStimulusApp(require.context( '@symfony/stimulus-bridge/lazy-controller-loader!./controllers', true, /\.(j|t)sx?$/ )); // register any custom, 3rd party controllers here // app.register('some_controller_name', SomeImportedController);
  26. symfony/ux Packages Nice (optional) Stimulus Controllers + Bundles @weaverryan

  27. composer require symfony/ux-chartjs { "devDependencies": { "@symfony/stimulus-bridge": "^2.0.0", "@symfony/ux-chartjs": "file:vendor/symfony/ux-chartjs/assets",

    "@symfony/webpack-encore": "^1.0.0", "chart.js": "^2.9.4" } } yarn install --force
  28. assets/controllers.json { "controllers": { "@symfony/ux-chartjs": { "chart": { "enabled": true,

    "fetch": "eager" } } } } {{ stimulus_controller(‘symfony/ux-chartjs/chart’, data) }}
  29. None
  30. Stimulus 3? No biggie - Package renamed: stimulus -> @hotwired/stimulus

    - "action parameters" - Value defaults - Debug mode @weaverryan
  31. The Lifecycle of an SPA Part 2 @weaverryan

  32. JavaScript downloaded (maybe from cache) JavaScript setup (create objects, attach

    listeners) server returns empty HTML JavaScript executes as user interacts User navigates (clicks link, submits form) SPA HTML is built
  33. JavaScript downloaded (maybe from cache) JavaScript setup (create objects, attach

    listeners) server returns full HTML JavaScript executes as user interacts User navigates (clicks link, submits form) Start Over! Traditional App
  34. SPA JavaScript boots once: is a long-running process Traditional JavaScript

    boots again… and again… and again… on every navigation
  35. The Enemy: Full Page Reloads Full Page Reloads "feel" slow

    Full Page Reloads cause your application** to reboot ** application == JavaScript parsing & initialization @weaverryan
  36. Hello Turbo Part 3 @weaverryan

  37. Turbo! Zoom! A JavaScript library Automatically turns every click &

    form submit into an AJAX call Requires (nearly) zero changes to your server Eliminates full page reloads! @weaverryan
  38. { "controllers": { "@symfony/ux-turbo": { "turbo-core": { "enabled": true, "fetch":

    "eager" } } } } composer require symfony/ux-turbo (It's a fake controller!)
  39. End Result: The Turbo JavaScript has been initialized! @weaverryan

  40. End Result: Full Page Refreshes are GONE @weaverryan

  41. The Devil is in the Details Part 4 @weaverryan

  42. No server changes you said? @weaverryan

  43. #[Route('/{animal}', name: 'app_homepage')] public function index(Request $request, string $animal =

    '🐑') { $form = $this->createForm(AnimalForm::class); $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { // ... } - return $this->render('default/index.html.twig', [ + return $this->renderForm('default/index.html.twig', [ 'animal' => $animal, - 'form' => $form->createView(), + 'form' => $form, ]); } Ok, just one
  44. The Elephant in the room: Your JavaScript needs to "support"

    only being initialized once… … and keep working while HTML changes. @weaverryan
  45. // assets/app.js $(document).ready(() => { console.log('page ready!') $('.some-element').on('click', () =>

    { console.log('clicked!'); }); }) console.log('script is done!'); Turbo Un-friendly JavaScript: Executed once per PAGE LOAD Only works for elements present on initial PAGE LOAD
  46. The solution is simple: Stimulus @weaverryan

  47. Wait, so Turbo forces me to write my JavaScript a

    speci fi c way? Boo that! @weaverryan
  48. You write your JavaScript so that it can be executed

    once and work forever. Then you get Turbo for free The Truth: @weaverryan
  49. 3rd Party JavaScript (e.g. analytics) sometimes expects full page reloads.

    This is solvable… and you share the problem with SPAs @weaverryan
  50. Extras! Part 5 @weaverryan 🥑🥑🥑

  51. Should I ever use React/Vue/etc? Sure! @weaverryan

  52. import React from 'react'; export function PrintMessage(props) { return (

    <span> {props.message} rendered at {(new Date()).toTimeString()} </span> ) } Component @weaverryan
  53. // assets/controllers/react-footer-controller.js import { Controller } from 'stimulus'; import {

    render } from 'react-dom'; import { PrintMessage } from '../components/PrintMessage'; export default class extends Controller { static values = { message: String } connect() { render( <PrintMessage message={this.messageValue} />, this.element ); } } Stimulus Controller
  54. <footer {{ stimulus_controller('react-footer', { message: 'Symfony World 2021!' }) }}></footer>

    Render wherever you want @weaverryan
  55. Tell your Controllers to be Lazy! @weaverryan

  56. // assets/controllers/react-footer-controller.js import { Controller } from 'stimulus'; import {

    render } from 'react-dom'; import { PrintMessage } from '../components/PrintMessage'; import React from 'react'; /* stimulusFetch: 'lazy' */ export default class extends Controller { // ... } Stimulus Controller Controller & dependencies are downloaded lazily The moment a matching element appears on the page
  57. Turbo Frames <iframe>… but without the "old" and "weird" @weaverryan

  58. <body> <div id="navigation">Links targeting the entire page</div> <turbo-frame id="message_1"> <h1>My

    message title</h1> <p>My message content</p> <a href="/messages/1/edit">Edit this message</a> </turbo-frame> <turbo-frame id="comments"> <div id="comment_1">One comment</div> <div id="comment_2">Two comments</div> <form action="/messages/comments">...</form> </turbo-frame> </body> <turbo-frame>
  59. Turbo Streams Update HTML elements directly from the server …

    for any user @weaverryan
  60. #[Route('/{animal}', name: 'app_homepage')] public function index(Request $request, HubInterface $hub) {

    // ... $form->handleRequest($request); if ($form->isSubmitted() && $form->isValid()) { $hub->publish(new Update( 'chat', $this->renderView('flock/animals.stream.html.twig', [ 'animal' => $animal ]) )); return $this->redirectToRoute('...'); } // ... } Publish an Update to Mercure
  61. <turbo-stream action="append" target="messages"> <template> <div id="message_1"> This div will be

    appended to the element with the DOM ID "messages". </div> </template> </turbo-stream> <turbo-stream action="prepend" target="messages"> <template> <div id="message_1"> This div will be prepended to the element with the DOM ID "messages". </div> </template> </turbo-stream> Turbo Streams
  62. What this all means for us: @weaverryan

  63. Stimulus Simple, no downside, add it incrementally, write JavaScript that

    you love & that always works. symfony/ux libraries Nice, free extra Stimulus controllers if they solve a problem you have. Turbo Your reward for writing fantastic JavaScript
  64. THANK YOU! Symfony UX Info: github.com/symfony/ux Turbo Tutorial symfonycasts.com/screencast/turbo Stimulus

    Tutorial symfonycasts.com/screencast/stimulus @weaverryan