How to use the Nested Components with Alpine.js v2
Chapters Close

How to use the Nested Components with Alpine.js v2

In this article, I am explaining how to use Nested Components with Alpine.js v2.

Alpine.js version support with different Hyva theme versions:

  • The Hyvä theme 1.1.x uses Alpine.js version 2.
  • The Hyvä theme 1.2.x uses Alpine.js version 3.

What is the issue with Nested Components with Alpine.js v2?

Sometimes, when implementing business logic that requires the use of Alpine.js components nested inside another component, the inner component needs access to the state of the outer component.

In Alpine.js v3, nested components already allow access to the parent scope, but in v2, the outer scope is not accessible.

Let’s explain this with one example:

Assume you have defined the ‘foo’ property through the x-data attribute in the main component. You want to access this ‘foo’ property inside its child component in Alpine.js v2. However, this is not allowed, and an error is displayed in the console stating ‘foo is not defined.’ Put the code below in your Hyva theme with Alpine.js v2 & Alpine.js v3 on any page and check the output.

<div x-data="{ foo: 'bar' }">
  <span x-text="foo"><!-- Will output: "bar" --></span>
  <div x-data="{ bar: 'baz' }">
      <span x-text="foo"><!-- Will output: "bar" --></span>
      <div x-data="{ foo: 'bob' }">
          <span x-text="foo"><!-- Will output: "bob" --></span>
      </div>
  </div>
</div>

Output in Alpine.js v2:

Output in Alpinejs v2

Output In Alpine.js v3:

With Alpine.js v3, this works as expected, and there’s no need for any additional adjustments.

Output In Alpinejs v3

So, how can we work around this limitation? Don’t worry; please check the different approaches below to overcome this constraint.

  • Storing shared state on a parent element data attribute
  • Sharing state with events
  • Sharing state through a global variable
  • Calling parent scope methods

Let’s go through each of these approaches one by one.

New to Hyvä?? Learn about the Hyvä Theme from basics!

Storing shared state on a parent element data attribute

This approach utilizes the DOM to store shared data. It is likely the simplest solution, and if it fits your use case, we recommend following this pattern.

Please check the below example:

<div x-data="{ foo: 'bar' }">
  <span x-text="foo"><!-- Will output: "bar" --></span>

  <div :data-foo="foo">
      <div x-data="{ bar: 'baz', parentFoo: null}" x-init="parentFoo = $el.parentElement.dataset.foo">
          <span x-text="parentFoo"><!-- Will output: "bar" --></span>

          <div x-data="{ foo: 'bob' }">
              <span x-text="foo"><!-- Will output: "bob" --></span>
          </div>
      </div>
  </div>
</div>

In this example, I declare the parent component with x-data=”{ foo: ‘bar’ }”. Next, I set the value of the ‘foo’ property in the div data attribute using :data-foo=”foo”.

Subsequently, I pass this value to the child component using x-init=”parentFoo = $el.parentElement.dataset.foo”. This allows me to access the parent component’s value in the child component using the parentFoo property.

Output

parent component's value in the child component

Examples with iterate over some objects:

<script>
  'use strict';
  function initDatasetExample() {
      return {
          items: [
              {sku: 'Item A', price: 10, isSalable: true},
              {sku: 'Item B', price: 7, isSalable: true},
              {sku: 'Item C', price: 5, isSalable: false},
          ]
      };
  }
</script>
<div x-data="initDatasetExample()">
  <h2>Nested Components with dataset</h2>
  <template x-for="item in items">
      <div :data-item="JSON.stringify(item)">
          <div x-data="{open: false, item: null}"
               x-init="item = JSON.parse($el.parentElement.dataset.item)"
               class="mb-1">
              <button type="button" class="btn mb-1" @click="open = !open" x-text="`Show ${item.sku}`"></button>
              <template x-if="open">
                  <table class="my-4">
                      <tr>
                          <th class="text-left">SKU</th>
                          <td x-text="item.sku"></td>
                      </tr>
                      <tr>
                          <th class="text-left">Price</th>
                          <td x-text="hyva.formatPrice(item.price)"></td>
                      </tr>
                      <tr>
                          <th class="text-left">Salable?</th>
                          <td x-text="item.isSalable ? 'In Stock' : 'Out of Stock'"></td>
                      </tr>
                  </table>
              </template>
          </div>
      </div>
  </template>
</div>

Output:

iterate over some objects

Sharing state with events

This approach employs an events-based mechanism to assign consecutive values to nested components. Please refer to the example below.

<script>
  'use strict';

  function initEventsExample() {
      return {
          items: [
              {sku: 'Item D', price: 10, isSalable: true},
              {sku: 'Item E', price: 7, isSalable: true},
              {sku: 'Item F', price: 5, isSalable: false},
          ]
      };
  }
</script>
<div x-data="initEventsExample()">
  <h2>Nested Components with events</h2>
  <div class="prose">This solution uses the single threaded nature of JavaScript to assign consecutive values</div>
  <template x-for="item in items">
      <div>
          <div x-data="{open: false, item: {}, receiveItem($event) {
              if (! this.item.sku) {
                  this.item = $event.detail.item;
                  $event.stopPropagation();
              }
          }}"
               @next-item.window="receiveItem($event)">
              <button type="button" class="btn mb-1" @click="open = !open" x-text="`Show ${item.sku}`"></button>
              <template x-if="item && open">
                  <table class="my-4">
                      <tr>
                          <th class="text-left">SKU</th>
                          <td x-text="item.sku"></td>
                      </tr>
                      <tr>
                          <th class="text-left">Price</th>
                          <td x-text="hyva.formatPrice(item.price)"></td>
                      </tr>
                      <tr>
                          <th class="text-left">Salable?</th>
                          <td x-text="item.isSalable ? 'In Stock' : 'Out of Stock'"></td>
                      </tr>
                  </table>
              </template>
          </div>
          <div x-text="$dispatch('next-item', {item: item})"></div>
      </div>
  </template>
</div>

Output:

Nested Components with events

Sharing state through a global variable

This approach utilizes a global state object and an array-based ordering of items to access the relevant records. Please check the example of this approach.

It can also work well in cases where the shared state is modified and needs to still be accessible by both the parent and the child component.

<script>
  'use strict';
  function initGlobalExample() {
      if (! window.globalSharedStateExample) {
          window.globalSharedStateExample = {};
      }
      window.globalSharedStateExample.items = [
          {sku: 'Item G', price: 10, isSalable: true},
          {sku: 'Item H', price: 7, isSalable: true},
          {sku: 'Item I', price: 5, isSalable: false},
      ];
      return {
          items: window.globalSharedStateExample.items
      };
  }
</script>
<div x-data="initGlobalExample()">
  <h2>Nested Components with global state</h2>
  <template x-for="item in items">
      <div x-data="{open: false, item: {}}"
          <?php // previous siblings: <h2> and <template>. We subtract 2 to get array index for current item ?>
           x-init="item = window.globalSharedStateExample.items[Array.from($el.parentElement.children).indexOf($el) -2]">
          <button type="button" class="btn mb-1" @click="open = !open" x-text="`Show ${item.sku}`"></button>
          <template x-if="open">
              <table class="my-4">
                  <tr>
                      <th class="text-left">SKU</th>
                      <td x-text="item.sku"></td>
                  </tr>
                  <tr>
                      <th class="text-left">Price</th>
                      <td x-text="hyva.formatPrice(item.price)"></td>
                  </tr>
                  <tr>
                      <th class="text-left">Salable?</th>
                      <td x-text="item.isSalable ? 'In Stock' : 'Out of Stock'"></td>
                  </tr>
              </table>
          </template>
      </div>
  </template>
</div>

Output:

Nested Components with global state

Calling parent scope methods

This can be easily achieved using events. Please refer to the example below.

<script>
  'use strict';

  function initMethodCallExample() {
      return {
          counter: 0,
          count() {
              this.counter++;
          },
          items: [
              {sku: 'Item J', price: 10, isSalable: true},
              {sku: 'Item K', price: 7, isSalable: true},
              {sku: 'Item L', price: 5, isSalable: false},
          ]
      };
  }
</script>
<div x-data="initMethodCallExample()" @count="count()">
  <h2>Calling parent component methods</h2>
  <div class="prose">This solution uses custom events to trigger parent component methods</div>
  <span class="btn w-32" x-text="'counter:' + counter"></span>
  <template x-for="item in items">
      <div>
          <table class="my-4">
              <tr>
                  <th class="text-left">SKU</th>
                  <td x-text="item.sku"></td>
              </tr>
              <tr>
                  <th class="text-left">Price</th>
                  <td x-text="hyva.formatPrice(item.price)"></td>
              </tr>
              <tr>
                  <th class="text-left">Salable?</th>
                  <td x-text="item.isSalable ? 'In Stock' : 'Out of Stock'"></td>
              </tr>
          </table>
      </div>
  </template>
  <div x-data="{title: 'This is child component'}">
      <h2 x-text="title"></h2>
      <button type="button" class="btn" @click="$dispatch('count')">Count from nested component</button>
  </div>
</div>

Output:

calling parent component methods

With the above approach, you can access the properties and call the functions of the parent component in Alpine.js v2 from the child component.

More resources on Hyva themes:

Speak your Mind

Post a Comment

Got a question? Have a feedback? Please feel free to leave your ideas, opinions, and questions in the comments section of our post! ❤️

* This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.

Grow your online business like 2,508 subscribers

    * This site is protected by reCAPTCHA and the Google Privacy Policy and Terms of Service apply.
    envelope

    Thank You!

    We are reviewing your submission, and will be in touch shortly.