Dynamically add widgets to Laravel Filament pages

I recently completed a project at Vormkracht10 where we developed an intranet application for a customer. This application is built entirely with Filament and while developing this project I created some useful things.

An example of this is the dynamic loading of widgets where the width and order of widgets can be configured. I haven't found an explanation of this on the internet yet, so I thought it would be nice to tell you more about it.

Short explanation

In Filament you can add widgets to pages by adding them to your Page class. You can determine the width and order of the widgets by setting the sort and columnSpan properties on the Widget class and setting the column width of the page in the same Page class.

That works well, but what if you let the user create pages themselves and add widgets? Then you have to ensure that this can be done dynamically and fortunately that is also a piece of cake when you use Filament!

  1. The widget class setup

    As I mentioned earlier you need to set the sort and columnSpan properties. The sort property is inherited from the parent Widget class. So you don't have to add them yourself. The columnSpan is a protected property and you set it with the following method: getColumnSpan. Because we want a value here that we can read and put in this function, I added a width property as follows:

public ?string $width = null;

Now you may be wondering how we can give those properties the correct values from the Page class. This can be done by calling the make method, which you also inherited from the parent Widget class:

/**
 * @param  array<string, mixed>  $properties
 */
public static function make(array $properties = []): WidgetConfiguration
{
    return app(WidgetConfiguration::class, ['widget' => static::class, 'properties' => $properties]);
}

As you can see, you can use this function to add properties to your widget class. I'll show you how to use it later.

  1. Load the widgets dynamically into your page(s)

I have a Page and Widget table in my database. These are linked together through a many-to-many relationship. In the linking table I have added some extra fields such as 'width' and 'sort' so that we can apply these when loading.

I will add the widgets to the relevant page, I add widgets in the following way:

Define the amount of columns

I have defined the number of columns that the page can contain in both the header and footer as follows in the Page class:

 public function getHeaderWidgetsColumns(): int|string|array
 {
     return 12;
 }

 public function getFooterWidgetsColumns(): int|string|array
 {
     return 12;
 }

This is the maximum number of columns and I chose this so that I give the user as much space as possible to determine how wide a widget can be. By default, the number of columns is 2. I think that is too few.

  1. Load the widgets into your page

You can add widgets to your page in filament with two functions:

  • getHeaderWidgets(): for adding widgets to the top of the page.
  • getFooterWidgets(); for adding widgets to the bottom of the page.

In addition to saving the width and order of the widgets, I also saved the position in a column ('position'). I use this to determine where the widget should be loaded on the page.

See the function below where I retrieve the widgets per page, filtered by position:

I then go through the collection of widgets and create an instance of the widget class itself. I can do this because the widget in my database contains the relative path of the class.

After loading the class, I call the 'make' method to which I give the 'width' value. In the 'getColumnSpan' method I use the 'width', which can have a number from 1 to 12 (or your set column width).

protected function getHeaderWidgets(): array
{
    return $this->page->widgets->filter(function ($widget) {
        // notice here that ‘top’ is my default widget location
        return $widget->pivot->position === 'top' || $widget->pivot->position === null; 
    })->map(function ($widget) {
        return $widget->class::make([
            'width' => $widget->pivot->width,
        ]);
    })->sortBy(fn ($widget) => $widget->getProperties()['sort'])->filter()->toArray();
}

protected function getFooterWidgets(): array
{
    return $this->page->widgets->where('pivot.position', 'footer')->map(function ($widget) {
        return $widget->class::make([
            'width' => $widget->pivot->width,
        ]);
    })->sortBy(fn ($widget) => $widget->getProperties()['sort'])->filter()->toArray();
}

Normally you would use the sort attribute to determine the order of the widgets. Because this must be dynamic, we sort the widgets ourselves in our desired order, which is why I sort the collection on the 'sort' column.

Conclusion

To show you how I did it and what you can do in Filament with dynamic widgets, I have put a repository online where you can use the code. In my next blog I will tell you more about how to make the widgets further configurable.