Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[8.x] Pass component children to Blade components #36472

Closed
wants to merge 1 commit into from

Conversation

mpociot
Copy link
Contributor

@mpociot mpociot commented Mar 4, 2021

This PR allows Blade components to access their own child elements. This can be extremely useful if you want to build more complex components, or if you want to use components as structured meta-data.
The child components will be accessible in a __children variable within your Blade components.

For example, you could create a table component, that accepts table-columns components, which will be used to determine the columns that should be rendered:

<x-table :data="[['id' => 1, 'name' => 'Marcel'], ['id' => 2, 'name' => 'Adam']]">
    <x-table-column name="ID" attribute="id" />
    <x-table-column name="Name" attribute="name" />
</x-table>

The (simplified) components could look like this:

{{-- table.blade.php --}}
<table>
    <thead>
        <tr>
            @foreach($__children as $tableColumn)
                <th>{{ $tableColumn->data()['name'] }}</th>
            @endforeach
        </tr>
    </thead>
    <tbody>
    @foreach($data as $row)
        <tr>
            @foreach($__children as $tableColumn)
                <td>{{ $row[$tableColumn->data()['attribute']] }}</td>
            @endforeach
        </tr>
    @endforeach
    </tbody>
</table>
{{-- table-column.blade.php --}}
@props([
    'name' => '',
    'attribute' => ''
]);

The table-column component does not require any actual view content, as it is only used to hold the meta information.

In order to pass the actual child component instances to the view, this new feature only works with class-based components (either AnonymousComponent or a custom component class). This means, that it won't work when using the @component syntax, which is no longer documented anyway.

I'm having a hard time figuring out whats the best way to properly test this, that's why this PR does not include automated tests yet.

@inxilpro
Copy link
Contributor

inxilpro commented Mar 5, 2021

I've written some very ugly code to make this happen. Framework support would be amazing!

I would like to see an API for accessing children from the parent component class. I originally was using a pattern like this for blade-alpine-instantsearch and in that case I really just wanted to access child components for configuration purposes (I eventually moved all the parent/child logic into Alpine.js for various reasons).

Another interesting thing would be the ability to access the parent from the child, which actually might be a more flexible solution:

class Table extends Component
{
  public array $columns = [];

  public function addColumn($name, $attribute) {
  {
    $this->columns[$attribute] = $name;
  }

  // ...
}

class Column extends Component
{
  public function __construct($name, $attribute)
  {
    $this->parent(Table::class)->addColumn($name, $attribute);
  }

  public function render()
  {
    return '';
  }
}

Sorry, I don't mean to muddy the water! I've definitely been wanting parent/child interaction in Blade components, so I'm just excited to imagine the possibilities!

@taylorotwell
Copy link
Member

@mpociot your example is actually already possible like so:

<x-table :data="[['id' => 1, 'name' => 'Marcel'], ['id' => 2, 'name' => 'Adam']]">
    {{ $component->addColumn('ID', 'id') }}
    {{ $component->addColumn('Name', 'name') }}
</x-table>

And the following component:

<?php

namespace App\View\Components;

use Illuminate\View\Component;

class Table extends Component
{
    public $columns = [];
    public $data = [];

    /**
     * Create a new component instance.
     *
     * @return void
     */
    public function __construct(array $data)
    {
        $this->data = $data;
    }

    /**
     * Add a column to the table.
     *
     * @param  string  $name
     * @param  array  $attributes
     */
    public function addColumn($name, $attribute)
    {
        $this->columns[] = compact('name', 'attribute');
    }

    /**
     * Get the view / contents that represent the component.
     *
     * @return \Illuminate\Contracts\View\View|string
     */
    public function render()
    {
        return fn () => view('components.table', [
            'columns' => $this->columns,
            'data' => $this->data
        ])->render();
    }
}

@inxilpro
Copy link
Contributor

inxilpro commented Mar 5, 2021

I think the ergonomics of being able to use the XML-style syntax is really really nice. @mpociot's original example with <x-table-column> feels great, and while using $component works, it doesn't map as nicely to how people work with HTML.

As an alternative approach, would you be open to an API that exposes ManagesComponents::$componentStack instead? That way a child component could access the stack to find its parent. This would mean that the CompilesComponent implementation wouldn't need to be altered…

@taylorotwell
Copy link
Member

taylorotwell commented Mar 5, 2021

@inxilpro yeah - i guess it's just preference at that point. Of course, if something is already possible using relatively simple syntax I would always lean towards not taking on any new code to maintain.

I actually find both approaches a little odd... I would have just passed the columns as a prop like the data.

@inxilpro
Copy link
Contributor

inxilpro commented Mar 6, 2021

Here's my use-case:

Algolia provides some really nice javascript APIs for searching data indexed with Laravel Scout. They have to be configured with JS because of the nature of instant search, but also interact with multiple layers of DOM nodes.

Imagine something like this:

<x-instantsearch :app-id="$algoliaId" :api-key="$algoliaKey" search-index="tee-shirts">
  <x-row>
    <x-column width="1/3">
      <x-instantsearch::refinement-list attribute="size" />
      <x-instantsearch::refinement-list attribute="color" />
    </x-column>
    <x-column width="2/3">
      <x-instantsearch::search-box />
      <x-instantsearch::hits />
    </x-column>
  </x-row>
  <x-instantsearch::pagination />
</x-instantsearch>

In this case, the final instant search configuration script needs to be set up just once in the parent element, but it needs to know about its children. At the same time, the child nodes need to add HTML to the output.

The resulting output should look something like:

<div id="instantsearch-container">
  <div class="flex">
    <div class="w-1/3">
      <div id="instantsearch-refinements-size"></div>
      <div id="instantsearch-refinements-color"></div>
    </div>
    <div class="w-2/3">
      <div id="instantsearch-search-box"></div>
      <div id="instantsearch-hits"></div>
    </div>
  </div>
  <div id="instantsearch-pagination"></div>
</div>

<script>
const search = instantsearch({
  indexName: 'tee-shirts',
  searchClient: algoliasearch(
    'appid',
    'apikey'
  ),
});
search.addWidgets([
  instantsearch.widgets.refinementList({ container: '#instantsearch-refinements-size' }),
  instantsearch.widgets.refinementList({ container: '#instantsearch-refinements-color' }),
  instantsearch.widgets.searchBox({ container: '#instantsearch-search-box' }),
  instantsearch.widgets.hits({ container: '#instantsearch-hits' }),
  instantsearch.widgets.pagination({ container: '#instantsearch-pagination' }),
]);
</script>

We need to be able to centralize the search.addWidgets call to one single place, while also being able to place blade components inside the structure of the view. The child components need to both operate as regular components and also pass configuration up to the appropriate parent (which isn't necessarily the immediate parent).

To achieve this, we'd need either:

  1. To have access to the children from the parent, or
  2. To have access to the parents from the children

I think option 2 introduces a little less complexity, because I think it can be achieved by exposing a
View::getComponentStack() method and very little else. But I think both directions are fine.

Does that help demonstrate the value any more?

@taylorotwell
Copy link
Member

@inxilpro does this PR as it is written right now support that exact syntax and use case?

@inxilpro
Copy link
Contributor

inxilpro commented Mar 8, 2021

I'm actually not sure that it does.

I didn't want to open a PR if you don't see a need, but I can try to write up some tests and a PR tonight or tomorrow.

@inxilpro
Copy link
Contributor

inxilpro commented Mar 8, 2021

Alright. Take a look at #36512 when you get a change. @mpociot — I'd love your thoughts on this approach, as well.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants