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

Hotwire: Aplicações Web Modernas e Minimalistas

Hotwire: Aplicações Web Modernas e Minimalistas

Aplicações web estão cada vez mais poderosas e ambiciosas. Com o passar do tempo, vêm ficando também demasiadamente complexas ao ponto de precisarmos separar os times de backend e frontend. Mas nem sempre foi assim, e nem precisa ser. Além disso, várias técnicas que utilizamos hoje em dia para desenvolver web apps entram em contradição com o que Roy Fielding idealizou em sua tese sobre aplicações REST.

Nessa talk, vamos discutir um pouco do que é REST, os problemas que enfrentamos ao desenvolver aplicações web com times separados de frontend e backend, e como Hotwire nos ajuda a simplificar esse processo e reduzir (e em alguns casos remover completamente!) muitas das complexidades acidentais que adquirimos com o passar do tempo no desenvolvimento de softwares para a web.

Tony Messias

October 09, 2023
Tweet

More Decks by Tony Messias

Other Decks in Programming

Transcript

  1. "Hotwire é uma abordagem alternativa para construir aplicações web modernas

    sem usar muito JavaScript, enviando HTML em vez de JSON pela rede (...)" - hotwired.dev
  2. <!-- Cada comentário com seu Turbo Frame --> <turbo-frame id="comment_{{

    $comment->id }}"> <p>{{ $comment->recordable->content }}</p> <a href="{{ route('comments.edit', $comment) }}"> Edit </a> </turbo-frame>
  3. <!-- Envolva o form em um Turbo Frame de mesmo

    ID --> <turbo-frame id="comment_{{ $comment->id }}"> <form action="{{ route('comments.update', $comment) }}" method="post" > <div> <label for="content">Content</label> <textarea name="content" id="content"> {{ $comment->recordable->content }} </textarea> </div> <button type="submit">Save</button> </form> </turbo-frame>
  4. public function update(Request $request, Recording $comment) { $comment->update(/** ... */);

    if ($request->wantsTurboStream()) { return turbo_stream_view('comments.turbo.updated', [ 'comment' => $comment, 'status' => __('Comment updated'), ]); } return redirect() ->route('comments.show', $comment) ->with('status', __('Comment updated')); }
  5. <x-turbo-stream action="replace" :target="[$comment, 'comment']" > @include('comments._comment', [ 'comment' => $comment,

    ]) </x-turbo-stream> <x-turbo-stream action="append" target="notifications"> @include('layouts._notification', [ 'message' => $status, ]) </x-turbo-stream>
  6. // Registra a action customizada… import { StreamActions } from

    "@hotwired/turbo" StreamActions.log = function () { console.log(this.getAttribute("message")) } <!-- Utilização da action --> <turbo-stream action="log" message="Hello, world" ></turbo-stream>
  7. # Adicionar a lib nas dependências: npm install --save @hotwired/turbo

    // Importar em um arquivo JavaScript: import "@hotwired/turbo"
  8. <script type="importmap"> { "imports": { "@hotwired/turbo": "https://cdn.skypack.dev/@hotwired/turbo", "@hotwired/stimulus": "https://cdn.skypack.dev/@hotwired/stimulus" }

    } </script> <script type="module"> import "@hotwired/turbo" import { Application, Controller } from "@hotwired/stimulus" window.Stimulus = Application.start() // Registrar os controllers… </script>
  9. <script type="importmap"> { "imports": { "@hotwired/turbo": "https://cdn.skypack.dev/@hotwired/turbo", "@hotwired/stimulus": "https://cdn.skypack.dev/@hotwired/stimulus" }

    } </script> <script type="module"> import "@hotwired/turbo" import { Application, Controller } from "@hotwired/stimulus" window.Stimulus = Application.start() // Registrar os controllers… </script>
  10. Stimulus.register("modal", class extends Controller { static values = { open:

    { type: Boolean, default: false }, } close() { this.openValue = false } toggle() { this.openValue = ! this.openValue } openValueChanged() { this.openValue ? this.element.showModal() : this.element.close() } })
  11. @props(['minHeight' => '', 'closable' => true]) <dialog {{ $attributes }}

    class="p-6 rounded w-full max-w-lg {{ $minHeight }}" data-controller="modal" data-action=" turbo:visit@window->modal#close turbo:submit-end->modal#closeAfterSubmitEndsSuccessfully " > <div class="flex items-center justify-end"> <button data-action="modal#close" @class([ 'sr-only' => !$closable, ])> {{ __('close') }} </button> </div> {{ $slot }} </dialog>
  12. @props(['minHeight' => '', 'closable' => true]) <dialog {{ $attributes }}

    class="p-6 rounded w-full max-w-lg {{ $minHeight }}" data-controller="modal" data-action=" turbo:visit@window->modal#close turbo:submit-end->modal#closeAfterSubmitEndsSuccessfully " > <div class="flex items-center justify-end"> <button data-action="modal#close" @class([ 'sr-only' => !$closable, ])> {{ __('close') }} </button> </div> {{ $slot }} </dialog>
  13. <x-header-link href="{{ route('posts.comments.create', $post) }}" data-turbo-frame="create_comment" >{{ __('Comment') }}</x-header-link> <x-modal

    id="create-comment-modal"> <x-turbo-frame id="create_comment" loading="lazy" class="mt-2" > <p class="text-gray-600">{{ __('Loading...') }}</p> </x-turbo-frame> </x-modal>
  14. <x-header-link href="{{ route('posts.comments.create', $post) }}" data-turbo-frame="create_comment" >{{ __('Comment') }}</x-header-link> <x-modal

    id="create-comment-modal"> <x-turbo-frame id="create_comment" loading="lazy" class="mt-2" > <p class="text-gray-600">{{ __('Loading...') }}</p> </x-turbo-frame> </x-modal>
  15. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" window.Stimulus = Application.start() Stimulus.register("modal", class extends Controller { /** … */ }) Stimulus.register("modal-trigger", class extends Controller { static outlets = ["modal"] toggle() { this.modalOutlet.toggle() } }) </script>
  16. <x-header-link href="{{ route('posts.comments.create', $post) }}" data-turbo-frame="create_comment" >{{ __('Comment') }}</x-header-link> <x-modal

    id="create-comment-modal"> <x-turbo-frame id="create_comment" loading="lazy" class="mt-2" ><!-- … --></x-turbo-frame> </x-modal>
  17. <x-header-link href="{{ route('posts.comments.create', $post) }}" data-turbo-frame="create_comment" data-controller="modal-trigger" data-modal-trigger-modal-outlet="#create-comment-modal" data-action="modal-trigger#toggle" >{{

    __('Comment') }}</x-header-link> <x-modal id="create-comment-modal"> <x-turbo-frame id="create_comment" loading="lazy" class="mt-2" ><!-- … --></x-turbo-frame> </x-modal>
  18. <x-header-link href="{{ route('posts.comments.create', $post) }}" data-turbo-frame="create_comment" data-controller="modal-trigger" data-modal-trigger-modal-outlet="#create-comment-modal" data-action="modal-trigger#toggle" >{{

    __('Comment') }}</x-header-link> <x-modal id="create-comment-modal"> <x-turbo-frame id="create_comment" loading="lazy" class="mt-2" ><!-- … --></x-turbo-frame> </x-modal>
  19. const defaultTheme = require('tailwindcss/defaultTheme'); const plugin = require('tailwindcss/plugin'); module.exports =

    { plugins: [ require("@tailwindcss/forms"), plugin(function ({ addVariant }) { return addVariant('native', ['&.native', '.native &']); }), ], };
  20. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" import { BridgeComponent, BridgeElement } from "@hotwired/strada" // … Stimulus.register("bridge--form", class extends BridgeComponent { static component = "form" static targets = [ "submit" ] submitTargetConnected(target) { const submitButton = new BridgeElement(target) const submitTitle = submitButton.title this.send("connect", { submitTitle }, () => { target.click() }) } }) </script>
  21. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" import { BridgeComponent, BridgeElement } from "@hotwired/strada" // … Stimulus.register("bridge--form", class extends BridgeComponent { static component = "form" static targets = [ "submit" ] submitTargetConnected(target) { const submitButton = new BridgeElement(target) const submitTitle = submitButton.title this.send("connect", { submitTitle }, () => { target.click() }) } }) </script>
  22. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" import { BridgeComponent, BridgeElement } from "@hotwired/strada" // … Stimulus.register("bridge--form", class extends BridgeComponent { static component = "form" static targets = [ "submit" ] submitTargetConnected(target) { const submitButton = new BridgeElement(target) const submitTitle = submitButton.title this.send("connect", { submitTitle }, () => { target.click() }) } }) </script>
  23. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" import { BridgeComponent, BridgeElement } from "@hotwired/strada" // … Stimulus.register("bridge--form", class extends BridgeComponent { static component = "form" static targets = [ "submit" ] submitTargetConnected(target) { const submitButton = new BridgeElement(target) const submitTitle = submitButton.title this.send("connect", { submitTitle }, () => { target.click() }) } }) </script>
  24. <script type="module"> import "@hotwired/turbo" import { Application, Controller } from

    "@hotwired/stimulus" import { BridgeComponent, BridgeElement } from "@hotwired/strada" // … Stimulus.register("bridge--form", class extends BridgeComponent { static component = "form" static targets = [ "submit" ] submitTargetConnected(target) { const submitButton = new BridgeElement(target) const submitTitle = submitButton.title this.send("connect", { submitTitle }, () => { target.click() }) } }) </script>
  25. <x-app-layout :title="__('Posts')" data-controller="bridge--form"> <x-container size="md"> <div class="... native:hidden"> <h2 class="...">

    {{ __('Posts') }} </h2> <div class="..."> <x-header-link data-bridge--form-target="submit" href="{{ route('posts.create') }}" > {{ __('Write') }} </x-header-link> </div> </div> // …
  26. <x-app-layout :title="__('Posts')" data-controller="bridge--form"> <x-container size="md"> <div class="... native:hidden"> <h2 class="...">

    {{ __('Posts') }} </h2> <div class="..."> <x-header-link data-bridge--form-target="submit" href="{{ route('posts.create') }}" > {{ __('Write') }} </x-header-link> </div> </div> // …
  27. class FormComponent( name: String, private val delegate: BridgeDelegate<NavDestination> ) :

    BridgeComponent<NavDestination>(name, delegate) { override fun onReceive(message: Message) { when (message.event) { "connect" -> handleConnectEvent(message) } } // … }
  28. class FormComponent( name: String, private val delegate: BridgeDelegate<NavDestination> ) :

    BridgeComponent<NavDestination>(name, delegate) { private fun handleConnectEvent(message: Message) { val data = message.data<MessageData>() ?: return binding.formSubmit.apply { text = data.title setOnClickListener { performSubmit() } } } private fun performSubmit(): Boolean { return replyTo("connect") } // … }
  29. Fim. Referências: • Documentação do Hotwire • "Turbolinks 5: I

    can't believe it's not native" • "REST: I don't Think it Means What You Think it Does" • Hypermedia Systems • "Basecamp 3 for iOS: Hybrid Architecture" github.com/tonysm @tonysmdev in/luizantoniosmessias tonysm.com