<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Marcus</title>
    <description>The latest articles on DEV Community by Marcus (@marcusatlocalhost).</description>
    <link>https://dev.to/marcusatlocalhost</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F243268%2F8297314c-106b-45b6-a5bd-7b9333c95413.jpeg</url>
      <title>DEV Community: Marcus</title>
      <link>https://dev.to/marcusatlocalhost</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/marcusatlocalhost"/>
    <language>en</language>
    <item>
      <title>How to Load Content into a Bootstrap Offcanvas Component with HTMX and Save State as a Hash in the URL</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 25 Mar 2023 22:18:33 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/how-to-load-content-into-a-bootstrap-offcanvas-component-with-htmx-and-save-state-as-a-hash-in-the-url-27k6</link>
      <guid>https://dev.to/marcusatlocalhost/how-to-load-content-into-a-bootstrap-offcanvas-component-with-htmx-and-save-state-as-a-hash-in-the-url-27k6</guid>
      <description>&lt;p&gt;To open a &lt;a href="https://getbootstrap.com/docs/5.3/components/offcanvas/"&gt;Bootstrap Offcanvas Component&lt;/a&gt; and load some HTML fragment with HTMX, the first thing I tried was the following: &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Equip the canvas (or modal) opener with the HTMX attributes&lt;/li&gt;
&lt;li&gt;call it a day
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-light"&lt;/span&gt; 
    &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt;
    &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt; 
    &lt;span class="na"&gt;hx-select=&lt;/span&gt;&lt;span class="s"&gt;".bookmark-list"&lt;/span&gt; 
    &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;".offcanvas-body"&lt;/span&gt;
    &lt;span class="na"&gt;data-bs-toggle=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas"&lt;/span&gt;
    &lt;span class="na"&gt;data-bs-target=&lt;/span&gt;&lt;span class="s"&gt;"#offcanvas"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Open Sidebar
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; 
     &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas"&lt;/span&gt; 
     &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas offcanvas-start"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Loading...
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This approach is completely decoupled; the click event triggers both the Bootstrap behavior and the HTMX ajax call. Both are unaware of one another. &lt;/p&gt;

&lt;p&gt;If the canvas fails to open, the Ajax request may succeed, but the result cannot be viewed.&lt;/p&gt;

&lt;p&gt;Back to the drawing board!&lt;/p&gt;

&lt;h3&gt;
  
  
  Connecting the behavior of HTMX and Bootstrap JS
&lt;/h3&gt;

&lt;p&gt;The first thought is to use HTMX and listen for the 'htmx:load' event, then call &lt;a href="https://getbootstrap.com/docs/5.3/components/offcanvas/#methods"&gt;&lt;code&gt;.show()&lt;/code&gt;&lt;/a&gt; to open the Offcanvas component. That would necessitate some UI to indicate that the loading is complete before the canvas appears.&lt;/p&gt;

&lt;p&gt;Or the other way around, listen to &lt;code&gt;show.bs.offcanvas&lt;/code&gt; and then trigger &lt;code&gt;htmx.ajax()&lt;/code&gt; to pull in the server-rendered HTML. This is better because it shows, something is happening right away.&lt;/p&gt;

&lt;h3&gt;
  
  
  Saving open state in the URL
&lt;/h3&gt;

&lt;p&gt;That previous approach makes the button or link responsible for opening the canvas (or modal)?&lt;/p&gt;

&lt;p&gt;If I navigate to &lt;code&gt;/sidebar#offcanvas&lt;/code&gt; I want the sidebar to be open on page load with the HTMX ajax request triggered. The problem with this approach is, the button is the single source of truth that holds the URL that's getting loaded by HTMX via &lt;code&gt;hx-get&lt;/code&gt; (or &lt;code&gt;href&lt;/code&gt;). &lt;/p&gt;

&lt;p&gt;I could go and &lt;code&gt;htmx.find('a[href="/sidebar"]').href&lt;/code&gt; and then use that in the Ajax request. Or I trigger a click on the button, that triggers the behavior. But that seems weird and too tightly coupled.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;DOMContentLoaded&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;

    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#offcanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
    &lt;span class="kd"&gt;let&lt;/span&gt; &lt;span class="nx"&gt;bookmarksOffcanvasInstance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;bootstrap&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;Offcanvas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getOrCreateInstance&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;hash&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#offcanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;){&lt;/span&gt;
        &lt;span class="c1"&gt;// open the sidebar&lt;/span&gt;
        &lt;span class="nx"&gt;bookmarksOffcanvasInstance&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;show&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="c1"&gt;// find the button and make an&lt;/span&gt;
        &lt;span class="c1"&gt;// ajax call via &lt;/span&gt;
        &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;ajax&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;GET&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href="/sidebar"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="cm"&gt;/* target etc */&lt;/span&gt;&lt;span class="p"&gt;})&lt;/span&gt;

        &lt;span class="c1"&gt;// -- OR --&lt;/span&gt;

        &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a[href="/sidebar"]&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Make the Offcanvas component the single source of truth
&lt;/h3&gt;

&lt;p&gt;There is another way I found by watching a video&lt;sup id="fnref1"&gt;1&lt;/sup&gt; about doing something similar with AlpineJS and an open issue with HTMX&lt;sup id="fnref2"&gt;2&lt;/sup&gt; that brought me to the following solution:&lt;/p&gt;

&lt;h4&gt;
  
  
  1. The button or link to open the sidebar is only responsible for that
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-light"&lt;/span&gt; 
    &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt;
    &lt;span class="na"&gt;data-bs-toggle=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas"&lt;/span&gt;
    &lt;span class="na"&gt;data-bs-target=&lt;/span&gt;&lt;span class="s"&gt;"#offcanvas"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Open Sidebar
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h4&gt;
  
  
  2. Tie the HTMX logic to the canvas itself and trigger a HTMX custom event
&lt;/h4&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; 
     &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas"&lt;/span&gt; 
     &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas offcanvas-start"&lt;/span&gt;
     &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt; 
     &lt;span class="na"&gt;hx-select=&lt;/span&gt;&lt;span class="s"&gt;".sidebar"&lt;/span&gt; 
     &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;".offcanvas-body"&lt;/span&gt;
     &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"filter-event"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Loading...
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;script&amp;gt;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getElementById&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;offcanvas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="c1"&gt;// after the canvas was opened, trigger the hx-get with&lt;/span&gt;
&lt;span class="c1"&gt;// the custom event and add the url with the state of the canvas&lt;/span&gt;
&lt;span class="c1"&gt;// into the history&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;shown.bs.offcanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;trigger&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;filter-event&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kc"&gt;null&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;target&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;

&lt;span class="c1"&gt;// on hiding the sidebar, remove the hash&lt;/span&gt;
&lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;hide.bs.offcanvas&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;history&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pushState&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;window&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;location&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;pathname&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;})&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;See the Pen &lt;a href="https://codepen.io/localhorst/pen/RwYEXNz"&gt;&lt;br&gt;
  Untitled&lt;/a&gt; by Marcus at Localhost (&lt;a href="https://codepen.io/localhorst"&gt;@localhorst&lt;/a&gt;)&lt;br&gt;
  on &lt;a href="https://codepen.io"&gt;CodePen&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Now, all the information for the Ajax request is associated with the offcanvas component, and it does not matter what triggers the opening of the sidebar; everything is contained in a single location.&lt;/p&gt;

&lt;p&gt;My initial thought probably stems from my synchronous consideration of requests. A click alters the appearance of another element &lt;strong&gt;after&lt;/strong&gt; a page reload. &lt;/p&gt;

&lt;p&gt;With JavaScript's asynchronous nature, this behavior is out the window.&lt;/p&gt;

&lt;h3&gt;
  
  
  Bonus: Offcanvas activated by AlpineJS that triggers HTMX custom event
&lt;/h3&gt;

&lt;p&gt;I adopted the whole script for the AlpineJS inline JavaScript style and here it is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"btn btn-light"&lt;/span&gt; &lt;span class="na"&gt;href=&lt;/span&gt;&lt;span class="s"&gt;"#offcanvas"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    Open Sidebar
&lt;span class="nt"&gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; 
    &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas"&lt;/span&gt; 
    &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas offcanvas-start"&lt;/span&gt;

    &lt;span class="na"&gt;x-data&lt;/span&gt;
    &lt;span class="na"&gt;x-init=&lt;/span&gt;&lt;span class="s"&gt;"()=&amp;gt;{
        const oc = new bootstrap.Offcanvas('#offcanvas');
        if(location.hash === '#offcanvas') oc.show();
    }"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;hashchange.window=&lt;/span&gt;&lt;span class="s"&gt;"if(location.hash === '#offcanvas') { bootstrap.Offcanvas.getOrCreateInstance(location.hash).show() }"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;shown-bs-offcanvas.dot=&lt;/span&gt;&lt;span class="s"&gt;"
        htmx.trigger($event.target, 'filter-event');
        history.pushState(null, null, '#' + $event.target.id);"&lt;/span&gt;
    &lt;span class="err"&gt;@&lt;/span&gt;&lt;span class="na"&gt;hide-bs-offcanvas.dot=&lt;/span&gt;&lt;span class="s"&gt;"history.pushState('', document.title, window.location.pathname);"&lt;/span&gt;

    &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt; 
    &lt;span class="na"&gt;hx-select=&lt;/span&gt;&lt;span class="s"&gt;".sidebar"&lt;/span&gt; 
    &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;".offcanvas-body"&lt;/span&gt;
    &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"filter-event"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;

        &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"offcanvas-body"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
            Loading...
        &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;

&lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;url=&lt;/span&gt;&lt;span class="s"&gt;"/sidebar"&lt;/span&gt; &lt;span class="na"&gt;delay=&lt;/span&gt;&lt;span class="s"&gt;"1500"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;h2&amp;gt;&lt;/span&gt;Sidebar Headline only visible when /sidebar is directly requested&lt;span class="nt"&gt;&amp;lt;/h2&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;class=&lt;/span&gt;&lt;span class="s"&gt;"sidebar"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
        Sidebar
    &lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A working Codepen can be found under &lt;a href="https://codepen.io/localhorst/pen/RwYvWyE"&gt;https://codepen.io/localhorst/pen/RwYvWyE&lt;/a&gt; (log in, switch to debug mode to see that URL hash change).&lt;/p&gt;

&lt;p&gt;What I like about this approach is, it's very compact. Everything is in one place. No snippets here and bits there. You look at the markup of that component and that's all there is. At least for this demo. At the same time, it's ugly, hard to format and as complexity grows you'll end up putting stuff in a dedicated script block or so.&lt;/p&gt;

&lt;p&gt;I don't know if there is any need to add AlpineJS in the mix as a third abstraction of code.&lt;/p&gt;

&lt;p&gt;It's a matter of style and maintenance I guess. But now I know how to listen for events from Bootstrap components (&lt;a href="https://alpinejs.dev/directives/on#dot"&gt;see the &lt;code&gt;.dot&lt;/code&gt; modifier&lt;/a&gt;) in AlpineJS.&lt;/p&gt;

&lt;p&gt;If you have correction or thoughts about it, please let me know. I don't claim, that's the way to do it. I just made it work that way.&lt;/p&gt;

&lt;p&gt;Article &lt;a href="https://marcus-obst.de/blog/htmx-bootstrap-5-offcanvas"&gt;How to Load Content into a Bootstrap Offcanvas Component with HTMX and Save State as a Hash in the URL&lt;/a&gt; on my blog.&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;&lt;a href="https://laracasts.com/series/modals-with-the-tall-stack/episodes/3"&gt;https://laracasts.com/series/modals-with-the-tall-stack/episodes/3&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;&lt;a href="https://github.com/bigskysoftware/htmx/issues/701"&gt;https://github.com/bigskysoftware/htmx/issues/701&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>javascript</category>
      <category>htmx</category>
      <category>bootstrap</category>
      <category>alpinejs</category>
    </item>
    <item>
      <title>Tailwind is a Leaky Abstraction by Jake Lazaroff</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Wed, 30 Nov 2022 15:48:34 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/tailwind-is-a-leaky-abstraction-by-jake-lazaroff-p83</link>
      <guid>https://dev.to/marcusatlocalhost/tailwind-is-a-leaky-abstraction-by-jake-lazaroff-p83</guid>
      <description>&lt;p&gt;&lt;a href="https://jakelazaroff.com/words/tailwind-is-a-leaky-abstraction/"&gt;Jake Lazaroff makes convincing arguments against using Tailwind CSS.&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;My main concern was always how unreadable the HTML became, regardless of whether I added line breaks between the class names or not.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--eDzKBDv---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yn2d25le6gsmx5mqagro.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--eDzKBDv---/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/uploads/articles/yn2d25le6gsmx5mqagro.png" alt="Image description" width="761" height="388"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;a href="https://jakelazaroff.com/words/tailwind-is-a-leaky-abstraction/"&gt;https://jakelazaroff.com/words/tailwind-is-a-leaky-abstraction/&lt;/a&gt;&lt;/p&gt;

</description>
      <category>tailwindcss</category>
    </item>
    <item>
      <title>JSONPath with Alpine.js CSP build in Postman Visualizer</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sat, 30 Apr 2022 20:47:42 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/jsonpath-with-alpinejs-csp-build-in-postman-visualizer-17ob</link>
      <guid>https://dev.to/marcusatlocalhost/jsonpath-with-alpinejs-csp-build-in-postman-visualizer-17ob</guid>
      <description>&lt;p&gt;Query JSON Response with JSON Path&lt;/p&gt;

&lt;p&gt;To work more easily with Postman JSON results, I implemented &lt;a href="https://goessner.net/articles/JsonPath/index.html#e3"&gt;JSONPath&lt;/a&gt; in a &lt;a href="https://learning.postman.com/docs/sending-requests/visualizer/#visualizer-api"&gt;Postman Visualizer&lt;/a&gt;, to query the results.&lt;/p&gt;

&lt;p&gt;JSONPath is to JSON what XPath is to XML, and it helps to get into large nested objects and find what you're looking for faster.&lt;/p&gt;

&lt;p&gt;First I was using just a jQuery version that can be found in the examples and that worked just fine. (&lt;a href="https://www.postman.com/postman/workspace/34f3a42c-18a7-4ad6-83fb-2c05767d63a7/request/1794236-b1c1129a-217b-4b47-8f7c-743dfd896dc4"&gt;JSONpath Visualizer | Postman Team Collections | Postman API Network&lt;/a&gt;)&lt;/p&gt;

&lt;p&gt;Then I thought this is a good opportunity of being a smart ass and implement it with &lt;a href="https://alpinejs.dev/"&gt;Alpine.js&lt;/a&gt;, and it was just a mess—but I found out a few things.&lt;/p&gt;

&lt;p&gt;The biggest roadblock was finding out about Postman's Content-Security-Policy that can't be changed, and it doesn't allow JavaScript in HTML attributes, something that happens in Alpine, behind the scenes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;alpinejs:5 Uncaught EvalError: Refused to evaluate a string as JavaScript because 'unsafe-eval' is not an allowed source of script in the following Content Security Policy directive: "script-src http: https: 'unsafe-inline'".
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;There is more insight on how Postman deals with CSP here: &lt;a href="https://community.postman.com/t/render-pdf-in-visualizer/9193/4"&gt;https://community.postman.com/t/render-pdf-in-visualizer/9193/4&lt;/a&gt; and in this video: &lt;a href="https://youtu.be/6ajkO9JETDs?t=275"&gt;https://youtu.be/6ajkO9JETDs?t=275&lt;/a&gt; - the short version is, Postman creates a html file in the temp directory and this files comes with CSP meta tags, that can't be overwritten, so one is stuck with it.&lt;/p&gt;

&lt;p&gt;Fortunately, Alpine.js offers a version that works in a more restrictive environment. That version is not on a CDN and I had to build it myself. This forced me to update all the Node and NPM stuff, which was a total mess,  but at the end, I got a working CSP version.&lt;/p&gt;

&lt;p&gt;Of course, it's nicer to just link files from a CDN, so I created a gist and use &lt;a href="https://raw.githack.com/"&gt;https://raw.githack.com/&lt;/a&gt; to serve the file from there.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://gist.github.com/marcus-at-localhost/dc19e9dd97743a1ef2fe039342e25b9e"&gt;The whole gist&lt;/a&gt;&lt;/p&gt;
&lt;h2&gt;
  
  
  How To:
&lt;/h2&gt;

&lt;p&gt;Paste the following code into the "Test" area of Postman and fire up a call.&lt;/p&gt;


&lt;div class="ltag_gist-liquid-tag"&gt;
  
&lt;/div&gt;



&lt;p&gt;&lt;a href="https://marcus-obst.de/blog/jsonpath-query-with-postman-vizualizer-and-alpinejs-csp-workaround"&gt;JSON Path with Alpine.js CSP build in Postman Visualizer by Marcus Obst&lt;/a&gt;&lt;/p&gt;

</description>
      <category>javascript</category>
      <category>webdev</category>
      <category>postman</category>
      <category>json</category>
    </item>
    <item>
      <title>Handle JSON API Results in Htmx</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Tue, 02 Jun 2020 07:38:42 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/handle-json-api-results-in-htmx-f46</link>
      <guid>https://dev.to/marcusatlocalhost/handle-json-api-results-in-htmx-f46</guid>
      <description>&lt;p&gt;&lt;a href="https://htmx.org/"&gt;Htmx&lt;/a&gt; is a javascript library that "allows you to access AJAX, WebSockets and Server Sent Events directly in HTML, using attributes, so you can build modern user interfaces with the simplicity and power of hypertext."&lt;/p&gt;

&lt;p&gt;&lt;a href="https://dev.to/blog/alpinejs-vuejs"&gt;In a former post I thought it was fun to compare Alpine.js to Vue.js&lt;/a&gt; and showed how similar their approaches are.&lt;/p&gt;

&lt;p&gt;The promise of these libraries is: you keep writing HTML and just add javascript behaviors directly in the DOM where you need them. This is great to enhance existing projects or hook into CMS generated output. (And you don't want to build a React app or move an existing website to one.)&lt;/p&gt;

&lt;p&gt;With the release of &lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;, the slimmer and jquery-free sibling of &lt;a href="https://intercoolerjs.org/"&gt;intercooler.js&lt;/a&gt;, I thought it would be interesting to see how htmx compares to Alpine.js or Vue.js.&lt;/p&gt;

&lt;p&gt;In short, it hardly compares - the approach is different, even if Alpine.js claims to enhance HTML by sprinkling in javascript.&lt;/p&gt;

&lt;p&gt;Htmx simplifies dealing with ajax and updating HTML fragments in the source document. You keep writing HTML and leave the ajax operations to htmx.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; 
  &lt;span class="na"&gt;hx-post=&lt;/span&gt;&lt;span class="s"&gt;"/clicked"&lt;/span&gt;
  &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"click"&lt;/span&gt;
  &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#parent-div"&lt;/span&gt;
  &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"outerHTML"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  Click Me!
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It comes with a whole set of HTTP headers so you can react on the requests on the server-side and generally, it wants you to serve rendered html back to the client and do the heavy work on the server and not in the client.&lt;/p&gt;

&lt;p&gt;I really like this approach, but there are times where you have to deal with data on the client-side, like requesting an API directly and render the results in HTML.&lt;/p&gt;

&lt;p&gt;Htmx lets you do that in a basic way, but not as elegant as Alpine.js or Vue.js. It's possible by extending htmx and use a third party template library like mustache, handlebar, or nunjucks to accomplish the goal.&lt;/p&gt;

&lt;p&gt;There is a &lt;a href="https://htmx.org/extensions/client-side-templates/"&gt;&lt;code&gt;client-side-templates&lt;/code&gt; Extension&lt;/a&gt; ready, but it's very basic and it didn't work for my special case, where I had to transform the JSON before using it.&lt;sup id="fnref1"&gt;1&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Fortunately, it's easy enough to customize the extension for my needs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Writing the HTML
&lt;/h2&gt;

&lt;p&gt;The cool thing about htmx is how you can read the attributes and understand what's going to happen:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;
&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;hx-ext=&lt;/span&gt;&lt;span class="s"&gt;"client-side-templates"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="c"&gt;&amp;lt;!-- hx-trigger="load, click" makes sure that api gets called on page load AND on click  !--&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;button&lt;/span&gt;
     &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"button"&lt;/span&gt;
     &lt;span class="na"&gt;hx-trigger=&lt;/span&gt;&lt;span class="s"&gt;"load, click"&lt;/span&gt;
     &lt;span class="na"&gt;hx-get=&lt;/span&gt;&lt;span class="s"&gt;"https://api.github.com/users/marcus-at-localhost/gists"&lt;/span&gt; 
     &lt;span class="na"&gt;nunjucks-template=&lt;/span&gt;&lt;span class="s"&gt;"gistlist"&lt;/span&gt;
     &lt;span class="na"&gt;hx-target=&lt;/span&gt;&lt;span class="s"&gt;"#list"&lt;/span&gt;
     &lt;span class="na"&gt;hx-swap=&lt;/span&gt;&lt;span class="s"&gt;"innerHTML"&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;Reload&lt;span class="nt"&gt;&amp;lt;/button&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;script &lt;/span&gt;&lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"gistlist"&lt;/span&gt; &lt;span class="na"&gt;type=&lt;/span&gt;&lt;span class="s"&gt;"nunjucks"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="nx"&gt;gist&lt;/span&gt; &lt;span class="k"&gt;in&lt;/span&gt; &lt;span class="nx"&gt;gists&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;li&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="nx"&gt;href&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{{gist.html_url}}&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;gist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;title&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/a&amp;gt;&amp;lt;br&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;        &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;small&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;{{&lt;/span&gt;&lt;span class="nx"&gt;gist&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;}}&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/small&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;      &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/li&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;    &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="nx"&gt;endfor&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/script&amp;gt;&lt;/span&gt;

  &lt;span class="nt"&gt;&amp;lt;ul&lt;/span&gt; &lt;span class="na"&gt;id=&lt;/span&gt;&lt;span class="s"&gt;"list"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Wrapped in &lt;code&gt;hx-ext="client-side-templates"&lt;/code&gt; we know this block is taken care of by an extension. &lt;/p&gt;

&lt;p&gt;The button tells us an action is triggered (&lt;code&gt;hx-trigger="load, click"&lt;/code&gt;) when we click on it, or when it appears in the DOM (on load).&lt;/p&gt;

&lt;p&gt;The action is a GET request &lt;code&gt;hx-get="https://api.github.com/users/marcus-at-localhost/gists"&lt;/code&gt; to the api.&lt;/p&gt;

&lt;p&gt;Then look for a template in nunjucks syntax &lt;code&gt;nunjucks-template="gistlist"&lt;/code&gt; and find the target HTML element in the DOM where the rendered template is going to be placed in (&lt;code&gt;hx-target="#list"&lt;/code&gt;)&lt;sup id="fnref2"&gt;2&lt;/sup&gt;&lt;/p&gt;

&lt;p&gt;Finally &lt;code&gt;hx-swap="innerHTML"&lt;/code&gt; tells us the method htmx inserts the rendered template into the DOM&lt;sup id="fnref3"&gt;3&lt;/sup&gt;. &lt;/p&gt;

&lt;p&gt;After we added the attributes to the HTML markup we have to define an extension to deal with all the JSON related stuff, like finding the client-side template fragment, manipulating the data object, and render the template.&lt;/p&gt;

&lt;p&gt;As I said, the original extension assumed the JSON comes in a format you can work with right away, but this might not be the case.&lt;/p&gt;

&lt;p&gt;So this is a minimal working case of my extension:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;defineExtension&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;client-side-templates&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;transformResponse&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;xhr&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;elt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;nunjucksTemplate&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;closest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;elt&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;[nunjucks-template]&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;nunjucksTemplate&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// manipulate the json and create my final data object.&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;gists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="c1"&gt;// parser : https://codepen.io/localhorst/pen/ZEbqVZd&lt;/span&gt;
          &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nx"&gt;leptonParser&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;description&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
          &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;};&lt;/span&gt;

      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;templateName&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;nunjucksTemplate&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;getAttribute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;nunjucks-template&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="kd"&gt;var&lt;/span&gt; &lt;span class="nx"&gt;template&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;htmx&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;templateName&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;nunjucks&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;renderString&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;template&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;innerHTML&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One limitation I've found is the restrictive access to the ajax object and results. I couldn't find a way to cache a request as it was possible in Alpine.js and Vue.js.&lt;/p&gt;

&lt;p&gt;In case you need full control, I guess you are better off dealing with it completely in javascript using the &lt;code&gt;fetch&lt;/code&gt; API, render the HTML and swap it in.&lt;/p&gt;

&lt;p&gt;Another roadblock was the additional HTTP header, htmx adds for its requests. The Github API didn't like them and returned with CORS errors.&lt;/p&gt;

&lt;p&gt;In order to remove all htmx headers (since we can't use them anywhere else than the server you have control over) we need to hook into the &lt;code&gt;configRequest.htmx&lt;/code&gt; event.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nb"&gt;document&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;addEventListener&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;configRequest.htmx&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="c1"&gt;// try to remove x-hx-* headers because gist api complains about CORS&lt;/span&gt;
    &lt;span class="nb"&gt;Object&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;keys&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;delete&lt;/span&gt; &lt;span class="nx"&gt;evt&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;detail&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;key&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
    &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And that's basically it.&lt;/p&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/localhorst/embed/OJyepKG?height=600&amp;amp;default-tab=html,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;💡 Please note, the list won't show in the codepens embedded below, because I'm using session storage and that's restricted in an &lt;code&gt;iframe&lt;/code&gt;&lt;/p&gt;

&lt;h3&gt;
  
  
  Alpine.js
&lt;/h3&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/localhorst/embed/vYNVxyX?height=600&amp;amp;default-tab=result,html&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;h3&gt;
  
  
  Vue.js
&lt;/h3&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/localhorst/embed/ExVdByo?height=600&amp;amp;default-tab=result,html&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;Published also here: &lt;a href="https://marcus-obst.de/blog/htmx-json-handling"&gt;Handle Json API Results in htmx&lt;/a&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;blockquote&gt;
&lt;p&gt;the best cut point is probably where the template plugin is doing the manipulation.  Maybe just copy the plugin and just the nunjucks part (since that's what you are using) and do the JSON transformation there? -- &lt;a href="https://gitter.im/intercooler-js/Lobby?at=5ed2addef0b8a2053ac37859"&gt;https://gitter.im/intercooler-js/Lobby?at=5ed2addef0b8a2053ac37859&lt;/a&gt; ↩&lt;/p&gt;
&lt;/blockquote&gt;
&lt;/li&gt;

&lt;li id="fn2"&gt;
&lt;p&gt;How and where you write templates depends on the template engine you are using. Nunjucks allows you to use template fragments from files. Here I just inlined the template. ↩&lt;/p&gt;
&lt;/li&gt;

&lt;li id="fn3"&gt;
&lt;p&gt;&lt;a href="https://htmx.org/attributes/hx-swap/"&gt;https://htmx.org/attributes/hx-swap/&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>htmx</category>
      <category>javascript</category>
      <category>json</category>
      <category>webdev</category>
    </item>
    <item>
      <title>AlpineJS compared to VueJS</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Sun, 17 May 2020 19:53:40 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/alpinejs-compared-to-vuejs-5014</link>
      <guid>https://dev.to/marcusatlocalhost/alpinejs-compared-to-vuejs-5014</guid>
      <description>&lt;p&gt;&lt;a href="https://github.com/alpinejs/alpine"&gt;AlpineJS&lt;/a&gt; calls itself &lt;em&gt;A rugged, minimal framework for composing JavaScript behavior in your markup&lt;/em&gt; and asks you to &lt;em&gt;think of it like Tailwind for JavaScript.&lt;/em&gt; &lt;/p&gt;

&lt;p&gt;While I have little to no idea what the first phrase means I know what to think of the second, even if I've never used Tailwind CSS so far: &lt;strong&gt;stuff a lot of data into HTML attributes&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;I'm fan of that!&lt;/strong&gt; I'm fan of &lt;a href="https://unpoly.com/"&gt;Unpoly&lt;/a&gt; and &lt;a href="https://intercoolerjs.org/"&gt;intercooler.js&lt;/a&gt;/&lt;a href="https://htmx.org/"&gt;htmx&lt;/a&gt;, and I love the approach of writing HTML and add some javascript behavior. Heck, I'm still a fan of jquery! There is nothing wrong with that.&lt;/p&gt;

&lt;p&gt;So I set out to give AlpineJS a try and while I expected to work with server-side rendered HTML fragments, I ended up consuming JSON. In this regard, AlpineJS is closer to VueJS (and it's not hiding the fact that some of the syntax is heavily borrowed from Vue) and therefore I just wrote a little thing one in AlpineJS and one in VueJS, to compare them.&lt;/p&gt;

&lt;p&gt;It has been a little bit of a challenge to understand the variable scope, since all the AlpineJS examples work with fixed JSON values and not dynamic data, fetched from an external data source. The one example that showcases &lt;code&gt;fetch&lt;/code&gt; shows it directly used in an attribute, which w a little bit too much simplified.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;div&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;alpineInstance()&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;
    &lt;span class="nx"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;fetch('https://jsonplaceholder.typicode.com/users')
        .then(response =&amp;gt; response.json())
        .then(data =&amp;gt; users = data)&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="c"&gt;&amp;lt;!--&lt;/span&gt; &lt;span class="o"&gt;--&amp;gt;&lt;/span&gt;
&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="sr"&gt;/div&lt;/span&gt;&lt;span class="err"&gt;&amp;gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;So &lt;strong&gt;how would I &lt;code&gt;fetch()&lt;/code&gt; data on &lt;code&gt;x-init&lt;/code&gt; and transform it?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;For my experiment I decided to &lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;pull a list of Github Gists,&lt;/li&gt;
&lt;li&gt;parse the description &lt;a href="http://hackjutsu.com/Lepton/"&gt;Lepton - GitHub Gist Client&lt;/a&gt; style&lt;/li&gt;
&lt;li&gt;save them into &lt;code&gt;sessionStorage&lt;/code&gt; to get around the API rate limit while testing (60 calls per hour)&lt;/li&gt;
&lt;li&gt;and just show them.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Bonus: use TailwindCSS.&lt;/p&gt;

&lt;p&gt;The barebone HTML looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight html"&gt;&lt;code&gt;&lt;span class="nt"&gt;&amp;lt;div&lt;/span&gt; &lt;span class="na"&gt;x-data=&lt;/span&gt;&lt;span class="s"&gt;"gistsData()"&lt;/span&gt; &lt;span class="na"&gt;x-init=&lt;/span&gt;&lt;span class="s"&gt;"init()"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;ul&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;template&lt;/span&gt; &lt;span class="na"&gt;x-for=&lt;/span&gt;&lt;span class="s"&gt;"gist in gists"&lt;/span&gt; &lt;span class="na"&gt;:key=&lt;/span&gt;&lt;span class="s"&gt;"gist.id"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;li&amp;gt;&lt;/span&gt;
        &lt;span class="nt"&gt;&amp;lt;a&lt;/span&gt; &lt;span class="na"&gt;x-bind:href=&lt;/span&gt;&lt;span class="s"&gt;"gist.html_url"&lt;/span&gt; &lt;span class="na"&gt;x-text=&lt;/span&gt;&lt;span class="s"&gt;"gist.parsed.title"&lt;/span&gt;&lt;span class="nt"&gt;&amp;gt;&amp;lt;/a&amp;gt;&lt;/span&gt;
      &lt;span class="nt"&gt;&amp;lt;/li&amp;gt;&lt;/span&gt;
    &lt;span class="nt"&gt;&amp;lt;/template&amp;gt;&lt;/span&gt;
  &lt;span class="nt"&gt;&amp;lt;/ul&amp;gt;&lt;/span&gt;
&lt;span class="nt"&gt;&amp;lt;/div&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;x-data&lt;/code&gt; declares the scope of the component, means all the data and methods you want to use in this component. In Vue, this is the &lt;code&gt;data&lt;/code&gt;, &lt;code&gt;methods&lt;/code&gt; and maybe &lt;code&gt;computed&lt;/code&gt; fields.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;x-init&lt;/code&gt; is a method that run on initialization or just some JSON. In this case it's a method, that fetches the data and saves the response in the &lt;code&gt;gists&lt;/code&gt; key so it's accessible in the HTML.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nx"&gt;gistsData&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Latest Gists&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;gists&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[],&lt;/span&gt;
    &lt;span class="nx"&gt;init&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="c1"&gt;// Testdata&lt;/span&gt;
      &lt;span class="cm"&gt;/*
        this.gists = [
          {
          "id": "8f6af49ffe693c15faca67a7f3bf1a31",
          "html_url": "https://gist.github.com/8f6af49ffe693c15faca67a7f3bf1a31",
          "description": "[Lepton Title Style] Some description and #hash #tags"
          }
        ];
        return;
      */&lt;/span&gt;

      &lt;span class="c1"&gt;// get gists&lt;/span&gt;
      &lt;span class="nx"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;https://api.github.com/users/marcus-at-localhost/gists&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;then&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
          &lt;span class="k"&gt;this&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;gists&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;gists&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="p"&gt;});&lt;/span&gt;
    &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="p"&gt;};&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So this is the most basic example how to structure your code.&lt;/p&gt;

&lt;p&gt;Check out the two codepens and compare them.&lt;/p&gt;

&lt;h2&gt;
  
  
  AlpineJS
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/localhorst/embed/vYNVxyX?height=600&amp;amp;default-tab=js,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;💡 If you want to debug &lt;code&gt;console.log(this.gists)&lt;/code&gt; it's pretty noisy. Checkout the &lt;a href="https://github.com/Te7a-Houdini/alpinejs-devtools"&gt;AlpineJS Devtools&lt;/a&gt; extension, that is similar to the Vue Devtools.&lt;/p&gt;

&lt;h2&gt;
  
  
  VueJS
&lt;/h2&gt;

&lt;p&gt;&lt;iframe height="600" src="https://codepen.io/localhorst/embed/ExVdByo?height=600&amp;amp;default-tab=js,result&amp;amp;embed-version=2"&gt;
&lt;/iframe&gt;
&lt;/p&gt;

&lt;p&gt;And Tailwind?  In its most basic form, it's the Atomic CSS approach, similar to Bootstrap, down to some of the same class names, like &lt;code&gt;mt-5&lt;/code&gt; for &lt;code&gt;margin-top: x;&lt;/code&gt;.  Just add classes as if you would write inline css.&lt;/p&gt;

&lt;p&gt;You can also &lt;a href="https://tailwindcss.com/docs/functions-and-directives/#apply"&gt;"compose" custom classes&lt;/a&gt; from those micro classes, almost as if you were writing CSS. :-o&lt;/p&gt;

&lt;p&gt;I can see that this is fun to use, but it's not superior to other frameworks. Use whatever works for you.&lt;/p&gt;

&lt;p&gt;Also on my &lt;a href="https://marcus-obst.de/blog/alpinejs-vuejs"&gt;blog&lt;/a&gt;&lt;/p&gt;

</description>
      <category>alpine</category>
      <category>vue</category>
      <category>javascript</category>
    </item>
    <item>
      <title>Request Google Sheets JSON API v4 with PHP</title>
      <dc:creator>Marcus</dc:creator>
      <pubDate>Mon, 11 May 2020 20:01:04 +0000</pubDate>
      <link>https://dev.to/marcusatlocalhost/request-google-sheets-json-api-v4-with-php-12ji</link>
      <guid>https://dev.to/marcusatlocalhost/request-google-sheets-json-api-v4-with-php-12ji</guid>
      <description>&lt;p&gt;Via the Google API, it's really easy to &lt;code&gt;GET&lt;/code&gt; your spreadsheet as JSON and then work with it in whichever way you like.&lt;/p&gt;

&lt;p&gt;Since the API v3 is going to go away in September 2020 it renders many tutorials useless, so here is how you do it with API v4&lt;sup id="fnref1"&gt;1&lt;/sup&gt;.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Set up a Google Form, a spreadsheet will be created automatically or you can point to an existing spreadsheet. (&lt;a href="https://support.google.com/docs/answer/2917686?hl=en"&gt;Help&lt;/a&gt;)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--B--V35Vg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/w8egkn3pyrnrrr5o1w61.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--B--V35Vg--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/w8egkn3pyrnrrr5o1w61.png" alt="Forms to Sheet" width="789" height="238"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;Add entries and check if they appear in your spreadsheet&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;a href="https://console.developers.google.com/apis/credentials"&gt;Acquire an API key&lt;/a&gt; to &lt;a href="https://developers.google.com/sheets/api/guides/authorizing#APIKey"&gt;authorize&lt;/a&gt; the access to the API.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Share the spreadsheet and save the &lt;code&gt;spreadsheetId&lt;/code&gt; and the &lt;code&gt;sheetName&lt;/code&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://res.cloudinary.com/practicaldev/image/fetch/s--9z_e-vLd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ddqed1qnmfs30f6kqxzo.png" class="article-body-image-wrapper"&gt;&lt;img src="https://res.cloudinary.com/practicaldev/image/fetch/s--9z_e-vLd--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_800/https://dev-to-uploads.s3.amazonaws.com/i/ddqed1qnmfs30f6kqxzo.png" alt="Share Sheet to Public" width="800" height="365"&gt;&lt;/a&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://sheets.googleapis.com/v4/spreadsheets/{spreadsheetId}/values/{sheetName}
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more in-depth information about how to query the spreadsheet, &lt;a href="https://developers.google.com/sheets/api/reference/rest/v4/spreadsheets.values/get"&gt;check out the docs&lt;/a&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight php"&gt;&lt;code&gt;&lt;span class="nb"&gt;defined&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'API_KEY'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="s1"&gt;'XXX'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="nv"&gt;$url&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;sprintf&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s1"&gt;'https://sheets.googleapis.com/v4/spreadsheets/1TlhxvW4GxayktKdjoWKt620qTzysEquC4UPGmOlGxb0/values/Formularantworten%203?key=%s'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="no"&gt;API_KEY&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="nv"&gt;$json&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;json_decode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;file_get_contents&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$url&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nv"&gt;$json&lt;/span&gt;&lt;span class="o"&gt;-&amp;gt;&lt;/span&gt;&lt;span class="n"&gt;values&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;foreach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$rows&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nb"&gt;var_dump&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;$row&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Result&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="err"&gt;//&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="err"&gt;https://sheets.googleapis.com/v&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;/spreadsheets/&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="err"&gt;TlhxvW&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;GxayktKdjoWKt&lt;/span&gt;&lt;span class="mi"&gt;620&lt;/span&gt;&lt;span class="err"&gt;qTzysEquC&lt;/span&gt;&lt;span class="mi"&gt;4&lt;/span&gt;&lt;span class="err"&gt;UPGmOlGxb&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="err"&gt;/values/Formularantworten%&lt;/span&gt;&lt;span class="mi"&gt;201&lt;/span&gt;&lt;span class="err"&gt;?key=xxxx&lt;/span&gt;&lt;span class="w"&gt;

&lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"range"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"'Formularantworten 1'!A1:M104"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"majorDimension"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ROWS"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"values"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Zeitstempel"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"What's up"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Short Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Long Text"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Matrix [Row 1]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Matrix [Row 2]"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Matrix [Row 3]"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"01.05.2020 18:06:54"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Nothing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Diana"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"My Answer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 3"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"01.05.2020 18:07:17"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"A Lot"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Marc"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"It's raining"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 2"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 1"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 3"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"01.05.2020 18:07:39"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Nothing"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Maria"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Still raining"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 2, Col 3"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;""&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="s2"&gt;"Col 1"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For more complex tasks, you might want to &lt;a href="https://developers.google.com/sheets/api/guides/concepts"&gt;check out google's api wrapper libraries&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;&lt;small&gt;This appeared first on my blog: &lt;a href="https://marcus-obst.de/blog/request-google-sheets-json-api-v4-with-php"&gt;Request Google Sheets JSON API v4 with PHP&lt;/a&gt;&lt;/small&gt;&lt;/p&gt;




&lt;ol&gt;

&lt;li id="fn1"&gt;
&lt;p&gt;It's much easier to work with v4! JSON result of v3 was a mess. &lt;a href="https://developers.google.com/sheets/api/guides/migration#retrieve_row_data"&gt;Migrate to v4&lt;/a&gt; ↩&lt;/p&gt;
&lt;/li&gt;

&lt;/ol&gt;

</description>
      <category>php</category>
      <category>webdev</category>
    </item>
  </channel>
</rss>
