Split Nova Display, Index, and Form Fields Into Different Functions

Laravel Nova - Quick Tips

August 06 2019

The Plan

If you've got a large or complicated model resource in Laravel Nova, it’s easy to get quickly overwhelmed by the fields function. You will often end up with multiple copies of a field. When I first started using Nova, I did not want the layout of the form fields to dictate the list-view, and vice versa.

Instead, what I want to do was break up the fields function into several different ones. I’d still like to be able to use the fields function exactly as it is by default, with an added benefit of being able to split your fields into any number of functions.

Implementation

I override the fields function in my base /app/Nova/Resource.php class, and added a few presets

  • Index Fields
  • Form Fields
  • Display Fields
  • Create Fields
  • Update Fields
  • System Fields
  • Default Fields

Index Fields

Index fields are fields which are available on the model index page as well as inside of relationships

Form Fields

Default value for Create & Update if those are not specified

Display Fields

Fields to display when viewing a record

Create Fields

The fields to show on the create model page, defaults to Form Fields if nothing is provided

Update Fields

The fields to show on the update model page, defaults to Form Fields if nothing is provided

System Fields

Often times there are system-specific fields, such as an ID, a create date, update date, or other system-wide fields we might want to display. Adding this as a dedicated function allows which fields we want to show globally, or system fields which are specific to a model

Default Fields

You might have common elements where you don’t want any special rules applied.

In the base resource, we make our fields array equal to the sum of all these different functions. Each function will apply their respective visibility rules (such as onlyOnDetail and onlyOnIndex).

The Code

Here are the extra fields in app/Nova/Resource.php

<?php

namespace App\Nova;

use Illuminate\Http\Request;
use Laravel\Nova\Fields\Heading;
use Laravel\Nova\Fields\ID;
use Laravel\Nova\Resource as NovaResource;
use Laravel\Nova\Http\Requests\NovaRequest;

abstract class Resource extends NovaResource
{
    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fields(Request $request)
    {
        return $this->processAllFields($request);
    }

    /**
     * Get system fields for this resource
     *
     * @param Request $request
     * @return array
     */
    public function fieldsSystem(Request $request) {
        return [
            Heading::make('System Information')->onlyOnDetail(),
            ID::make()->onlyOnDetail(),
        ];
    }

    /**
     * Manually controlled fields for this resource
     *
     * @param Request $request
     * @return array
     */
    public function defaultFields(Request $request) {
        return [];
    }

    /**
     * Fields which are available on the create and edit form by default
     * This can be overwrote by using fieldsCreate and fieldsUpdate if you want
     * to control specific forms
     *
     * @param Request $request
     * @return array
     */
    public function fieldsForm(Request $request) {
        return [];
    }

    /**
     * Fields available on the default index view
     *
     * @param Request $request
     * @return array
     */
    public function fieldsIndex(Request $request) {
        return [];
    }

    /**
     * Fields to display when viewing the resource
     *
     * @param Request $request
     * @return array
     */
    public function fieldsDisplay(Request $request) {
        return [];
    }

    /**
     * Fields which are available on the create page for this resource
     * Defaults to fieldsForm
     *
     * @param Request $request
     * @return array
     */
    public function fieldsCreate(Request $request) {
        return $this->fieldsForm($request);
    }

    /**
     * Fields which are available on the update page for this resource
     * Defaults to fieldsForm
     *
     * @param Request $request
     * @return array
     */
    public function fieldsUpdate(Request $request) {
        return $this->fieldsForm($request);
    }

    /**
     * Merge and process the available field functions
     *
     * @param Request $request
     * @return array
     */
    protected function processAllFields(Request $request) {
        return array_merge(
            $this->processSystemFields($request),
            $this->processIndexFields($request),
            $this->processCreateFields($request),
            $this->processUpdateFields($request),
            $this->processDisplayFields($request),
            $this->processDefaultFields($request)
        );
    }

    /**
     * Process the index fields visibility
     *
     * @param Request $request
     * @return array
     */
    protected function processIndexFields(Request $request) {
        return collect($this->fieldsIndex($request))->map(function ($field) {
            if(method_exists($field, 'onlyOnIndex')) {
                return $field->onlyOnIndex();
            }

            return $field;
        })->toArray();
    }

    /**
     * Process the display fields visibility
     *
     * @param Request $request
     * @return array
     */
    protected function processDisplayFields(Request $request) {
        return collect($this->fieldsDisplay($request))->map(function ($field) {
            if(method_exists($field, 'onlyOnDetail')) {
                return $field->onlyOnDetail();
            }

            return $field;
        })->toArray();
    }

    /**
     * Process the create fields visibility
     *
     * @param Request $request
     * @return array
     */
    protected function processCreateFields(Request $request) {
        return collect($this->fieldsCreate($request))->map(function ($field) {
            if(method_exists($field, 'onlyOnForms')) {
                return $field->onlyOnForms()->hideWhenUpdating();
            }

            return $field;
        })->toArray();
    }

    /**
     * Process the update fields visibility
     *
     * @param Request $request
     * @return array
     */
    protected function processUpdateFields(Request $request) {
        return collect($this->fieldsUpdate($request))->map(function ($field) {
            if(method_exists($field, 'onlyOnForms')) {
                return $field->onlyOnForms()->hideWhenCreating();
            }

            return $field;
        })->toArray();
    }

    /**
     * Process manually controlled default fields
     *
     * @param Request $request
     * @return array
     */
    protected function processDefaultFields(Request $request) {
        return $this->defaultFields($request);
    }

    /**
     * Process system fields
     *
     * @param Request $request
     * @return array
     */
    protected function processSystemFields(Request $request) {
        return $this->fieldsSystem($request);
    }
}

Here is an example of its usage. If you want to break up the fields in the resource, be sure to remove the default Fields function so that the base Resource’s call is made.

class ExampleUsage extends Resource
{
  ...

    public function fieldsIndex(Request $request)
    {
        return [
          ...
        ];
    }

    /**
     * Get the fields displayed by the resource.
     *
     * @param  \Illuminate\Http\Request  $request
     * @return array
     */
    public function fieldsDisplay(Request $request)
    {
        return [
          ...
        ];
    }

    public function fieldsForm(Request $request)
    {
        return [
          ...
        ];
    }
}

Conclusion

This system works pretty well at keeping things split into their respective functions. I end up making heavy use of the Index, Display, and Form fields, and not so much system and default fields since the ordering is not ideal.

This does produce a lot of fields in the fields array and does have a slight impact on performance since you are essentially duplicating every field. Additionally, you will have to maintain a list of display and form fields. Be sure that they match, and when you add a field to one, you account for the other if it is an editable field.

This solution may not be for everyone, but it brings Nova inline with what I would expect from other CRM or CRUD related systems.

Please let me know if you have any questions or comments by reaching out to me on twitter at:

Twitter - Code By Kyle

About Me

Kyle Shovan

I am a developer who is currently living in Ho Chi Minh City, Vietnam. I work with Laravel, VueJS, React, and other technologies in order to provide software to companies. Formerly a Microsoft Dynamics consultant.

Comments