Jak kombinovat podporu Railss Ajax a stimul

V tomto příspěvku vysvětlím, jak zkoumáme přidávání interaktivity podobné SPA do správce a co jsme se zatím naučili. Používám slovo „prozkoumávání“, protože se jedná o probíhající práci, která zatím není ve Forems vidět, takže hodně nebo nemusí odrážet konečnou verzi, ale myslím, že i tak jsou z toho užitečné ponaučení.

Předpokládám, že znáte Ruby on Rails, Stimulus a koncept komponentizace.

Čeho jsme chtěli dosáhnout

Začněme ukázkou videa:

Cílem je poskytnout uživateli dojem interaktivity, a toho chceme dosáhnout, aniž bychom uvolnili plnohodnotnou jednostránkovou aplikaci na straně klienta. Administrátorské rozhraní Foremu je většinou vykreslováno na serveru a my jsme chtěli prozkoumat cestu k postupnému vylepšování zážitku, aniž bychom museli přepisovat.

Jaký je stav aktuálního správce?

V současné době je Admin na backendu na zakázku vyrobenou sbírkou ovladačů Rails, pro všechny záměry a účely součástí základní aplikace Forem. Není to externí webová aplikace, není také generována drahokamem třetí strany. Myslíme si, že zkušenost Forem Creator (a jejich veselá parta spolupracovníků) je prvořadá a vyvinula se z potřeb DEV nyní k většímu ekosystému Forem.

Protože jde o vlastní aplikaci, která se v průběhu let vyvíjela, je to samozřejmě směs technologií, které se snažíme zefektivnit, a proto potřebujeme vývoj starého dobrého průzkumného softwaru. Na frontendu aktuálně používá:jQuery, Bootstrap, vanilla JS, Stimulus, Preact, několik webových komponent a náš vlastní designový jazyk Crayons.

Proč jsme zkoumali alternativu?

Konečným cílem je zredukovat to na Crayons, Stimulus a používat Preact nebo Web Components, když je to absolutně nutné, k podpoře svižnější architektury s opětovným použitím mezi webem "frontoffice" a administrátorem, kde je to možné.

Po projednání s týmem jsem se rozhodl prozkoumat následující předpoklad (nikoli přímou citaci):„Chceme, aby akce uživatelů byly interaktivní a minimalizovaly opětovné načítání stránek, a proto budeme odesílat bloky serveru vykreslil jim HTML vložením označení do stránky." .

Pokud to zní jako barebone verze pozoruhodných frameworků, jako je Phoenix LiveView od Elixir, StimulusReflex nebo Hotwire Turbo od Rails, LiveWire od PHP, Django's Reactor... no, máte pravdu! (Bonus:můj kolega @jgaskins vytvořil klon LiveView pro Crystal)

V těchto rámcích můžete cítit vzor a poptávku, kterou splňují.

V našem případě jsme však žádný z nich nepoužili. Chtěl jsem prozkoumat, jak daleko bychom mohli zajít bez přidání celého rámce a pomocí nástrojů, které jsme měli trochu více do hloubky. Tím se sníží kognitivní zátěž pro každého, kdo se chystá prohloubit tento průzkum nebo přijmout tento vzorec pro správce jako celek.

Kromě zřejmého „proč bych měl potřebovat framework k odesílání základního HTML klientovi“, máme na straně klienta již spoustu frameworků a knihoven a frameworky obvykle trvá poměrně dlouho, než se je naučíte. Také jsme malý tým.

Takže já jsem to implementoval takto :

  • Rails a HTML na straně serveru s trochou JSON v případě potřeby. Trochu jsem ošidil omezení, která jsem si nastavil pomocí ViewComponent GitHubu, ale podobných výsledků můžete dosáhnout pomocí vestavěných Rails dílčích prvků a tento příspěvek nezachází do hloubky o ViewComponent.

  • Rails's UJS (Unobtrusive JavaScript) a Stimulus na straně klienta. UJS je knihovna zabudovaná v Rails, která umožňuje interakci JavaScriptu na DOM prostřednictvím speciálních pomocníků Rails, jako je link_to nebo button_to .

Jak to všechno do sebe zapadá?

Začněme znovu od cíle:uživatel klikne na odkaz, klientská strana odešle požadavek na server, provede se nějaká akce, odešle se zpět nějaké HTML, toto HTML se vloží do stránky .

To se stane, když uživatel například klikne na jedno z šedých polí:

Kliknutím na "E-maily" se zobrazí EmailsController což vykreslí EmailsComponent (což by opět mohlo být jen částečné), výsledné HTML je odesláno do stimulu, který volá funkci JavaScriptu vkládající HTML, čímž se dokončí přepnutí sekce.

Podívejme se na kód, krok za krokem:

Zahájení kontaktu mezi klientem a serverem

Takto je v Rails definován šedý rámeček s názvem „E-maily“:

<%= link_to admin_user_tools_emails_path(@user), remote: true,
                                                 data: { action: "ajax:success->user#replacePartial" },
                                                 class: "crayons-card box js-action" do %>
  <h4 class="crayons-subtitle-3 mb-4">Emails</h4>

  <span class="color-base-70">
    <%= pluralize(@emails.total, "past email") %>
    <% if @emails.verified %> - Verified<% end -%>
  </span>
<% end %>

a toto je příklad výsledného HTML:

<a
  class="crayons-card box js-action"
  href="/admin/users/13/tools/emails"
  data-remote="true"
  data-action="ajax:success->user#replacePartial"
>
  <h4 class="crayons-subtitle-3 mb-4">Emails</h4>

  <span class="color-base-70"> 7 past emails </span>
</a>

V takovém malém úryvku kódu se něco málo děje, pojďme si to rozbalit:

  • href="/admin/users/13/tools/emails" identifikuje to jako běžný odkaz HTML, pokud bych jej navštívil pomocí svého prohlížeče, dostal bych stejnou odpověď, jakou bude odeslán JavaScript, když uživatel aktivuje kliknutí.

  • data-remote="true" (výsledek remote: true v Ruby) je způsob, jakým Rails určuje, zda by spojení mělo být zpracováno Ajaxem nebo ne. Rails nazývá tyto vzdálené prvky, mohou to být odkazy, formuláře nebo tlačítka.

  • data-action="ajax:success->user#replacePartial" je způsob, jakým spojujeme Rails UJS
    a stimul dohromady. data-action je akce stimulu (popis, jak zpracovat událost), ajax:success je vlastní událost spouštěná Rails UJS.

To je to, co to všechno znamená:po spuštění kliknutí na odkaz nechte Rails UJS načíst odpověď přes Ajax a po úspěšné odpovědi zpracuje ajax:success událost pomocí metody replacePartial ve stimulu UserController třída .

To je spousta chování v několika řádcích. Čte se to jako deklarativní programování s dobrou abstrakcí, které funguje dobře, pokud chce člověk minimalizovat množství vlastního JavaScriptu k zápisu a potřebuje tedy popisovat chování přímo v šablonách :-)

Zdroj, na který odkaz odkazuje, je běžný úryvek HTML, při ruční návštěvě se zobrazí toto:

Skvělé (podle mého názoru) je, že celé dotyčné chování stále funguje izolovaně:je vykreslováno na straně serveru, ve výchozím nastavení se přesměrovává při odeslání tak, jak by mělo, je to v podstatě běžný HTML formulář.

Možnost hrát si s těmito komponentami izolovaně rozhodně urychluje vývoj .

Celá sekce (kterou jsem nazval ToolsComponent na serveru) funguje
v izolaci:

Co se stane na serveru, když je tento požadavek odeslán?

Ještě jednou, začněme od kódu:

module Admin
  module Users
    module Tools
      class EmailsController < Admin::ApplicationController
        layout false

        def show
          user = ::User.find(params[:user_id])

          render EmailsComponent.new(user: user), content_type: "text/html"
        end
      end
    end
  end
end

A je to. Řekneme Rails, aby nevkládal komponentu (nebo částečnou) do rozvržení, načteme uživatelský objekt a řekneme frameworku, aby vykreslil HTML, aby jej poslal zpět klientovi jako HTML (tento poslední drobný detail je důležitý, protože Railsův „vzdálený režim" výchozí text/javascript za odpověď, která nám v tomto případě moc nepomůže...).

Co dělá frontend, když obdrží HTML?

Podívejme se ještě jednou na kód:

<a
  class="crayons-card box js-action"
  href="/admin/users/13/tools/emails"
  data-remote="true"
  data-action="ajax:success->user#replacePartial"
>
  <h4 class="crayons-subtitle-3 mb-4">Emails</h4>

  <span class="color-base-70"> 7 past emails </span>
</a>

Dali jsme aplikaci pokyn, aby spustila replacePartial uvnitř stimulu
UserController , to dělá toto:

replacePartial(event) {
  event.preventDefault();
  event.stopPropagation();

  const [, , xhr] = event.detail;

  if (event.target.classList.contains('js-action')) {
    this.toolsComponentTarget.classList.add('hidden');
    this.replaceTarget.innerHTML = xhr.responseText;
    this.announceChangedSectionToScreenReader();
  }
}

Tato metoda:

  1. zabrání výchozímu chování a zastaví šíření
  2. extrahuje XMLHttpRequest vložený Rails
  3. skryje sekci, na kterou se díváme, a zobrazí novou
  4. oznámí změnu čtečce obrazovky, protože neměníme adresu URL ani neprovádíme opětovné načtení celé stránky.

Jak jsme to zpřístupnili?

Po prodiskutování s naším rezidentním guru přístupnosti, @suzanne, navrhla, abychom použili "pouze čtečku obrazovky" aria-live živel:

<div
  class="screen-reader-only"
  data-user-target="activeSection"
  aria-live="polite"
></div>

To je řízeno Stimulem, který na konci akce získá název nové sekce, oznámí ji čtečce obrazovky a změní fokus, aby byla sekce připravena k použití.

Shrnutí dosavadního průběhu

Zatím jsme viděli docela dost věcí:

  • používá vestavěné možnosti Rails k propojení kódu na straně klienta a na straně serveru přes Ajax, ale pomocí HTML na straně serveru
  • používání stimulu k naslouchání akci a rozšiřování chování, jak uznáme za vhodné, aby byl kód uspořádaný
  • nahrazení části HTML jinou částí, která je sama o sobě obsažena v komponentě, která může být alespoň funkční i bez JavaScriptu

Jak odeslat e-mail pomocí Rails and Stimulus

Zde si ukážeme, jak toto „spojení“ funguje, na příkladu odeslání e-mailu.

Začněme z pohledu uživatele:

Co dělá e-mailový formulář?

Vzhledem k tomu, že jsme v doméně UJS a stimulu dohromady, musíme se podívat na to, jak jsou propojeny:

<section
  data-controller="users--tools--ajax"
  data-action="ajax:success@document->users--tools--ajax#success ajax:error@document->users--tools--ajax#error">

  <!-- ... -->

    <%= form_with url: send_email_admin_user_path(@user) do |f| %>
      <!-- ... -->
    <% end -%>
</section>

Naše sekce "E-maily" deklaruje, že potřebuje ovladač Stimulus s názvem AjaxController a že do něj bude odesílat události Rails UJS ajax:success a ajax:error .

Po aktivaci tlačítka Odeslat e-mail odešle Rails formulář přes Ajax na server, který po úspěšném odeslání odpoví daty, v tomto případě JSON.

Co se stane na serveru?

Ještě jednou nejprve kód:

if # email sent
  respond_to do |format|
    message = "Email sent!"

    format.html do
      flash[:success] = message
      redirect_back(fallback_location: admin_users_path)
    end

    format.js { render json: { result: message }, content_type: "application/json" }
  end
end

Pokud je e-mail odeslán, server zjistí, zda se jednalo o běžné odeslání formuláře, a tak vyvolá přesměrování, nebo zda se jednalo o odeslání přes Ajax (náš případ), odešle zpět zprávu se zpětnou vazbou v JSON.

Používám zde JSON, protože se dobře hodí k upozorněním na snackbaru, ale mohli bychom poslat dobře stylizovaný HTML pro vložení pro bohatší interakci, stejně jako v první části.

Určení typu obsahu je důležité, protože výchozí Rails je text/javascript pro interakce Ajax.

Co udělá klient, jakmile obdrží úspěšnou odpověď?

export default class AjaxController extends Controller {
  success(event) {
    const [data, ,] = event.detail;
    const message = data.result;

    // close the panel and go back to the home view
    document.dispatchEvent(new CustomEvent('user:tools'));

    if (message) {
      // display success info message
      document.dispatchEvent(
        new CustomEvent('snackbar:add', { detail: { message } }),
      );
    }
  }
}

Obslužná rutina události „úspěch“ extrahuje zpětnou vazbu odeslanou serverem a poté odešle dvě vlastní události, které asynchronně komunikují se dvěma různými oblastmi stránky:

  1. user:tools komunikuje se stimulátorem UsersController řeknete mu, aby zahájil navigaci zpět na úvodní obrazovku, část "Nástroje". Jak? Prostřednictvím tohoto řádku v kódu HTML stránky kontejneru:

    data-action="user:tools@document->user#fetchAndOpenTools"
    
  2. snackbar:add komunikuje se stimulátorem SnackbarController říct mu, aby přidal novou zprávu do zásobníku zpráv, aby se zobrazil uživateli. Napsal jsem příspěvek, pokud vás zajímá, jak tato část funguje.

Jakmile je přijata první událost, je vyvolána následující funkce, která spustí volání Ajax a načte serverovou stranu ToolsComponent HTML a jeho zobrazení v uživatelském rozhraní:

fetchAndOpenTools(event) {
  event.preventDefault();
  event.stopPropagation();

  Rails.ajax({
    url: this.toolsComponentPathValue,
    type: 'get',
    success: (partial) => {
      this.replaceTarget.innerHTML =
        partial.documentElement.getElementsByClassName(
          'js-component',
        )[0].outerHTML;
      this.announceChangedSectionToScreenReader();
    },
  });
}

Rails.ajax je vestavěný v Rails UJS a příliš se neliší od použití window.fetch .

Závěry

Zde se toho děje docela dost, v závislosti na vaší úrovni obeznámenosti s hlavními částmi:Rails a Stimulus.

Podle mého názoru je Stimulus opravdu dobrý k tomu, aby udržoval vanilla JS organizovaný a připojoval chování k HTML značkám vykresleným na straně serveru, deklarativním způsobem.

Díky využití integrované podpory Ajax a tenké vrstvy Rails můžete přidat interaktivitu, aniž byste se museli spoléhat na větší rámce nebo museli přecházet na vykreslování na straně klienta.

Pokud je to něco, co vyhovuje vašemu případu použití, víte jen vy, ale doufám, že vám tento příspěvek ukázal, jak zkombinovat dva rámce, abyste zlepšili uživatelský zážitek bez strmé křivky učení, a tím zvýšili úroveň produktivity vývojářů.

Zdroje

Kromě bezpočtu vyhledávání DuckDuckGo (existuje jen malá dokumentace o tom, jak poskládat všechny části dohromady) a čtení zdrojového kódu, jsem trávil čas hlavně zde:

  • Práce s JavaScriptem v Rails
  • Referenční dokumentace stimulu
  • Lepší stimul
  • stimulus-remote-rails
  • Počáteční žádost o stažení formuláře