Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Nova Percent Field Laravel Package

nsavinov/nova-percent-field

A Laravel Nova field for entering and displaying percentage values. Adds a dedicated percent input with formatting and validation, so percentages store and render cleanly in Nova resources without custom field code.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and verify compatibility before writing any resource code.

composer require nsavinov/nova-percent-field

The release date (2018-09-26) is the first thing to deal with. That puts the package in the Nova 1/2 era: Vue 2, Laravel Mix, Nova::script() registering a prebuilt dist/js/field.js. Nova is not distributed via Packagist, so the package's laravel/nova constraint is typically a hard pin against an old major. On a current Nova install the require either fails to resolve or resolves against a stale constraint. If so, fork it, loosen the constraint, and pull it in via a VCS or path repository:

"repositories": [
    { "type": "vcs", "url": "https://github.com/your-org/nova-percent-field" }
]

Read these three things, in this order, before you build on it:

  1. src/ — the field class. Confirm whether it extends Laravel\Nova\Fields\Number (likely) or implements Field directly, and note the $component string. Extending Number means you inherit rules(), nullable(), sortable(), step() and friends for free.
  2. The service provider's boot() — a Nova::script() / Nova::style() call against dist/js/field.js is the Nova 1–3 asset pattern. Confirm your Nova major still honours it; if your app is Vite-built, assume the shipped bundle will not register.
  3. resources/js/components/FormField.vue — this is where the actual percent behaviour lives: the % suffix, the scaling (×100 / ÷100), rounding, and null handling. The PHP class is thin; the component is the product.

First use case — a discount rate on a resource:

use Nsavinov\PercentField\Percent;

public function fields(Request $request)
{
    return [
        Percent::make('Discount Rate', 'discount_rate')
            ->rules('numeric', 'min:0', 'max:100'),
    ];
}

If the field never appears, the cause is almost always asset registration — look for a 404 on dist/js/field.js in the browser console or a stale nova:publish — not the field class. Check the console before reading PHP.

Before your first save, write down the storage contract. Does 15 in the column mean 15%, or does 0.15 mean 15%? Read the Vue component to see which side of the transform your column lands on, then enforce that decision once in a wrapper field (below) so nobody re-litigates it per resource — or per seeder, factory, API resource, or CSV import.

Implementation Patterns

Wrap it in your own field class. With 21 stars, zero dependents, and no release since 2018, treat this as vendored code you own. One App\Nova\Fields\Percent that sets your standard rules, step, and formatting gives you a single file to patch when you fork and a single file to delete when you replace it:

namespace App\Nova\Fields;

use Nsavinov\PercentField\Percent as BasePercent;

class Percent extends BasePercent
{
    public function __construct($name, $attribute = null, $resolveCallback = null)
    {
        parent::__construct($name, $attribute, $resolveCallback);

        $this->step(0.01)
             ->rules('numeric', 'min:0', 'max:100')
             ->nullable();
    }
}

Then every resource just uses \App\Nova\Fields\Percent::make('VAT Rate').

Do any scaling in fillUsing/resolveUsing, symmetrically. If you store fractions but users type whole percents, convert next to the field that owns the value rather than in a model mutator or accessor:

Percent::make('Rate')
    ->fillUsing(function ($request, $model, $attribute) {
        $value = $request->{$attribute};
        $model->{$attribute} = ($value === null || $value === '') ? null : $value / 100;
    })
    ->resolveUsing(fn ($value) => $value === null ? null : $value * 100);

The pair must be exact inverses. An asymmetric pair is the single biggest source of "saved 1500%" bugs. If you write one test for this package, make it round-trip through this pair: fill a request, resolve the model value, assert it equals the input.

Separate display variants from input variants. If you want 15% in the index table but a raw number input on the form, use displayUsing and keep the input untouched:

Percent::make('Discount Rate')
    ->displayUsing(fn ($value) => $value === null ? '—' : number_format($value, 2).'%')
    ->onlyOnIndex();

Mirror every client-side constraint into ->rules(). The component's min/max/step are browser hints. API writes, Nova actions, imports, and pasted values bypass them entirely. Duplicate them server-side every time — the field will not do it for you.

Keep multi-field logic out. Percentages rarely stand alone (weights summing to 100, discount vs. margin, tax inclusive vs. exclusive). This field has zero awareness of sibling fields. Validate totals in a Rule on the request or in a Nova action handler, never inside the field.

Normalise at the boundary for bulk paths. Actions, imports, and artisan commands don't render Vue. Whatever convention you picked (whole percent vs. fraction) has to be applied in those paths by hand — a small Percent::toStorage($value) helper on your wrapper keeps them consistent.

When to skip it entirely. For greenfield work, Number::make('Rate')->step(0.01)->min(0)->max(100)->suffix('%')->rules(...) covers the vast majority of cases with zero third-party surface area. Reach for this package only when maintaining an existing install that already depends on it, or when you genuinely want the fraction↔percent convention shipped as-is and are willing to own it.

Gotchas and Tips

  • Age is the headline risk. Vue 2 + Mix-era bundle on a Vite-built Nova may not mount at all. Budget for a FormField.vue rewrite, not a drop-in. Verify in staging against your exact Nova major before promising a timeline.
  • Blank vs. zero. A percent field that pipes its value through parseFloat turns null into 0, silently converting "no discount set" into "0% discount". Check setInitialValue/prepare in the component and add an explicit null guard on both sides of the round trip. ->nullable() on the PHP side does not protect you here.
  • Step and precision. A default step of 1 blocks 12.5 at the browser level with no server-side error to explain it — users just can't type the value. Set ->step(0.01) deliberately. For float comparisons in tests, use rounded comparison or bccomp; or store basis points as integers and sidestep the problem.
  • Locale separators. Comma-decimal users type 12,5. The 2018-era component almost certainly assumes a dot and may truncate silently. Normalise server-side or document the limitation for internal users.
  • Sorting looks wrong. With a scaling or display transform, sortable() sorts the raw column while the header shows a transformed value. Monotonic transforms are usually harmless — confirm it before someone files "wrong ordering."
  • Debugging a field that will not render: run php artisan nova:publish, hard-refresh, and check the browser console for an asset 404. Nova::script() registrations also fail silently if two fields reuse the same key — a real hazard if you have forked a copy and left the original installed alongside it.
  • Fork hygiene. If you patch FormField.vue, keep the build step (or a checked-in dist/) reproducible, and pin the VCS reference to a tag — otherwise every composer update on a branch ref becomes a surprise.
  • Extension points are thin. Your viable customisation surface is your own wrapper field plus fillUsing / resolveUsing / displayUsing. Do not expect the package to grow features. Plan the fork or the replacement now, ideally before the next Nova major upgrade forces it on you.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
abbasudo/artisan-route
mirvan/ippanel
ignibyte/aic-framework
glavweb/data-schema-bundle
glavweb/datagrid-bundle
tacticmedia/scim-server
uni-method/frame
neika-ai/sdk
adachsoft/php-code-style
pharaonic/laravel-notification-dependencies
beautinow/translug
dev-brainiac/bot-cloaked
octoberrain/html
octoberrain/extension
octoberrain/exception
octoberrain/contracts
adachsoft/api-version-bundle
cgv/localized-resource
noith/settings
kda/laravel-package