Why is my Accordion not working using HTML and Stimulusjs - stimulusjs

Please I need help with making this accordion work I am not too familiar with StimulusJS. The first accordion works fine but the rest does not respond. I have attached a snippet of the code here please let me know what I am doing wrong thank you.
The script tag contains the stimulusjs code. Please I would appreciate your comments.
<script src="https://cdn.tailwindcss.com"></script>
<script type="module">
import { Application, Controller } from "https://unpkg.com/#hotwired/stimulus/dist/stimulus.js"
window.Stimulus = Application.start()
Stimulus.register("dropdown", class extends Controller {
static targets = ["background", "drop", "expand", "button"];
static values = { accordionValue: Number };
connect() {
console.log("Drop Down connected");
}
initialize() {
this.isOpen = true;
}
onToggle = (e) => {
Array.prototype.forEach.call(this.buttonTargets, function (element, index) {
element.addEventListener("click", function () {
console.log(index)
})
})
this.isOpen ? this.show() : this.hide();
this.isOpen = !this.isOpen;
};
show() {
this.dropTarget.className = "block w-full text-base font-light pt-3";
this.backgroundTarget.className = "bg-[#F0F0F0] mb-2 py-6 px-4";
this.expandTarget.innerHTML = "-";
console.log("dropdown is active");
}
hide() {
this.dropTarget.className = "hidden";
this.backgroundTarget.className = "bg-white -mb-2 w-full py-6 px-4";
this.expandTarget.innerHTML = "+";
console.log("dropdown is closed");
}
})
</script>
<div data-controller="dropdown" class="w-full flex flex-col gap-12 md:flex-row sm:max-w-[400px] md:max-w-[450px] lg:max-w-[500px] text-[#868686]">
<div class="md:min-h-[450px] w-full mt-3">
<h4 class="text-4xl font-semibold px-4 pb-3">FAQ's</h4>
<div data-dropdown-target="background" class=" py-4 px-4">
<div data-action="click->dropdown#onToggle" data-dropdown-target="button" class="cursor-pointer flex justify-between items-center">
<h5 class="text-xl font-bold">Waar hebben jullie nieuwe tuinen aangelegd? </h5><span data-dropdown-target="expand" class="font-light text-[18px] self-start">+</span>
</div>
<p data-dropdown-target="drop" class="hidden">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<div data-dropdown-target="background" class=" py-4 px-4">
<div data-action="click->dropdown#onToggle" data-dropdown-target="button" class="cursor-pointer flex justify-between items-center">
<h5 class="text-xl font-bold">Waar moet een goede hovenier aan voldoen?</h5><span data-dropdown-target="expand" class="font-light text-[18px] self-start">+</span>
</div>
<p data-dropdown-target="drop" class="hidden ">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<div data-dropdown-target="background" class="py-4 px-4">
<div data-action="click->dropdown#onToggle" data-dropdown-target="button" class="cursor-pointer flex justify-between items-center">
<h5 class="text-xl font-bold">Wat kost de aanleg van een nieuwe tuin? </h5><span data-dropdown-target="expand" class="font-light text-[18px] self-start">+</span>
</div>
<p data-dropdown-target="drop" class="hidden">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
<div data-dropdown-target="background" class="py-4 px-4">
<div data-action="click->dropdown#onToggle" data-dropdown-target="button" class="cursor-pointer flex justify-between items-center">
<h5 class="text-xl font-bold">Zijn de afspraken vrijblijvend?</h5><span data-dropdown-target="expand" class="font-light text-[18px] ml-auto self-start">+</span>
</div>
<p data-dropdown-target="drop" class="hidden">Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
</div>
</div>
</div>

Firstly - start with the HTML
The Stimulus docs provide a good guide and one of the key principles is 'Start with the HTML'.
All modern browsers support the HTML details disclosure element, and you can find really solid documentation for this on the MDN site. https://developer.mozilla.org/en-US/docs/Web/HTML/Element/details
Using Tailwind you can get pretty far but you may need to add a plugin to style against the open attribute.
For styling, see
https://css-tricks.com/two-issues-styling-the-details-element-and-how-to-solve-them/
How to target elements with a specific data attribute with Tailwind CSS?
https://tailwindcss.com/docs/adding-custom-styles
It would probably be a bit easier without Tailwind but that is up to you.
Get as close as you can without writing a single line of JavaScript, a nicely styled single accordion item that opens & closes without additional code.
The main benefits of using native HTML elements are
Less code to maintain is always better.
Forces you to think about accessibility and keyboard control instead of rolling your own 'button' like things that are not really buttons and breaking accessibility.
Might be good enough to leave as is and move on, you may not need the 'close others when one opens' feature and you can move on to other things in your project.
Secondly - fill in the styling gaps with Stimulus
You may find you have to use some JavaScript to add/remove some classes, try your best to avoid this, but if you have to your code approach above is close enough.
You may want to be more explicit about what classes to use on the element using the Stimulus classes approach https://stimulus.hotwired.dev/reference/css-classes
Finally - add accordion like behaviour
Assuming you want some kind of behaviour like the Bootstrap accordion where expanding one item will collapse any others already open.
Below is the simplest JavaScript to have an accordion controller that closes all other items (targets) when one opens.
When the method toggle is called, it reads that event's target and then works out if it is opening or closing. If it is opening, we go through all other accordion item targets and close them.
import { Controller } from '#hotwired/stimulus';
class AccordionController extends Controller {
static targets = ['item'];
toggle({ target }) {
const isOpening = target.hasAttribute('open');
if (isOpening) {
// if opening - close the others
this.itemTargets.forEach((item) => {
if (item === target) return;
item.removeAttribute('open');
});
}
}
}
export default AccordionController;
In the HTML we declare the data-controller="accordion" on the outer container.
For each details element we add two attributes:
data-accordion-target="item" - this scopes this details element to the accordion controller's this.itemTargets array.
data-action="toggle->accordion#toggle" - this adds an event listener for the build in details toggle event, note that this event does not bubble (unlike click) hence why we need it on each details element.
<section class="prose m-5" data-controller="accordion">
<h2>Multiple 'details' element into an accordion</h2>
<details
class="py-4 px-4"
data-action="toggle->accordion#toggle"
data-accordion-target="item"
>
<summary
class="flex justify-between items-center text-xl font-bold
cursor-pointer">
Section A
</summary>
<div class="bg-[#F0F0F0] mb-2 py-6 px-4">
Content
</div>
</details>
<details
class="py-4 px-4"
data-action="toggle->accordion#toggle"
data-accordion-target="item"
>
<summary
class="flex justify-between items-center text-xl font-bold
cursor-pointer">
Section B
</summary>
<div class="bg-[#F0F0F0] mb-2 py-6 px-4">
Content
</div>
</details>
<details
class="py-4 px-4"
data-action="toggle->accordion#toggle"
data-accordion-target="item"
>
<summary
class="flex justify-between items-center text-xl font-bold
cursor-pointer">
Section C
</summary>
<div class="bg-[#F0F0F0] mb-2 py-6 px-4">
Content
</div>
</details>
</section>
General tips
Never use a div for something that is clickable, it is just going to give you a hard time, always use a button with type="button".
There are some nuances with accessibility and the details element, be sure you understand these if your application needs to be AA compliant or higher.

Related

How to load props in Vue component asynchronously?

Im making a blog site and on the editPost page im trying to load data that is already in the database (sent by Laravel to Vue) and put it inside the input field so that the user can for example add/remove just one word and save it.
It seems like loading props takes some time and before that time passes , Vue renders the view and i end up with an error because im trying to use undefined variables ( not loaded yet).
I tried loading props without using them inside the script and i saw in the Vue devtools that they've been loaded.
So i need to load props asynchronously and assign them to the form that i use later.
i tried using some lifecycle hooks , watchers but i believe that my approach isnt correct.
The whole question is how to make this :
async defineProps ({ await 'post' : {
type: Object
})
Thanks in advance.
<script setup>
import {Head, useForm} from '#inertiajs/inertia-vue3';
defineProps({
'post': {
type: Object
}
})
// It brakes here
let form = useForm({
title: post.title,
description: post.description,
text: post.text
});
</script>
<Head title="Create Post"/>
<form class="ml-4 mt-4" #submit.prevent="submit">
<div>
<label for="name">Title</label>
<input type="text" class="mt-1 block w-100" v-model="form.title" required autofocus>
<label class="mt-2" :message="form.errors.title"/>
</div>
<div class="mt-4">
<label for="description">Description</label>
<input type="text" class="mt-1 block w-100" v-model="form.description" required autofocus
autocomplete="username"/>
<label class="mt-2" :message="form.errors.description"/>
</div>
<div class="mt-4">
<label for="text">Text</label>
<input type="text" class="mt-1 block w-100" v-model="form.text" required autofocus/>
<label class="mt-2" :message="form.errors.text"/>
</div>
<div class="mt-4 ">
<button class="ml-4 bg-red-300" :class="{ 'opacity-25': form.processing }" :disabled="form.processing">
Create Post
</button>
</div>
</form>
Here im sending the props from Laravel to Vue
public function edit(Post $post)
{
return Inertia::render('AuthPages/Posts/PostEdit', [
'post' => $post
]);
}
The error that occurs
Uncaught (in promise) ReferenceError: post is not defined
at setup (PostEdit.vue:13:12)
at callWithErrorHandling (runtime-core.esm-bundler.js:155:22)
at setupStatefulComponent (runtime-core.esm-bundler.js:7186:29)
at setupComponent (runtime-core.esm-bundler.js:7141:11)
at mountComponent (runtime-core.esm-bundler.js:5493:13)
at processComponent (runtime-core.esm-bundler.js:5468:17)
at patch (runtime-core.esm-bundler.js:5058:21)
at ReactiveEffect.componentUpdateFn [as fn] (runtime-core.esm-bundler.js:5680:17)
at ReactiveEffect.run (reactivity.esm-bundler.js:185:25)
at instance.update (runtime-core.esm-bundler.js:5714:56)
When i don't use props at all , just load them with defineProps , i can see that they're loaded in Vue devtools
post:Object
author_id:5
created_at:"2022-09-03T10:37:28.000000Z"
description:"Velit quo sit suscipit qui. Mollitia voluptatem ab at magni. Possimus sed maiores quia qui ullam vel. Nihil odit est sint aut non illum accusamus. Nemo eligendi sed sint exercitationem amet."
id:2
text:"Corrupti optio consectetur sit ut. Voluptatem et porro dolores et ut non. Eveniet dignissimos in enim ratione eaque. Accusantium tempore repellendus sapiente veniam accusamus ipsa."
title:"Elwyn Bernhard"
updated_at:"2022-09-03T10:37:28.000000Z"

simple Vue carousel v-for with dinamic index not working with no console errors

I'm trying to build a simple slider with Vue and dinamic data changing based on the selected index but can't get it to work.
Here's the code, I'm pretty sure there is something wrong in the way the index is selected because the looped element is not rendering if I attach the index in sliderTop[sliderIndex], but it renders all the element without it.
HTML:
<div class="slider">
<div class="prev" #click="prev">
<i class="fas fa-angle-left"></i>
</div>
<div class="first-image">
<div class="first-background" v-for="(el) in sliderTop[sliderIndex]" v-if='(el.visible === true)'>
<div class="first-content">
<h1>{{el.title}} <span>{{el.specialFont}}</span></h1>
<p>{{el.paragraph}}</p>
<button>{{el.button}}</button>
</div>
</div>
</div>
<div class="next" #click="next">
<i class="fas fa-angle-right"></i>
</div>
</div>
VUE:
sliderIndex: 0,
sliderTop: [
{
title: 'Devotion that never',
specialFont: 'ends',
paragraph: 'Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi.',
button: 'READ MORE',
visible: true,
},
{
title: 'Projects made with',
specialFont: 'love',
paragraph: 'Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi.',
button: 'READ MORE',
visible: true,
},
{
title: 'Our new folio full of',
specialFont: 'joy',
paragraph: 'Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi.',
button: 'READ MORE',
visible: true,
}
],
next: function (){
if (this.sliderIndex < 2) {
this.sliderIndex += 1;
} else {
this.sliderIndex = 0;
}
},
prev: function (){
if (this.sliderIndex > 0) {
this.sliderIndex -= 1;
} else {
this.sliderIndex = 2;
}
},
If you want to make a slider as you mentioned, you need to create a v-for for sliderTop array. Currently, you have only one slide.
You used v-for and v-if in the same div, which is not allowed.
When you use v-for with object (sliderTop[sliderIndex] is an object) "el" becomes the value. For example, in the first iteration, el is "Devotion that never", in the second iteration, it is "ends", etc.
<div class="slider">
// prev button
<div
:class="['slide', `slide-${index}`, index == sliderIndex ? 'active' : '']"
v-for="(slide, index) in sliderTop"
:key="index"
>
<h1>{{slide.title}}
<span>{{slide.specialFont}}</span></h1>
<p>{{slide.paragraph}}</p>
<button>{{slide.button}}</button>
</div>
// next button
</div>
After that, you need to change sliderIndex by prev/next buttons and change the visibility in CSS based on active class.
For example
.slider {
position: relative
}
.slide {
position: absolute
top:0
left:0
right:0
bottom:0
opacity:0
transform: translateX(-100%)
transition: all 0.5s ease-in-out
}
.slide.active {
opacity:1
transform: translateX(0)
}

Using Splittext from GSAP in Vue.js

I am very new to GSAP and Vue.js. I am actually using it for the first time.
I am trying to do a text animation with Splittext from GSAP.
This is what I did but I am getting this error : gsap__WEBPACK_IMPORTED_MODULE_2__.SplitText is not a constructor.
I really don't understand where this come from.
<script>
import gsap from 'gsap'
import {TimelineLite, SplitText} from 'gsap';
export default {
mounted(){
this.textReveal()
},
methods : {
textReveal(){
var $text = document.querySelector(".splittext"),
mySplitText = new SplitText($text, {type:"words"}),
splitTextTimeline = gsap.timeline();
gsap.set($text, {perspective:400});
mySplitText.split({type:"chars, words"});
splitTextTimeline.from(mySplitText.chars, {duration: 0.5, scale:2, autoAlpha:0, transformOrigin:"100% 50%", ease: "back.out", stagger: 0.02});
splitTextTimeline.play();
}
}
}
</script>
<template>
<div class="help-page">
<img class="close" src="#/assets/icons/close.svg" alt="">
<div class="text-container">
<h1>What to see</h1>
<p class="splittext">Lorem ipsum dolor, sit amet consectetur adipisicing elit. Earum, quos, magnam omnis perferendis laboriosam voluptatibus aperiam eligendi rerum fugit esse, ipsum vero quis reiciendis accusantium totam soluta ad. Quia tempore aliquam dolore eligendi amet, id quae sed tempora minus dignissimos deserunt corporis debitis error delectus dicta quidem, alias eaque!</p>
</div>
</div>
</template>
Thank you for your help :)
Well, Splittext is apparently not free and that is why I can't use it ...

Why my custom search filter in vue.js can't work?

I can't understand where is my fault . I try to make a custom search filter.I make a search box Where I search anythings but when it is matching in my list it gives me matching output only .But its not working .It's not look like dynamic.I am using vue 2.Hopefully I forget to add something in my computed property
<template>
<div class ="container">
<div class="new">
<form >
<h1><label>Enter country name:</label></h1>
<input type="text" name="name" class="form-control" v-model="search">
</form>
</div>
<div class='new'>
<ul>
<li v-for="country in countries">{{country.name}}
<p>Lorem ipsum, dolor sit amet consectetur adipisicing elit. Facere dignissimos architecto quia, quisquam ad similique corporis. Laborum, error id qui consequuntur facilis est delectus velit vel ea nisi repudiandae doloribus. </p>
</li>
</ul>
</div>
</div >
</template>
<script>
export default {
data(){
return {
countries:[
{name:'AMERICA'},
{name:'INDIA'},
{name:'PAKISTAN'},
{name:'SRILANKA'},
],
search:'',
}
},
computed: {
newfuntion(){
return this.countries.filter((funtion)=>{
return funtion.match(this.search)
});
}
}
};
</script>
It looks like you are not using your computed property in your v-for.
Try changing it to this: v-for="country in newfuntion"

The Movie Database popular people api with ionic 3

I'm trying to build a ionic 3 app using the movie database api.
The popular people page in my app views the list using the site's api as it would be. The problem i'm facing is when to click on the actor/actress name, the people-detail page should view everything the site has on them. But the api uses the actor/actress ID in the api. I used the person's ID as a variable and pushed it to the people-detail page (as shown below), and I retrieved it using navparams, but nothing seems to work. The browser's console shows the json contents, but I can't retrieve it in the people-detail.html.
I'm trying to find a solution to retrieve the json and view it in the people-detail.html
I need your help, thanks in advance...
note: I hid the apiKey as it's a private key.
Here is what I did to clarify:-
people.html:-
<ion-content padding>
<ion-list *ngFor="let people of peoples">
<ion-item (click)="openPeopleDetailPage(people.id)">
<ion-avatar item-start>
<img src="{{'https://image.tmdb.org/t/p/w600_and_h900_bestv2'+people.profile_path}}">
</ion-avatar>
<h2>{{people.name}}</h2>
</ion-item>
</ion-list>
</ion-content>
people.ts:-
export class PeoplePage {
constructor(public navCtrl: NavController, public navParams: NavParams, private peoplesprovider: PeoplespProvider) {
//popular peoples
this.peoplesprovider.getPeople().subscribe(data => {
console.log(data.results);
this.peoples = data.results;
});
}
ionViewDidLoad() {
console.log('ionViewDidLoad PeoplePage');
}
openPeopleDetailPage(people: any){
this.navCtrl.push(PeopleDetailPage, {people:people})
}
}
peopleProvider.ts:-
#Injectable()
export class PeoplespProvider {
people_url: string= "https://api.themoviedb.org/3/person/popular?<api_key>";
constructor(public http: HttpClient) {
console.log('Hello PeoplespProvider Provider');
}
getPeople(){
return this.http.get(this.people_url)
}
getPeopleDetail(peopleId: number){
return this.http.get('https://api.themoviedb.org/3/person/${peopleId}?
<api_key>')
}
}
people-detail.ts:-
export class PeopleDetailPage {
people: any;
detail: any;
constructor(public navCtrl: NavController, public navParams: NavParams,
private peoplesprovider: PeoplespProvider) {
this.people = this.navParams.get('people');
}
ionViewDidLoad() {
console.log('ionViewDidLoad PeopleDetailPage');
const peopleId = this.people.id;
this.peoplesprovider.getPeopleDetail(peopleId).subscribe(data => {
console.log(data);
this.people = data;
});
}
}
people-detail.html:-
<ion-content padding>
<div>
<div text-center>
<img class="people-img" src="
{{'https://image.tmdb.org/t/p/w600_and_h900_bestv2'+people?.profile_path}}">
</div>
<div>
<h1 text-center>{{people?.name}}</h1>
</div>
<div>
<ion-toolbar>
<ion-segment [(ngModel)]="controls" color="secondary">
<ion-segment-button value="info">Info</ion-segment-button>
<ion-segment-button value="movies">Movies</ion-segment-button>
<ion-segment-button value="tv-shows">TV Shows</ion-segment-button>
</ion-segment>
</ion-toolbar>
</div>
<div [ngSwitch]="controls">
<div *ngSwitchCase="'info'">
<ion-grid>
<ion-row>
<p>Born on <b>14/02/1986</b></p>
</ion-row>
<ion-row>
<p text-wrap>From <b></b></p>
</ion-row>
<ion-row>
<p>Also known as <b>Alex Daddario</b></p>
</ion-row>
</ion-grid>
<p text-wrap>Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem.</p>
</div>
<div *ngSwitchCase="'movies'">
<ion-list>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2 text-wrap>The Big Bang Theory is here</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2>Cher</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2>Cher</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
</ion-list>
</div>
<div *ngSwitchCase="'tv-shows'">
<ion-list>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2 text-wrap>The Big Bang Theory is here</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2>Cher</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
<ion-item>
<ion-thumbnail item-start>
<img src="assets/imgs/1.jpg">
</ion-thumbnail>
<p class="movie-year">2018</p>
<h2>Cher</h2>
<p class="role-name">Constance Blackwood</p>
</ion-item>
</ion-list>
</div>
</div>
In your people.html it seems that when you're opening a person's details youre pushing the person's ID but in your people-detail.ts you assigned this id to a people variable. When you're fetching the details later, you're using
Const peopleid = this.people.id
This should not be the case. Try using
Const peopleid = this.people.
In your people.html you are saying people.id;
<ion-item (click)="openPeopleDetailPage(people.id)">
But in people-detail.ts you are saying
this.people = this.navParams.get('people');
... and then
const peopleId = this.people.id;
This code is trying to access people.id.id, but that doesn't exist (I guess?).
SOLUTION
Try using this instead:
const peopleId = this.people;
This should give you the right ID. And btw, I see that you do this.people = data; in the end of your code. I would recommend you creating a different object for this data, instead of overwriting your peopleId-object. For instance, in top of you file:
peopleData: any;
And then, you just say:
this.peopleData = data;
Remember to replace all the people.-interpolations in people-detail.html to peopleData.