magdicom/nova-visible-password
A Laravel Nova field that adds a password input with a show/hide visibility toggle, making password editing in Nova resources clearer and more user-friendly with minimal setup.
What it actually does. One job: renders a Nova input as type="password" with an eye / eye-off control that flips it to type="text" on the client. No migrations, no config file, no service layer — just a Nova\Fields\Password-shaped field class plus a small Vue component.
Prerequisites. Laravel Nova 3.x or 4.x (the last tag is 2022-11-17, pre-Nova 5 — see Gotchas). PHP 7.4+.
Install.
composer require magdicom/nova-visible-password
If Packagist resolution fails (plausible for a 3-star package), pin the VCS repo:
"repositories": [
{ "type": "vcs", "url": "https://github.com/magdicom/nova-visible-password" }
]
then composer require magdicom/nova-visible-password:dev-master.
Where to look first. Three files tell you everything:
vendor/magdicom/nova-visible-password/src/ — the field class (likely Magdicom\NovaVisiblePassword\VisiblePassword) and its FieldServiceProvider. The provider is auto-discovered via extra.laravel.providers and typically calls Nova::script('nova-visible-password', __DIR__.'/../dist/js/field.js').resources/js/components/Field.vue — the toggle itself. Expect roughly: data() { return { show: false } } and :type="show ? 'text' : 'password'".dist/js/field.js — the compiled asset Nova actually loads. If this is missing or stale, you get a plain password input and no toggle.Confirm the class name before writing any code:
grep -rn "class .*extends" vendor/magdicom/nova-visible-password/src
First use case. Swap Nova\Fields\Password for this field in your User resource so an admin can verify the temp password they just typed before saving.
use Magdicom\NovaVisiblePassword\VisiblePassword;
public function fields(NovaRequest $request)
{
return [
VisiblePassword::make('Password')
->onlyOnForms()
->creationRules('required', 'string', 'min:12')
->updateRules('nullable', 'string', 'min:12'),
];
}
If the toggle renders and you can read what you typed, the integration is done. Everything below is about not breaking it.
Hash on write, never in the field. Pick one of these, not both:
protected $casts = ['password' => 'hashed']; — Nova hands over plaintext, Eloquent hashes on save.fillUsing when you need to skip empty submissions and have no cast available:VisiblePassword::make('Password')
->onlyOnForms()
->fillUsing(function ($request, $model, $attribute, $requestAttribute) {
if (filled($request->input($requestAttribute))) {
$model->{$attribute} = Hash::make($request->input($requestAttribute));
}
});
fillUsing replaces Nova's normal fill for that attribute, so don't stack a hashed cast on the same model column.
Create vs. update is the whole game. creationRules('required|min:12') + updateRules('nullable|min:12') is the standard pair for "leave blank to keep the current password." Always pair it with ->onlyOnForms() so the field never renders on index or detail — on detail it would print the bcrypt hash.
Confirmation field. If you want the type-twice UX, add a second instance against password_confirmation and validate with confirmed:
use Illuminate\Validation\Rules\Password;
VisiblePassword::make('Password')
->rules('required', 'confirmed', Password::min(12)),
VisiblePassword::make('Password Confirmation')
->onlyOnForms(),
Submit the form once to confirm the package doesn't mangle the _confirmation attribute — it shouldn't, since it's just a text input, but it's a one-minute check.
Wrap it once and reuse. The package adds nothing but a Vue component, so subclass it and centralize your rules:
class UserPassword extends VisiblePassword
{
public function __construct($name = 'Password', $attribute = 'password')
{
parent::__construct($name, $attribute);
$this->onlyOnForms()
->creationRules('required', 'string', 'min:12')
->updateRules('nullable', 'string', 'min:12')
->hideFromIndex();
}
}
This also insulates resource files if you later drop the package for a hand-rolled field.
Permission-gate the field. Wherever the password field isn't universally trusted:
->canSee(fn ($request) => $request->user()->can('updatePassword', $this->resource))
The toggle itself is cosmetic, but hiding the field keeps the input out of the DOM for users who shouldn't be resetting passwords.
Nova actions / custom tools. If you have a "Change Password" action, don't try to reuse the resource field's fillUsing — actions have their own field pipeline and handle(). Reuse the component pattern (form-only field + hashing in the action) instead.
Local dev loop. To iterate on the component: cd vendor/magdicom/nova-visible-password && npm install && npm run dev, then hard-refresh. Better: copy the package into nova-components/VisiblePassword so npm run watch runs from your own tree.
nova-components/ and you remove the Composer dependency from your risk register.Nova::script(...) registration and asset paths are the fragile bits. Test one form before rolling out across resources.dist/. Nova loads dist/js/field.js, not resources/js/. If you see a password input with no eye icon, the script isn't registered or is 404ing. Check the Network tab on the form route before touching PHP.php artisan nova:publish after any change, and hard-refresh. A cached field.js is the classic "it worked yesterday" cause. If you rebuild, npm run prod — a dev build in production will be obvious.''. Use updateRules('nullable') plus either fillUsing or a mutator that ignores blanks. Test the "edit user, change only the name" path explicitly.->hideFromIndex()->hideFromDetail() or just ->onlyOnForms(). The field resolves the stored hash when displayed outside a form.name/autocomplete attributes come from the Vue component — if you see doubled values, that's the cause. Verify in a real browser, not just Dusk.value before you click. If "who may reveal" is part of your threat model, this adds no control — gate the field instead.vendor:publish. If you want a default icon or reveal-on-focus behavior, you're editing the Vue component, which means you own the fork. Treat that as the extension point.Nova.booting((app) => app.component('visible-password', ...)) vs. the $component set on the PHP field class. Grep both.composer dump-autoload sees the class; (2) field.js loads on the form route; (3) component name matches; (4) the field is actually rendered by this form (not onlyOnForms on a detail view); (5) input type flips in the DOM inspector when you click the icon.How can I help you explore Laravel packages today?