Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: reactive user updates in NavUser & fix password input focus in DeleteUser modal. #54

Open
wants to merge 1 commit into
base: main
Choose a base branch
from

Conversation

chabdulrahmn
Copy link

This PR ensures the user info in NavUser updates reactively after a profile update and properly focuses the password input on error in the DeleteUser modal, improving the overall user experience.

Fix 1: Ensure user avatar & username update immediately in NavUser.vue after profile update.

  • Changed const user = page.props.auth.user as User;
    to const user = computed(() => page.props.auth.user as User);
  • This ensures the UI updates automatically after profile edits without requiring a page refresh.

Fix 2: Properly focus password input on error in DeleteUser modal

  • Fixed TypeError: passwordInput.value?.focus is not a function
  • Used passwordInput.value?.$el?.focus() instead of passwordInput.value?.focus()
  • This ensures correct access to the native <input> inside the Input component from shadcn-vue.

Copy link
Contributor

@tnylea tnylea left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is excellent 👏

Thanks for the PR.

@tnylea tnylea added the Approved Approved for merge label Feb 26, 2025
@AaronLil
Copy link

@chabdulrahmn and @tnylea,

Adding .$el will break the build process because that syntax is no longer the recommended approach in Vue 3. The “Vue way” is to expose the focus() method using defineExpose. For example:

File: resources/js/components/ui/input/Input.vue

// ...
import { ref, defineExpose } from 'vue';
// ...

// Create a reference to the native input element
const inputElement = ref<HTMLInputElement | null>(null);

// Expose the focus method
defineExpose({
  focus: () => inputElement.value?.focus()
});

// ...
// And add a ref to the input element
<input ref="inputElement" v-model="modelValue" ...

Then, in File: resources/js/components/DeleteUser.vue, change:

// Original
const passwordInput = ref<HTMLInputElement | null>(null);

// Updated
const passwordInput = ref<{ focus: () => void } | null>(null);

With these changes, you can call focus() directly from the parent component without relying on .$el, which is not recommended in Vue 3.

@tnylea
Copy link
Contributor

tnylea commented Feb 28, 2025

@AaronLil, thanks for your input.

I just tried this out, and everything is working fine. I do not see that it is breaking the build process. The files that are located inside of the resources/js/components/ui/input/Input.vue are taken directly from ShadCN Vue library (https://github.com/unovue/shadcn-vue/blob/dev/apps/www/src/registry/default/ui/input/Input.vue).

We can incorporate this if the ShadCN vue library changes how they handle this.

If you feel strongly that this should be handled differently, please open a new PR, and we can discuss it further.

Appreciate it.

@AaronLil
Copy link

Hi @tnylea,

I recently started a brand new project using Laravel 12 with the Vue starter kit, and after merging this PR, I encountered the error shown in the image during the build process. That's why I left the comment.

image

@tnylea
Copy link
Contributor

tnylea commented Feb 28, 2025

Thanks @AaronLil, looks like the latest version is using the Vue TSC prebuild step which is not included in this PR. taking a further look.

@AaronLil
Copy link

@tnylea,
I think I found a way to do this without changing the Input component. I’ll send a new PR in a few minutes.

@tnylea
Copy link
Contributor

tnylea commented Feb 28, 2025

@AaronLil. Perfect!

Question though. In your screenshot, your npm run build is running vue-tsc. Did you add that yourself, because we haven't merged that yet from this PR: https://github.com/laravel/vue-starter-kit/pull/52/files

@AaronLil
Copy link

AaronLil commented Feb 28, 2025

@tnylea, You're right, I merged PR #52 on my own for testing purposes 😅. Regardless, I'll submit a new PR with my changes, and then you can decide what's best for the project.

Best regards.

Edit: I just sent PR #65. Cheers!

@chabdulrahmn
Copy link
Author

@AaronLil, Thanks for your commits! On your question about $el: my original fix used passwordInput.value?.$el?.focus() to access the <input> inside shadcn-vue’s Input component, as Vue sets $el to the component’s root DOM element, and focus() wasn’t directly available.

After feedback, I’ve updated it to avoid $el and the ref entirely. Now, it uses the form’s submit event:

Changes Made

  1. Remove passwordInput Ref:

    • Delete const passwordInput = ref<HTMLInputElement | null>(null); from <script setup>.
    • Remove ref="passwordInput" from the <Input> component in the template.
  2. Update deleteUser Function:

    • Modified the onError callback to use e.target instead of a ref:
      • Original: onError: () => passwordInput.value?.$el.focus()
      • Update:
        onError: () => {
            nextTick(() => {
                const formElement = e.target as HTMLFormElement;
                const passwordInput = formElement.password as HTMLInputElement;
                passwordInput?.focus();
            });
        }
    • Added nextTick to ensure DOM updates are complete before focusing.
    • Cast e.target to HTMLFormElement and accessed the password input via formElement.password using its name="password" attribute.

Full Updated Code

For reference, here’s the final result with all changes applied:

<script setup lang="ts">
import { useForm } from '@inertiajs/vue3';
import { nextTick } from 'vue';

// Components
import HeadingSmall from '@/components/HeadingSmall.vue';
import InputError from '@/components/InputError.vue';
import { Button } from '@/components/ui/button';
import {
    Dialog,
    DialogClose,
    DialogContent,
    DialogDescription,
    DialogFooter,
    DialogHeader,
    DialogTitle,
    DialogTrigger,
} from '@/components/ui/dialog';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';

const form = useForm({
    password: '',
});

const deleteUser = (e: Event) => {
    e.preventDefault();

    form.delete(route('profile.destroy'), {
        preserveScroll: true,
        onSuccess: () => closeModal(),
        onError: () => {
            nextTick(() => {
                const formElement = e.target as HTMLFormElement;
                const passwordInput = formElement.password as HTMLInputElement;
                passwordInput.focus();
            });
        },
        onFinish: () => form.reset(),
    });
};

const closeModal = () => {
    form.clearErrors();
    form.reset();
};
</script>

<template>
    <div class="space-y-6">
        <HeadingSmall title="Delete account" description="Delete your account and all of its resources" />
        <div class="space-y-4 rounded-lg border border-red-100 bg-red-50 p-4 dark:border-red-200/10 dark:bg-red-700/10">
            <div class="relative space-y-0.5 text-red-600 dark:text-red-100">
                <p class="font-medium">Warning</p>
                <p class="text-sm">Please proceed with caution, this cannot be undone.</p>
            </div>
            <Dialog>
                <DialogTrigger as-child>
                    <Button variant="destructive">Delete account</Button>
                </DialogTrigger>
                <DialogContent>
                    <form class="space-y-6" @submit="deleteUser">
                        <DialogHeader class="space-y-3">
                            <DialogTitle>Are you sure you want to delete your account?</DialogTitle>
                            <DialogDescription>
                                Once your account is deleted, all of its resources and data will also be permanently deleted. Please enter your
                                password to confirm you would like to permanently delete your account.
                            </DialogDescription>
                        </DialogHeader>

                        <div class="grid gap-2">
                            <Label for="password" class="sr-only">Password</Label>
                            <Input id="password" type="password" name="password" v-model="form.password" placeholder="Password" />
                            <InputError :message="form.errors.password" />
                        </div>

                        <DialogFooter class="gap-2">
                            <DialogClose as-child>
                                <Button variant="secondary" @click="closeModal"> Cancel </Button>
                            </DialogClose>

                            <Button variant="destructive" :disabled="form.processing">
                                <button type="submit">Delete account</button>
                            </Button>
                        </DialogFooter>
                    </form>
                </DialogContent>
            </Dialog>
        </div>
    </div>
</template>

This grabs the <input name="password"> directly from the form via e.target.password, keeping it simple and native. No need for $el or component internals—works with shadcn-vue as-is. Let me know if you have questions!

Best,
@chabdulrahmn

@AaronLil
Copy link

AaronLil commented Mar 3, 2025

@chabdulrahmn, that looks fancy and worked like a charm! Now you can update this PR to use the code you provided so we can wrap up this issue.

I ended up submitting another PR addressing the same problem, but I'll delete it as soon as your PR includes the new code.

Great stuff, man! 🚀

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Approved Approved for merge
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants