qwik icon indicating copy to clipboard operation
qwik copied to clipboard

Need an easy solution to share a context with slotted elements

Open emexal-company opened this issue 2 years ago • 9 comments

Is your feature request related to a problem?

I'm trying to create a set of components that could be used as following:

<Dropdown label="Animals">
  <MenuItem>Cat</MenuItem>
  <MenuItem>Dog</MenuItem>
</Dropdown>

The MenuItem elements will be in a slot of the parent Dropdown component. They need to communicate to the parent, whenever they are clicked, in order to have only one of them selected at a time.

So I need some kind of dynamic context that is created for each instance of a Dropdown element, that will be shared with the MenuItem elements.

Today, the only solution to perform that is to create a new context outside of the Dropdown element, and give it as property to the Dropdown, and the MenuItem elements.

const dropdownContext = createContext('dropdown');

<Dropdown label="Animals" context={dropdownContext}>
  <MenuItem context={dropdownContext}>Cat</MenuItem>
  <MenuItem context={dropdownContext}>Dog</MenuItem>
</Dropdown>

Each MenuItem would use a callback from the context to inform the Dropdown about the click event. And the Dropdown would inform the MenuItems about any change if the need to unselect themselves.

The drawbacks of this solution is that I need to create manually a new context each time I create a new Dropdown element.

Describe the solution you'd like

I would like an easy way to create a context inside a parent element, and share it with slotted elements. A possible sollution would to allow slotted elements retreive a context from a parent, using the context name (currently, the only way for a child to retreive a context, is using a reference).

Describe alternatives you've considered

Any other solution for communication between parent and slotted elements is acceptable for me.

Additional context

No response

emexal-company avatar Oct 06 '22 14:10 emexal-company

why every Dropdown needs to create a new context?

manucorporat avatar Oct 06 '22 18:10 manucorporat

the parent should use: useContextProvider() to pass a shared store and, the MenuItem use useContext() to get a holding of that store.

both using the same globally declared createContext()

manucorporat avatar Oct 06 '22 18:10 manucorporat

I've tried the following using a globally declared createContext():

  export const Dropdown = component$((props: DropdownProps) => {
  const store = useStore<DropdownStore>({
    open: false,
    selected: props.selected,
    onMenuItemClicked$: async (name: string) => {
      console.log(`${name} clicked`);
      store.selected = name;
    },
  });
  useContextProvider(DropdownContext, store);

But actually, this doesn't work, because the callback can't be serialized. So, I don't find any solution for a slotted element, to communicate with a parent.

Any suggestion ?

emexal-company avatar Oct 07 '22 13:10 emexal-company

Try this:

import {$} from '@builder.io/qwik';
  const store = useStore<DropdownStore>({
    open: false,
    selected: props.selected,
    onMenuItemClicked: $(async (name: string) => {
      console.log(`${name} clicked`);
      store.selected = name;
    }),
  });

manucorporat avatar Oct 07 '22 23:10 manucorporat

Didn't work... I've got the following error: 2:09:14 PM [vite] Internal server error: Cannot access 'store' before initialization The problem occurs because of the line store.selected = name;

emexal-company avatar Oct 10 '22 13:10 emexal-company

This should be fixed by #1985.

nnelgxorz avatar Nov 05 '22 06:11 nnelgxorz

I don't understand if this is fixed and in the actual (0.16.1) package. Following the example of @manucorporat I have the same error of @emexal-company

[vite] Internal server error: Cannot access 'store' before initialization

artecoop avatar Dec 21 '22 11:12 artecoop

Actually, this is solved. The problem is that I was trying to use the variable store whereas I was initializing it.

The final solution that I used is :

  1. Create the store without the event handler
const store = useStore<DropdownStore>({
   open: false,
   selected: props.selected,
});
  1. Create the event handler
const onMenuItemClicked$ = $((value: string) => {
   console.log(`${value} clicked`);
   store.selected = value;
});
  1. Add the handler to the store (Put it inside useWatch, otherwise you will see a warning)
useWatch$(() => {
   store.onMenuItemClicked$ = onMenuItemClicked$;
});

omarbelkhodja avatar Dec 21 '22 12:12 omarbelkhodja

I ended up making something like you suggested, moving the event handler outside the store, and referencing them inside. No need to use useWatch$, which is deprecated

artecoop avatar Dec 21 '22 14:12 artecoop

This is fixed!

manucorporat avatar Jul 01 '23 11:07 manucorporat