Skip to content

feat(useOverlay)!: handle programmatic modals and slideovers #3279

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

Merged
merged 75 commits into from
Feb 27, 2025

Conversation

genu
Copy link
Contributor

@genu genu commented Feb 9, 2025

Related Issue

Related Discussion: #1758

Should fix these issues:
Resolves #2041
Resolves #2777
Resolves #2799
Resolves #3157

❓ Type of change

  • πŸ“– Documentation (updates to the documentation or readme)
  • 🐞 Bug fix (a non-breaking change that fixes an issue)
  • πŸ‘Œ Enhancement (improving an existing functionality)
  • ✨ New feature (a non-breaking change that adds functionality)
  • 🧹 Chore (updates to the build process or auxiliary tools and libraries)
  • ⚠️ Breaking change (fix or feature that would cause existing functionality to change)

πŸ“š Description

@benjamincanac This is an ambitious PR from previous conversations that we had around slideovers and modals.

It tries to address two things that came up in the past. Perhaps its best to address them before v3 is released:

  1. Ability work with multiple (nested) overlays reliably
  2. Overlay logic of both slideover and modal is essentially the same.

useOverlay
Introduced to manage the modals and slideovers that are created

useOverlay().create(...)
It essentially does the same thing that useSlideover and useModal did, but its more generic. It takes any component and exposes:

  • open
  • close
  • patch

useOverlayInstance()
Used to inject the instance of an overlay. This would be useful to allow allow a instantiated overlay to also programmatically also perform actions on itself (e.g. close itself)

Under the hood useOverlay manages the overlays, mount/unmount components, and handle the stacking of the components (i.e. nesting)

Looking forward to some feedback.

The API change would now look like this:

const overlay = useOverlay()

// Create an instance with some initial params. This DOESN'T open the modal!
const modalA = overlay.create(ModalExample, {
  attrs: {
    count: 1
  }
})

// Create another instance without passing any params. Doesn't open the modal
const modalB = overlay.create(ModalExample2)

// Create another instance, but it opens right away
const modalC = overlay.create(ModalExample3, { defaultOpen: true })

// Open modal A, but also set a new param before opening
// Since modalC is already open, modelA would actually be open above modalC
modalA.open({ anotherParam: "test" })

// Open modal B as is
modalB.open()

// Open a slideover
const slideoverA = overlay.create(SlideoverExampleA)

// Open it
slideoverA.open()

πŸ“ Checklist

  • I have linked an issue or discussion.
  • I have updated the documentation accordingly.

@genu genu closed this Feb 10, 2025
@genu genu reopened this Feb 10, 2025
@genu genu marked this pull request as ready for review February 10, 2025 16:46
@genu
Copy link
Contributor Author

genu commented Feb 10, 2025

There are some APIs missing at this point until I get initial feedback on this design.

@genu
Copy link
Contributor Author

genu commented Feb 11, 2025

It seems that useModal and useSlideover are looking mostly identical at this point. maybe we can have a single composable for working with both?

The new useManagedOverlay does most of the work now, and we simply expose the attrs from the inferred component.

Edit:
Introduced useOverlayInstance to handle both useSlideover and useModal. If you like this design, we can eliminate those two composable in favor of the generic useOverlayInstance composable.

let me know your thoughts.

@genu genu requested a review from benjamincanac February 26, 2025 14:07
@benjamincanac benjamincanac merged commit 108d36f into nuxt:v3 Feb 27, 2025
2 checks passed
@genu genu deleted the feature/useOverlayManager branch February 27, 2025 16:34
@benjamincanac
Copy link
Member

Thanks @genu! 😊

@simonmaass
Copy link

i get the following errors when using:
image

image

@simonmaass
Copy link

also
image
@benjamincanac

@simonmaass
Copy link

OverlayProvider.vue is missing
import { computed } from "vue";

and useOverlay.js is missing
import { reactive, markRaw, shallowReactive } from "vue";

Copy link
Member

benjamincanac commented Feb 28, 2025

Damn it, just released beta.1 😒

@simonmaass
Copy link

Damn it, just released beta.1 😒

sorry :( - i also commented on ur last commit about it and tagged you!

Copy link
Member

benjamincanac commented Feb 28, 2025

Is this breaking your app? Going to release beta.2 in a moment.

@eevho
Copy link

eevho commented Mar 2, 2025

declare function _useOverlay(): {
    overlays: import("vue").ShallowReactive<Overlay[]>;
    open: <T extends Component>(id: symbol, props?: ComponentProps<T>) => Promise<any>;
    close: (id: symbol, value?: any) => void;
    create: <T extends Component>(component: T, _options?: OverlayOptions<ComponentProps<T>>) => OverlayInstance<T>;
    patch: <T extends Component>(id: symbol, props: Partial<ComponentProps<T>>) => void;
    unMount: (id: symbol) => void;
};

e.g:

const overlay = useOverlay()
overlay.create(UModal, { props: {untyped} })

I didn't get the correct props type when using useOverlay, and there are no property hints when passing overlay.open or the open function after create

@genu
Copy link
Contributor Author

genu commented Mar 3, 2025

declare function _useOverlay(): {
    overlays: import("vue").ShallowReactive<Overlay[]>;
    open: <T extends Component>(id: symbol, props?: ComponentProps<T>) => Promise<any>;
    close: (id: symbol, value?: any) => void;
    create: <T extends Component>(component: T, _options?: OverlayOptions<ComponentProps<T>>) => OverlayInstance<T>;
    patch: <T extends Component>(id: symbol, props: Partial<ComponentProps<T>>) => void;
    unMount: (id: symbol) => void;
};

e.g:

const overlay = useOverlay()
overlay.create(UModal, { props: {untyped} })

I didn't get the correct props type when using useOverlay, and there are no property hints when passing overlay.open or the open function after create

Could you please create a reproduction, and create a separate issue for this.

@eevho
Copy link

eevho commented Mar 3, 2025

Ok, I have resubmitted it. #3436

@hasan-ozbey
Copy link

How one catch emit in root component, which was fired from programmatic modal? The usage of the modal component is more complex than ever!

const modal = overlay.create(AvatarCropperModal, {
  props: {
    avatar: avatar.value,
  },
})

How would I catch avatarUpdated emit fired from AvatarCropperModal here so I can perform some operations in the root component based on that emit? What am I missing?

@genu
Copy link
Contributor Author

genu commented Mar 8, 2025

How one catch emit in root component, which was fired from programmatic modal? The usage of the modal component is more complex than ever!

const modal = overlay.create(AvatarCropperModal, {
  props: {
    avatar: avatar.value,
  },
})

How would I catch avatarUpdated emit fired from AvatarCropperModal here so I can perform some operations in the root component based on that emit? What am I missing?

When opening programmatic modals, only a single event can be emitted. This is because an overlay is assumed to either return something or nothing. This is by design.

In your case, the way you would do it is:

const modal = overlay.create(AvatarCropperModal, {
  props: {
    avatar: avatar.value,
  },
})

const modalResult = await modal.open()

// Perform any operation you want with the result.

The important thing to remember is that your AvatarCropperModal component needs to emit a close event with the data that you want to return to the parent component.

See the example here: https://ui3.nuxt.dev/composables/use-overlay#example

@hasan-ozbey
Copy link

hasan-ozbey commented Mar 8, 2025

okay just a heads up, it was indeed working with:

await modal.open({
    avatar: avatar.value,
    onAvatarUpdated: () => ...
})

But I decided to pass data through close emit since it doesn't matter for me.

Now why I can't submit form which lies inside a modal, is this somehow related??

<UModal v-model:open="isLanguageModalOpen">
  <UButton icon="i-heroicons-plus-circle" />
  <template #content>
    <UForm :schema="knownLanguageSchema" :state="modalState" @submit.prevent="onAddNewLanguage">
      <UFormField name="newLanguage">
        <USelectMenu v-model="modalState.newLanguage" :items="languages" value-key="code" label-key="name" />
      </UFormField>

      <UFormField name="motherTongue">
        <UCheckbox v-model="modalState.motherTongue" />
      </UFormField>

      <div class="flex gap-4 items-center">
        <UButton type="submit" />
        <UButton @click="onCancelNewLanguage" />
      </div>
    </UForm>
  </template>
</UModal>

when I submit this form using a button type="submit", onAddNewLanguage() function is not working at all.

@genu
Copy link
Contributor Author

genu commented Mar 8, 2025

The submission issue might be unrelated.

Do note, however, that with dynamic modals, you shouldn't add v-model:open="isLanguageModalOpen" because the open state is managed automatically by useOverlay composable.

If you still have issues, please open up a new issue with a reproduction.

rdjanuar pushed a commit to rdjanuar/ui that referenced this pull request Apr 23, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment