Skip to content

Instantly share code, notes, and snippets.

@dedemenezes
Created September 10, 2025 02:08
Show Gist options
  • Select an option

  • Save dedemenezes/8e6e1f653cf11fb7370a28038aa722e4 to your computer and use it in GitHub Desktop.

Select an option

Save dedemenezes/8e6e1f653cf11fb7370a28038aa722e4 to your computer and use it in GitHub Desktop.
# Programs controller i generate the filter options
# Gathering filter options
paises_filter   = base_scope.includes(pelicula: :paises)
                            .map { _1.pelicula.paises }
                            .flatten
                            .uniq
                            .sort_by { |it| it.nome_pais }
                            .as_json(only: %i[id nome_pais])

mostras_filter  = Mostra.where(edicao_id: EDICAO_ATUAL)
                        .to_a
                        .uniq { |m| m.id }
                        .sort_by { |it| it.permalink_pt }
                        .as_json(
                          only: %i[id permalink_pt nome_abreviado],
                          methods: [ :tag_class, :display_name ]

cinemas_filter = Cinema.where(edicao_id: EDICAO_ATUAL)
                        .to_a
                        .uniq { |m| m.id }
                        .sort_by { |it| it.nome }
                        .as_json(only: %i[id nome endereco edicao_id])

#  down below i pass to the ProgramPage.vue
render inertia: "ProgramPage", props: {
  rootUrl: @root_url,
  tabBaseUrl: program_url,
  items:,
  elements: @programacoes,
  pagy: @pagy,
  mostrasFilter: mostras_filter,
  cinemasFilter: cinemas_filter,
  paisesFilter: paises_filter,
  menuTabs: @menu_tabs,
  current_filters: {
    query: params[:query],
    mostrasFilter: selected_mostra,
    cinemasFilter: selected_cinema,
    paisesFilter: selected_pais
  },
  has_active_filters: params.permit(:query, :mostrasFilter).to_h.values.any?(&:present?),
  crumbs: breadcrumbs(
    [ "", @root_url ],
    [ "Programação", "" ],
    [ "Programação Completa", "" ],
  )
}

In the program page i receive them as props

// ProgramPage.vue
const props = defineProps({
  tabBaseUrl: { type: String, required: true },
  items: { type: Array, required: true }
  ,elements: { type: Object, required: true }
  ,pagy: { type: Object, required: true }
  ,mostrasFilter: { type: Array, default: () => [] }
  ,cinemaOptions: { type: Array, default: () => [] }
  ,paisesOptions: { type: Array, default: () => [] }
  // ...
}

// And below in the template i forward them to the ProgramFilterForm.vue
<ResponsiveFilterMenu
  :is-open="isFilterMenuOpen"
  :initialFilters="localFilters"
  @filtersApplied="filterSearch"
  @filtersCleared="clearSearchQuery"
  @close-filter-menu="closeMenu"
>
  <template #filters="{ modelValue, updateField }">
    <ProgramsFilterForm
      :model-value="modelValue"
      :update-field="updateField"
      :mostrasFilter="props.mostrasFilter"
      :cinemaOptions="props.cinemaOptions"
      :paisesOptions="props.paisesOptions"
    />
  </template>
</ResponsiveFilterMenu>

In the ProgramFilterForm i receive them as props and pass the options to the combobox component.

// ProgramFilterForm.vue
const props = defineProps({
  modelValue: { type: Object, required: true },
  updateField: { type: Function, required: true },
  mostrasFilter: { type: Array, default: () => [] }, // Program-specific prop
  cinemaOptions: { type: Array, default: () => [] }, // Program-specific prop
  paisesOptions: { type: Array, default: () => [] }

});

<!-- MOSTRAS -->
<AccordionGroup
text="Mostra"
:isOpen="!!props.modelValue.mostrasFilter"
>
<template v-slot:content>
  <div class="overflow-hidden w-full">
    <ComboboxComponent
    :collection="mostrasFilterOptions"
    :modelValue="props.modelValue.mostrasFilter?.permalink_pt || null"
    @update:modelValue="(val) => props.updateField('mostrasFilter', getMostraObjectFromTagClas(val))"
    />
  </div>
</template>
</AccordionGroup>

<!-- CINEMAS -->
<AccordionGroup
text="Cinema"
:isOpen="!!props.modelValue.cinemasFilter"
>
<template v-slot:content>
  <div class="overflow-hidden w-full">
    <ComboboxComponent
    :collection="cinemasFilterOptions"
    :modelValue="props.modelValue.cinemasFilter?.id || null"
    @update:modelValue="(val) => props.updateField('cinemasFilter', val)"
    />
  </div>
</template>
</AccordionGroup>

<!-- PAISES -->
<AccordionGroup
text="Pais"
:isOpen="!!props.modelValue.cinemasFilter"
>
<template v-slot:content>
  <div class="overflow-hidden w-full">
    <ComboboxComponent
    :collection="paisesFilterOptions"
    :modelValue="props.modelValue.paisesFilter?.id || null"
    @update:modelValue="(val) => props.updateField('paisesFilter', val)"
    />
  </div>
</template>
</AccordionGroup>

And what about the props.modelValue. Those, we receive the current_filters from the controller in the ProgramPage as you cansee up there in the controller.

in the program page we create the localFilters ref from the props.current_filters

// PRogramPage.vue
const localFilters = ref({ ...props.current_filters })

// and we forward the to the ResponsiveFilterMenu as :initialFilters
        <ResponsiveFilterMenu
          :is-open="isFilterMenuOpen"
          :initialFilters="localFilters"
          @filtersApplied="filterSearch"
          @filtersCleared="clearSearchQuery"
          @close-filter-menu="closeMenu"
        >
          <template #filters="{ modelValue, updateField }">
            <ProgramsFilterForm
              :model-value="modelValue"
              :update-field="updateField"
              :mostrasFilter="props.mostrasFilter"
              :cinemasFilter="props.cinemasFilter"
              :paisesFilter="props.paisesFilter"
            />
          </template>
        </ResponsiveFilterMenu>

In the responsive filter we receive them as props and do a few things

const props = defineProps({
  isOpen: { type: Boolean, required: true },
  initialFilters: { type: Object, required: true },
});

// 1. we create the internalFilters
const internalFilters = ref({ ...props.initialFilters });

// 2. we forwatd it as v-model to searchFilter
<SearchFilter
  v-model="internalFilters"
  @update:modelValue="(val) => emit('update:modelValue', val)"
  @filtersApplied="emit('filtersApplied', internalFilters)"
  @filtersCleared="emit('filtersCleared')"
  @close-filter-menu="emit('close-filter-menu')"
>
  <template #filters="slotProps">
    <slot
      name="filters"
      :modelValue="internalFilters"
      :updateField="(field, value) => {
        internalFilters[field] = value
      }"
    />
  </template>
</SearchFilter>

// And we get them back from the slot as well. So this is what makes possible the entire sttrucure and flow.

What i dont get is why the cinema and pais is not behaving like the mostra when reopening the menu after the first form submit. If i refresh it works as expected but not in the first one after the submit.

So back to the ProgramFilter where we actually render over those slots and have access to modelValue which is internalFilters, so thats why in the Accordion > Combobox we can do props.modelValue.paisesFilter, which is actually not the option but the ref we use to store the selected option. So that's why i dont get the combobox not beign open and selected after we submit on pais or cinema option while mostra does.

So help me out to debug this. Dont come with solution right away. lets understand the problem and check things first, before changing anything.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment