About Sharp for Laravel — Part 4.
Let’s build a form list (finally)
Form lists are what they seem to be: a way of handling a “hasMany” relationship between entities. We already built the opposite, with the autocomplete field to link a Player to his Team, in Part 2, and even if we could use this to add a Players list in a Team, we won’t, since it would be strange to handle both side (altough we could, and maybe even should, add some read-only players list in the Team form, to show this relation here).
Anyway, for this part I’ve decided to add a new data in the system: we now want to track Titles won by teams. We’ll do it the easiest possible way, to keep it short: let’s start with the new titles table:
Schema::create('titles', function (Blueprint $table) {
$table->increments('id');
$table->string("competition");
$table->string("title");
$table->unsignedInteger("team_id");
$table->timestamps();
});
And the Model:
class Title extends Model
{
protected $guarded = [];
public function team()
{
return $this->belongsTo(Team::class);
}
}
And finally the relation in the Team Model:
class Team extends Model
{
[...]
public function titles()
{
return $this->hasMany(Title::class);
}
}
Ok, we’re all set. Let’s tackle the Sharp side now, in the TeamSharpForm class:
class TeamSharpForm extends SharpForm
{
[...]
function buildFormFields()
{
$this->addField(
SharpFormTextField::make("name")
->setLabel("Name")
)->addField(
SharpFormTextField::make("country")
->setLabel("Country")
)->addField(
SharpFormListField::make("titles")
->setLabel("Titles")
->setAddable()->setAddText("Add a title")
->setRemovable()
->addItemField(
SharpFormTextField::make("competition")
->setLabel("Competition name")
)->addItemField(
SharpFormTextField::make("title")
->setLabel("Title")
)
);
} [...]
}
The new code, in bold, builds the list field defining its behavior (we can add and remove items) and the template of each item: as you see, an item can be composed with all form fields available in Sharp (well, except list, because Sharp won’t allow lists in list). So we define here that a Title in a Team is made of two text fields: competition and title.
Like we’ve seen before in this series, we have now to add this new list field in the form layout:
class TeamSharpForm extends SharpForm
{
[...]
function buildFormLayout()
{
$this->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField("name")
->withSingleField("country"); })->addColumn(6, function(FormLayoutColumn $column) {
$column->withSingleField(
"titles",
function(FormLayoutColumn $item) {
$item->withFields("competition|7", "title|5");
}
);
});
} [...]
}
We choose to display the list in a new form column. Then, notice that we build here the list item layout also, in a closure—because of course the item can have any layout we want. As we’ve done for Players, we place competition and title fields on the same line, indicating field size after a pipe separator on a 12-column grid:
Here’s the best part: because we are using Sharp’s WithSharpFormEloquentUpdater trait, and Eloquent of course, we’re almost done here, the update is working, and we can add and remove titles. Well, I said almost, because if you proceed and clic the Update button and then re-display the form, you’ll be disapointed: despite the fact there was no error, the titles list is empty. This is an easy-to-fall into one, we have to load the titles relation of our Team Model on the form, and that’s easy with Eloquent:
class TeamSharpForm extends SharpForm
{
[...]
function find($id): array
{
return $this->transform(
Team::with('titles')->findOrFail($id)
);
} [...]
}
And now, everyting is working. From here, we can do several things, like display this titles list in the Team EntityList, but first let’s update our Validator to handle the list—or let’s in fact create the Validator for the Team, since we skip this step:
class TeamSharpValidator extends FormRequest
{
public function authorize()
{
return true;
}
public function rules()
{
return [
"name" => "required",
"country" => "required",
"titles.*.competition" => "required"
];
}
}
We are leveraging Laravel’s useful array validator here to ensure that we provide at least a competition name for each title. Like reviewed before, we have to declare this validator in sharp’s config:
in config/sharp.phpreturn [
[...]
"entities" => [
"players" => [
[...]
],
"teams" => [
"list" => \App\Sharp\TeamSharpList::class,
"form" => \App\Sharp\TeamSharpForm::class,
"validator" => \App\Sharp\TeamSharpValidator::class,
],
], [...]
];
And to close this part 4, let’s talk about something we sometimes put aside: how can we let the user choose the order of items in this list of titles? Well, quite easily in fact.
Step one, write the migration to add a new order column to titles:
class AddOrderToTitles extends Migration
{
public function up()
{
Schema::table('titles', function (Blueprint $table) {
$table->unsignedSmallInteger("order")->default(10);
});
}
}
Step two, declare this order column in the Team side of the relation:
class Team extends Model
{
[...]
public function titles()
{
return $this->hasMany(Title::class)
->orderBy("order");
}
}
Step three, declare this in the Team Sharp Form:
class TeamSharpForm extends SharpForm
{
[...]
function buildFormFields()
{
$this->addField(
[...] )->addField(
SharpFormListField::make("titles")
->setLabel("Titles")
->setAddable()->setAddText("Add a title")
->setSortable()->setOrderAttribute("order")
->setRemovable()
[...]
);
} [...]
}
Step four… forget about step four, we’re done. Here’s the result:
When the list contains two items at least, a “sort” button appears. Clicking it switch the list in a read-only drag and drop mode:
This drag state could in fact be personalized, as documented here, with a Vue template, useful when items are huge and need to be collapsed just for reordering.
And because we provide the name of the order attribute in the list declaration, we don’t even have to write any update code, Sharp will do the trick (of course, as always, you can easily hook on this for complex cases).
Ok, we’re done for this part, for which full code is available here in Github. Next one will be about Commands (EDIT: and it’s now live).