Alpinejs Accordion and Toggle on click - javascript

I’m using Alpinejs and Tailwind.
I'm trying to create an accordion with a toggle on each tab, but I want that toggle to trigger at the same time as when a user clicks to open each part of the accordion... here's what I got so far:
<ul class="block mb-4" x-data="{pay:null}">
<li class="flex flex-col">
<div #click="pay !== 'cc' ? pay = 'cc' : pay = null" class="cursor-pointer px-5 py-3 flex items-center bg-blue-50 border text-xl font-semibold border-blue-800 text-blue-800 inline-block hover:shadow rounded-t">
<button type="button" class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-orange-500 bg-gray-200" x-data="{ on: false }" aria-pressed="false" :aria-pressed="on.toString()" #click="on = !on" x-state:on="Enabled" x-state:off="Not Enabled" :class="{ 'bg-orange-500': on, 'bg-white': !(on) }">
<span class="sr-only">Credit Card</span>
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0" x-state:on="Enabled" x-state:off="Not Enabled" :class="{ 'translate-x-5': on, 'translate-x-0': !(on) }"></span>
</button>
<span class="ml-2">Credit / Debit Card</span>
</div>
<p x-show="pay == 'cc'" class="bg-white border-l border-r border-blue-800 py-4 px-2">
This is made with Alpine JS and Tailwind CSS
</p>
</li>
<li class="flex align-center flex-col">
<h4 #click="pay !== 1 ? pay = 1 : pay = null" class="cursor-pointer px-5 py-3 bg-indigo-400 text-white text-center inline-block hover:opacity-75 hover:shadow hover:-mb-3">Accordion item 2</h4>
<p x-show="pay == 1" class="border py-4 px-2">
There's no external CSS or JS
</p>
</li>
<li class="flex align-center flex-col">
<h4 #click="pay !== 2 ? pay = 2 : pay = null" :class="{'cursor-pointer px-5 py-3 bg-indigo-500 text-white text-center inline-block hover:opacity-75 hover:shadow hover:-mb-3': true, 'rounded-b': pay != 2}">Accordion item 3</h4>
<p x-show="pay == 2" :class="{'border py-4 px-2': true, 'rounded-b': pay == 2}">
Pretty cool huh?
</p>
</li>
</ul>
https://codepen.io/kennyk3/pen/eYBwEXN

I'm pretty green, so bear with me. I actually just want to comment but don't have enough reputation.
First of all, you need to remove bg-gray-200 from your <button class> because you're binding :class="{ bg-{color} } for on: true and on: false states. Another one is bg-orange-500 and ring-orange-500 isn't a defined utility on Tailwind CSS. Maybe you mean bg-yellow-500 and ring-yellow-500? Other than that, what is x-state? I can't find it in Alpine.js docs.
Okay, about your requested problem, your code actually has 2 issues.
You want a parent component to communicate with a child component, like a nested component. Alpine.js doesn't do that. Either you merge both components holding multiple properties in the parent component so that you only have one x-data or you $dispatch('customEvent') on your <div> at the #click event (together with pay toggling) and listen to the #customEvent.window on your <button> component.
Alternatively, you can install Alpine Magic Helpers for the $component/$parent communication or install Spruce for saving all states.
Because of your <button> is nested in your <div>, you're firing both #click events every time you #click the <button>. This is why your code looks like the <button> component communicates with your <ul> component which is untrue. You're firing two different #click events. Because of this behavior, if you $dispatch('customEvent') from your <div> component and listen to the #customEvent.window on your <button>, you'll end up toggling your on state twice. Try add #click.debounce.250 on your <button> together with the #customEvent.window and you'll see it toggles to and fro.
As for that, you need to remove #click event on your <button> while listening to the #customEvent.window so that the toggling happens once, or better you merge both x-data making your <ul> holding multiple properties (no need to $dispatch('customEvent') on your <div> and #click on your <button>, just one #click on your <div>).
In my implementation, I make x-data to hold both:
<ul class="block mb-4" x-data="{pay:null, on: false}">
<li class="flex flex-col">
<div #click="pay !== 'cc' ? pay = 'cc' : pay = null, on = !on" class="cursor-pointer px-5 py-3 flex items-center bg-blue-50 border text-xl font-semibold border-blue-800 text-blue-800 inline-block hover:shadow rounded-t">
<button type="button" class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-yellow-500" aria-pressed="false" :aria-pressed="on.toString()" x-state:on="Enabled" x-state:off="Not Enabled" :class="{ 'bg-yellow-500': on, 'bg-gray-200': !(on) }">
<span class="sr-only">Credit Card</span>
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0" x-state:on="Enabled" x-state:off="Not Enabled" :class="{ 'translate-x-5': on, 'translate-x-0': !(on) }"></span>
</button>
<span class="ml-2">Credit / Debit Card</span>
</div>
<p x-show="pay == 'cc'" class="bg-white border-l border-r border-blue-800 py-4 px-2">
This is made with Alpine JS and Tailwind CSS
</p>
</li>
<li class="flex align-center flex-col">
<h4 #click="pay !== 1 ? pay = 1 : pay = null" class="cursor-pointer px-5 py-3 bg-indigo-400 text-white text-center inline-block hover:opacity-75 hover:shadow hover:-mb-3">Accordion item 2</h4>
<p x-show="pay == 1" class="border py-4 px-2">
There's no external CSS or JS
</p>
</li>
<li class="flex align-center flex-col">
<h4 #click="pay !== 2 ? pay = 2 : pay = null" :class="{'cursor-pointer px-5 py-3 bg-indigo-500 text-white text-center inline-block hover:opacity-75 hover:shadow hover:-mb-3': true, 'rounded-b': pay != 2}">Accordion item 3</h4>
<p x-show="pay == 2" :class="{'border py-4 px-2': true, 'rounded-b': pay == 2}">
Pretty cool huh?
</p>
</li>
</ul>
https://codepen.io/wanahmadfiras/pen/jOVgBrz

Accordions (among other components) are now part of official AlpineJS documentation. Use them for perfect out of the box accordions.

Related

Popover on hover vue headllessui

I'm trying to implement the popover from headlessui of vue package with hover. I try to use the mouseenter and mouseleave and the other mouse events but nothing change.
Any solution? There is a better solution? i search on internet I cant nothing about this. I search on headlessui github discussions but nothing.
<template>
<div class="fixed top-16 w-full max-w-sm px-4">
<Popover v-slot="{ open }" class="relative">
<PopoverButton
:class="open ? '' : 'text-opacity-90'"
class="group inline-flex items-center rounded-md bg-orange-700 px-3 py-2 text-base font-medium text-white hover:text-opacity-100 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-opacity-75"
>
<span>Solutions</span>
<ChevronDownIcon
:class="open ? '' : 'text-opacity-70'"
class="ml-2 h-5 w-5 text-orange-300 transition duration-150 ease-in-out group-hover:text-opacity-80"
aria-hidden="true"
/>
</PopoverButton>
<transition
enter-active-class="transition duration-200 ease-out"
enter-from-class="translate-y-1 opacity-0"
enter-to-class="translate-y-0 opacity-100"
leave-active-class="transition duration-150 ease-in"
leave-from-class="translate-y-0 opacity-100"
leave-to-class="translate-y-1 opacity-0"
>
<PopoverPanel
class="absolute left-1/2 z-10 mt-3 w-screen max-w-sm -translate-x-1/2 transform px-4 sm:px-0 lg:max-w-3xl"
>
<div
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="relative grid gap-8 bg-white p-7 lg:grid-cols-2">
<a
v-for="item in solutions"
:key="item.name"
:href="item.href"
class="-m-3 flex items-center rounded-lg p-2 transition duration-150 ease-in-out hover:bg-gray-50 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<div
class="flex h-10 w-10 shrink-0 items-center justify-center text-white sm:h-12 sm:w-12"
>
<div v-html="item.icon"></div>
</div>
<div class="ml-4">
<p class="text-sm font-medium text-gray-900">
{{ item.name }}
</p>
<p class="text-sm text-gray-500">
{{ item.description }}
</p>
</div>
</a>
</div>
<div class="bg-gray-50 p-4">
<a
href="##"
class="flow-root rounded-md px-2 py-2 transition duration-150 ease-in-out hover:bg-gray-100 focus:outline-none focus-visible:ring focus-visible:ring-orange-500 focus-visible:ring-opacity-50"
>
<span class="flex items-center">
<span class="text-sm font-medium text-gray-900">
Documentation
</span>
</span>
<span class="block text-sm text-gray-500">
Start integrating products and tools
</span>
</a>
</div>
</div>
</PopoverPanel>
</transition>
</Popover>
</div>
</template>
Seems like common request in HeadlessUI community.
Another solution found this solution on Github that worked fine for me.
for vanilla vue 3 with js the original solution link Github Issue
Nuxt 3 with typescript maintaining accessibility here's the code ↓
<script setup lang="ts">
import { Popover, PopoverButton, PopoverPanel } from '#headlessui/vue'
interface Props {
label: string
hasHref?: boolean
href?: string
}
const props = defineProps<Props>()
const popoverHover = ref(false)
const popoverTimeout = ref()
const hoverPopover = (e: any, open: boolean): void => {
popoverHover.value = true
if (!open) {
e.target.parentNode.click()
}
}
const closePopover = (close: any): void => {
popoverHover.value = false
if (popoverTimeout.value) clearTimeout(popoverTimeout.value)
popoverTimeout.value = setTimeout(() => {
if (!popoverHover.value) {
close()
}
}, 100)
}
</script>
<template>
<Popover v-slot="{ open, close }" class="relative">
<PopoverButton
:class="[
open ? 'text-primary' : 'text-gray-900',
'group inline-flex items-center rounded-md bg-white text-base font-medium hover:text-primary focus:outline-none focus:ring-2 focus:ring-primary focus:ring-offset-2'
]"
#mouseover="(e) => hoverPopover(e, open)"
#mouseleave="closePopover(close)"
>
<span v-if="!hasHref">{{ props.label }}</span>
<span v-else>
<NuxtLink :to="href">
{{ props.label }}
</NuxtLink>
</span>
<IconsChevronDown
:class="[
open ? 'rotate-180 transform text-primary' : '',
' ml-1 h-5 w-5 text-primary transition-transform group-hover:text-primary'
]"
aria-hidden="true"
/>
</PopoverButton>
<transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-1"
>
<PopoverPanel
class="absolute left-1/2 z-10 mt-3 ml-0 w-auto min-w-[15rem] -translate-x-1/2 transform px-2 sm:px-0"
#mouseover.prevent="popoverHover = true"
#mouseleave.prevent="closePopover(close)"
>
<div
class="overflow-hidden rounded-lg shadow-lg ring-1 ring-black ring-opacity-5"
>
<div class="relative grid gap-1 bg-white p-3">
<slot> put content here </slot>
</div>
</div>
</PopoverPanel>
</transition>
</Popover>
</template>
It is in the docs:showing-hiding-popover.
open is an internal state used to determine if the component is shown or hidden. To implement your own functionality, you can remove it and use the static prop to always render a component. Then you can mange the visibility with your own state ref and a v-if/v-show. The mouse-action has to be in the upper scope, so it is not triggered leaving the component, e.g. moving the mouse from button to panel.
Below is a modified example from the API documentation:
<template>
<Popover
#mouseenter="open = true"
#mouseleave="open = false"
#click="open = !open"
>
<PopoverButton #click="open = !open">
Solutions
<ChevronDownIcon :class="{ 'rotate-180 transform': open }" />
</PopoverButton>
<div v-if="open">
<PopoverPanel static>
Insights
Automations
Reports
</PopoverPanel>
</div>
</Popover>
</template>
<script setup>
import { ref } from 'vue';
import { Popover, PopoverButton, PopoverPanel } from '#headlessui/vue';
import { ChevronDownIcon } from '#heroicons/vue/20/solid';
const open = ref(false);
</script>

I have to click 2 times to the link in mobile view of my Next JS application in order to change routes

I am building a Next JS application,
On the desktop view, there is no problem, but when it changes to the mobile view after a scroll event or changing routes when I want to click on a link/button, etc. I need to click two times on it. I am suspicious that after an event, the focus on the page is being lost, but there is no overflow or something. I couldn't figure out where the focus was gone.
Here is an example of my code
<Link href={storyUrl}>
<a className="group inline-flex flex-col mb-4 md:mb-11 space-y-2 ">
<div className="flex items-center gap-2">
{isDeleted && (
<div className="group">
<Info className="text-slate-400" />
<div className="hidden rounded-lg p-2 text-xs bg-gray-100 absolute z-50 group-hover:block shadow-md w-48 mt-5">
This story has been deleted or in draft
</div>
</div>
)}
{draft && (
<span className="inline-flex items-center px-3 py-0.5 rounded-full text-sm font-medium bg-purple-50 text-purple-800">
Draft
</span>
)}
<h2
className={`text-slate-900 text-3xl font-semibold leading-9 tracking-md transition ease-in-out duration-150 group-hover:text-purple-700 ${
isDeleted ? 'line-through' : ''
}`}
>
{title}
</h2>
</div>
<p className="text-slate-500 text-sm tracking-sm line-clamp-2 break-words w-full xl:w-[600px] ">
{infoText}
</p>
</a>
</Link>
Thanks for any responses in advance
I tried to change the Links to the router.push but as I thought it was irrelevant.

How to set and unset cookies using AlpineJS and Tailwind Toggle

I have the following code that uses tailwindcss for the toggle, and AlpineJS for adding a cookie base on the toggle state (on/off).
But I can't seem to make it work.
Here's the codepen: https://codepen.io/williamharvey/pen/GRMBqzO
Any help or direction will be appreciated
Thanks
And here's the code.
<div
class="flex w-full items-center bg-gray-100 border-b border-gray-200 px-5 py-3 text-sm"
x-data="{ cookieConcent1: docCookies.getItem('cookieConcent1') === 'true'} "
x-init="$watch('!cookieConcent1', val => docCookies.setItem('cookieConcent1', val))"
x-bind:class="{ 'cookieConcent1': cookieConcent1 }">
<div class="flex-1">
<p>Strictly necessary cookies</p>
</div>
<div class="w-10 text-right">
<button type="button"
class="relative inline-flex flex-shrink-0 h-6 w-11 border-2 border-transparent rounded-full cursor-pointer transition-colors ease-in-out duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-gray-300 bg-gray-200"
x-data="{ on: true }"
role="switch"
aria-checked="false"
:aria-checked="on.toString()"
#click="on = !on;cookieConcent1 = !cookieConcent1"
x-state:on="Enabled"
x-state:off="Not Enabled"
:class="{ 'bg-green-400': on, 'bg-gray-200': !(on) }
">
<span class="sr-only">Use setting</span>
<span aria-hidden="true" class="pointer-events-none inline-block h-5 w-5 rounded-full bg-white shadow transform ring-0 transition ease-in-out duration-200 translate-x-0"
x-state:on="Enabled"
x-state:off="Not Enabled"
:class="{ 'translate-x-5': on, 'translate-x-0': !(on) }
"></span>
</button>
</div>
</div>

How to properly handle mouse events with AlpineJS for my menu?

I'm working on a mega dropdown menu component with alpine.js and Tailwind CSS. Right now I have met some difficulties and I can't make the mouse events work. Below is my code and the red block is the dropdown mega menu. As you can see, when you move the cursor on the Product menu item, the mega menu is shown. After that if you move the cursor down a little bit on the mega menu, the menu is still shown. At this state, if you move out of the mega menu, the mega menu is close. The problem is that when you move on to the Product menu and then move your cursor right to the *Pricing" menu item, the dropdown is still shown, which is not correct. When the user moves out of the Product menu, how can I test if the destination is the mega dropdown or other menu items like Pricing (and close the mega dropdown in this case) using alpine.js?
<script src="https://cdnjs.cloudflare.com/ajax/libs/alpinejs/2.8.2/alpine.js"></script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.1.2/tailwind.min.css" rel="stylesheet"/>
<div class="border-b border-gray-200 relative h-20 flex py-0 items-stretch" x-data="{isProdMenuOpen: false, isLearnMenuOpen: false}">
<div class="flex container mx-auto items-center justify-between h-20"><img class="flex-none h-6" src="https://sitebulb.com/images/icons/logo.svg" />
<ul class="flex items-center justify-between flex-none">
<li class="h-20 border-b border-l border-r border-transparent mx-6 flex items-center relative" #mouseenter="isProdMenuOpen = true">
<!--div.h-20.w-full.flex.items-center.border-b.border-transparent(class="hover:border-red")--><a class="font-semibold flex-none" href="">Product</a>
<div class="h-full absolute -left-6 -right-6 top-0 border-r border-l border-gray-200">
<div class="w-full absolute bottom-0 bg-black z-100 inset-x-0" style="transform: translate(0px, 2px); height:4px;"></div>
</div>
</li>
<li class="h-20 flex items-center mx-6"><a class="font-semibold flex-none" href="">Pricing</a></li>
<li class="h-20 flex items-center mx-6"><a class="font-semibold flex-none" href="">About</a></li>
</ul>
<div class="flex items-center justify-between flex-none"><button class="bg-white rounded border px-3 py-2 text-sm font-medium border-gray-200">Login</button><button class="bg-white rounded bg-green-400 text-white text-sm font-medium px-3 py-2 ml-2 hover:bg-green-500">Free Trial</button></div>
</div><!-- Popup Menu Items -->
<div class="flex flex-row items-start border-b border-gray-200 w-screen h-40 absolute left-0 top-20 bg-red-400" id="prodmenu" x-show="isProdMenuOpen" #mouseenter="isProdMenuOpen = true" #mouseleave="isProdMenuOpen = false"></div>
</div>
There's a solution for this that is independent of Alpine or Javascript at all. Using Tailwind's group class on the li, making the popup a child of the group li and removing relative positioning from the li so the child div can go full width will give the same effect I believe you are looking for and fully controlled by CSS.
There will need to be some fudging around to make a similar effect that you are doing with the different color underline on the li in this example I used a border and relative positioning of the anchor tag to offset the border's height. Also, you will need to extend your variants to allow display to be manipulated in group-hover in the code snippet here on SO below I'm just using the class generated by that config to fake it. Here's a Tailwind play that shows the actual config as well https://play.tailwindcss.com/EYY0alaQuB?file=config
.group:hover .group-hover\:flex {
display: flex;
}
<link href="https://cdnjs.cloudflare.com/ajax/libs/tailwindcss/2.1.2/tailwind.min.css" rel="stylesheet" />
<div class="border-b border-gray-200 relative h-20 flex py-0 items-stretch">
<div class="flex container mx-auto items-center justify-between h-20"><img class="flex-none h-6" src="https://sitebulb.com/images/icons/logo.svg" />
<ul class="flex items-center justify-between flex-none">
<li class="h-20 border-b-4 border-l border-r border-gray-200 px-6 flex items-center group" style="border-bottom: 4px solid black">
<a class="font-semibold flex-none relative top-1" href="">Product</a>
<!-- Popup Menu Items -->
<div class="hidden group-hover:flex flex-row items-start border-b border-gray-200 w-screen h-40 absolute left-0 top-20 bg-red-400" id="prodmenu"></div>
</li>
<li class="h-20 flex items-center mx-6 border-b-4 border-transparent"><a class="font-semibold flex-none relative top-1" href="">Pricing</a></li>
<li class="h-20 flex items-center mx-6 border-b-4 border-transparent"><a class="font-semibold flex-none relative top-1" href="">About</a></li>
</ul>
<div class="flex items-center justify-between flex-none">
<button class="bg-white rounded border px-3 py-2 text-sm font-medium border-gray-200">Login</button>
<button class="rounded bg-green-400 text-white text-sm font-medium px-3 py-2 ml-2 hover:bg-green-500">Free Trial</button>
</div>
</div>
</div>

How to test value in x-for variable and change the CSS Style according to the value?

How can I test the value of an x-for variable and then meet an IF condition to change the CSS value?
<template x-for="item in myForData" :key="item">
<div id="jh-stats-neutral" class="flex items-center shadow hover:bg-indigo-100
hover:shadow-lg hover:rounded transition duration-150 ease-in-out transform
hover:scale-102 flex flex-col justify-center px-2 py-3 mt-0 bg-white border
border-gray-300 rounded md:mb-0"
<div class="text-md">
<p
class="text-md font-semibold text-center text-red-600 *<% if ("item.categoria.substring(3)" == 'Rec') { %> bg-blue-200 <% } %>*"
x-text="item.categoria"
></p>
<p
class="text-lg text-center text-red-700"
x-text="item.total"
></p>
</div>
</div>
</template>
Someone can help me?
You can bind to the class attribute using x-bind:class or :class for short.
x-bind:class has special syntax that means you can pass it an object whose key is the class(es) and the value is whether the class(es) is applied.
Eg. x-bind:class="{ 'hello': false, 'world': true }" will add the world class but not hello.
In your case you can do the class toggling using Alpine:
*<% if ("item.categoria.substring(3)" == 'Rec') { %> bg-blue-200 <% } %>* turns into class="...otherclasses" x-bind:class="{ 'bg-blue-200': item.categoria.substring(3) === 'Rec' }".
Your full code updated with the x-bind:class
<template x-for="item in myForData" :key="item">
<div id="jh-stats-neutral" class="flex items-center shadow hover:bg-indigo-100
hover:shadow-lg hover:rounded transition duration-150 ease-in-out transform
hover:scale-102 flex flex-col justify-center px-2 py-3 mt-0 bg-white border
border-gray-300 rounded md:mb-0"
<div class="text-md">
<p
class="text-md font-semibold text-center text-red-600 *<% if ("item.categoria.substring(3)" == 'Rec') { %> bg-blue-200 <% } %>*"
x-bind:class="{ 'bg-blue-200': item.categoria.substring(3) === 'Rec' }"
x-text="item.categoria"
></p>
<p
class="text-lg text-center text-red-700"
x-text="item.total"
></p>
</div>
</div>
</template>
I would like to answer your question - unfortunately I cannot add a comment - please provide more details - what exactly do you want to do.
First of all, x-for = "item in myForData" is not a variable but an attribute.
Fantastic Hugo!
Your explanation open my mind to a world of possibilities... I left behind some trash in original code above, but it's my code updated:
<template x-for="item in myForData" :key="item">
<div id="jh-stats-neutral" class="flex items-center shadow hover:bg-indigo-100
hover:shadow-lg hover:rounded transition duration-150 ease-in-out transform
hover:scale-102 flex flex-col justify-center px-2 py-3 mt-0 bg-white border
border-gray-300 rounded md:mb-0"
<div class="text-md">
<p
class="card text-md font-semibold text-center"
x-bind:class="{ 'text-blue-600': item.total.substring(0,4) !== 'R$ -' , 'text-red-600': item.total.substring(0,4) === 'R$ -' }"
x-text="item.conta"
></p>
<p
class="value text-lg text-center text-gray-700"
x-text="item.total"
></p>
</div>
</div>
</template>
Thanks once more time and let you know that I'm your fã!

Categories

Resources