Accordion for a list (show and hide data when user clicks category)


#1

I have a two level list with categories. I want to load the page showing only the categories, and then when a user clicks a category, show the details of that category. This is sometimes called an “accordion”.

How can i do that with the basic example below?

I have seen a few examples online for nativescript, but the data format in those examples was different and I haven’t been able to make them work for this data. I also tried the nativescript-accordion plugin, but again the examples there are for data with a different structure, and I haven’t gotten it to work for data like I have here.

The data is like this:

countryData = [
    {category: US, people: [{name: Bill, age: 22}, {name: Suzy, age: 23} ] }, 
    {category: England, people: [{name: Sarah, age: 21}, {name: Barb, age: 23} ] },     
    {category: Canada, people: [{name: Fred, age: 30}, {name: Ted, age: 31} ] }
]

So I want to have the following displayed:

US
England
Canada

And then when a user clicks on one of these, the people associated with that category appear below.

I can display the full data with radlistview, but can’t figure out how to hide the people part until the user clicks the country. How can I do it?

In case helpful, this would be the radlistview if I was showing all the data at once:


    <RadListView [items]="countryData">
        <ng-template tkListItemTemplate let-country="item">
            <StackLayout>
                <Label [text]="country.category" "></Label>
                <StackLayout *ngFor="let person of country.people">
                        <Label [text]="person.name"></Label>
                        <Label [text]="person.age"></Label>
                </StackLayout>
            </StackLayout>
        </ng-template>
    </RadListView>

#2

I’m using repeater for this, because I can detect tap on every element not whole item tap, add tap event to country category and toggle persons visibility from it


#3

Thanks. I have seen some references here and there to repeaters, but I am not familiar with them and don’t see straightforward examples. Do you have any example code?


#4

Also open to non-repeater methods too. I have been able to get the toggle to work with simple showing and hiding with *ngIf or visibility, but the problem is when the items (people in this case) are hidden, the space they take up remains. So instead of the item space collapsing, it stays there with just white space.


#5

Can you try creating a playground example, that helps to understand what & where exactly its causing issue?


#6

I created my own using ng-content.

<StackLayout #wrapper height="24" class="vocab-item" (tap)="toggleBody(wrapper)">
<GridLayout columns="*,20" class="vocab-header">
    <ng-content select="[vocab-title]" col="0" class="vocab-header-text"></ng-content>
    <Label text="&#xea0a;" class="vocab-icon" col="1" textWrap="true" [visibility]="wrapperClosed ? 'visible' : 'collapsed'"></Label>
    <Label text="&#xea0b;" class="vocab-icon" col="1" textWrap="true" [visibility]="!wrapperClosed ? 'visible' : 'collapsed'"></Label>
</GridLayout>
    <StackLayout>
            <ng-content select="[vocab-body]" class="vocab-body"></ng-content>
    </StackLayout>
</StackLayout>
toggleBody(el: Layout) {
        if(this.wrapperClosed){
            el.height = 'auto';
            this.wrapperClosed = false;
        } else {
            el.height = 26;
            this.wrapperClosed = true;
        } 
    }

#7

Thanks. I’ll try it out. I have not used “Layout” before. Can you provide your imports in the ts file that are necessary for that?


#8

(for the toggleBody(el: Layout) I get error: “Cannot use namespace “Layout” as a type”. I think I just don’t have proper imports)


#9

I set up a basic playground example, showing an attempt at an accordion. This displays the countries, and then if you click on the country, that country is added to an array meant to tell the component to display the values. So on (tap) visibility is supposed to change to visible. Right now, though, nothing happens.

Here is the playground: https://play.nativescript.org/?template=play-ng&id=msqsiE&v=16

So its not working but I am hoping it is close. Any suggestions for how to make visible / collapse data on tap?

@bradrice’s suggestion did not quite work for me–I may not have been using “Layout” correctly, but my attempt with that suggestion yielded the same result as the code below.

FYI, the code used is:
html:

<GridLayout style="margin-top: 40px">
	<RadListView [items]="countryData">
		<ng-template tkListItemTemplate let-country="item">
			<StackLayout style="margin-bottom: 30px">
				<Label horizontalAlignment="center" [text]="country.category" (tap)="clicked(country)" style="text-align: center; font-size: 20px; width: 100%"></Label>
				<StackLayout *ngFor="let person of country.people">
					<Label [visibility]="clickedArray.includes(country) ? 'visible' : 'collapsed'" [text]="person.name "></Label>
					<Label [visibility]="clickedArray.includes(country) ? 'visible' : 'collapsed'" [text]="person.age "></Label>
				</StackLayout>
			</StackLayout>
		</ng-template>
	</RadListView>
</GridLayout>

ts:

export class HomeComponent implements OnInit {

    public countryData: Array<any> = [];

    public clickedArray: Array<any> = [];

    constructor() {
        this.countryData = [
            {
                category: 'US',
                people:
                    [{ name: 'Bill', age: 22 }, { name: 'Suzy', age: 23 }]
            },
            {
                category: 'England',
                people:
                    [{ name: 'Sarah', age: 21 }, { name: 'Barb', age: 23 }]
            },
            {
                category: 'Canada',
                people: [{ name: 'Fred', age: 30 }, { name: 'Ted', age: 31 }]
            }
        ]
    }

    ngOnInit(): void {
        console.log('loaded page')
    }

    clicked(country) {
        if (this.clickedArray.indexOf(country) != -1) {
            var index = this.clickedArray.indexOf(country)
            this.clickedArray.splice(index, 1)
        } else {
            this.clickedArray.push(country)
        }
    }

}

#10

Take a look here:

I think it is the list view with the *ngFor inside. Your logic is ok, it is the layout.


#11

Thanks. I see that its working. Seems like the big difference is not using radlistview, but using *ngFor. Do you think *ngFor performance is the same as radlistview?


#12

You might get it to work using ListView. I just noticed the layout was going off the screen so I modified it to show you it was working. Laying out the listview with the ngFor just seems complicated.


#13

What do you mean “going off the screen”? Seems like the key change was to get rid of radlistview and use *ngFor instead.


#14

well on the listview version, when I rotated my phone I saw the people after clicking, but I didnt’ see it in portrait mode. I first tried fixing the layout inside of the listview, but didn’t have luck with it, so I just wanted to quickly demonstrate your code was working. ListView may have some autosizing logic built in. I"m not really sure, but I too have had trouble with *ngFor inside of a listview.


#15

Interesting–I see what you are saying. I had not seen that before. It does seem like the RadListView does something styling wise that makes the difference. I’ll test out *ngFor for a bit without listivew and see if it is sufficient. Thanks a lot for your help.


#16

I am going to post the working code here in case others don’t have access to the playground. Thanks again, @bradrice. It seems like the key here is using *ngFor.

Using RadListView creates funky behavior where certain items are visible in portrait mode but not landscape mode. The RadListView playground that shows that is here:
https://play.nativescript.org/?template=play-ng&id=msqsiE&v=32.

This code, using *ngFor instead, seems to work:

html:

<GridLayout style="margin-top: 40px">
	<StackLayout>
			<StackLayout style="margin-bottom: 30px" *ngFor="let country of countryData">
				<Label  horizontalAlignment="center" [text]="country.category" (tap)="clicked(country)" style="text-align: center; font-size: 20px; width: 100%"></Label>
				<StackLayout horizontalAlignment="center" *ngFor="let person of country.people; let i = index">
					<Label [visibility]="clickedArray.includes(country) ? 'visible' : 'collapsed'" [text]="person.name "></Label>
					<Label horizontalAlignment="center" [visibility]="clickedArray.includes(country) ? 'visible' : 'collapsed'" [text]="person.age "></Label>
				</StackLayout>
			</StackLayout>
	</StackLayout>
</GridLayout>

ts:

export class HomeComponent implements OnInit {

    public countryData: Array<any> = [];

    public clickedArray: Array<any> = [];

    constructor() {
        this.countryData = [
            {
                category: 'US',
                people:
                    [{ name: 'Bill', age: 22 }, { name: 'Suzy', age: 23 }]
            },
            {
                category: 'England',
                people:
                    [{ name: 'Sarah', age: 21 }, { name: 'Barb', age: 23 }]
            },
            {
                category: 'Canada',
                people: [{ name: 'Fred', age: 30 }, { name: 'Ted', age: 31 }]
            }
        ]
    }

    clicked(country) {
        if (this.clickedArray.indexOf(country) != -1) {
            var index = this.clickedArray.indexOf(country)
            this.clickedArray.splice(index, 1)
        } else {
            this.clickedArray.push(country)
        }
    }

}

#17

Use collapse

Here is a simple example to show how the collapse property can be used with a tap event to create an accordion.

I have not gone to the trouble of setting up bindingContext or dynamically creating the views. But the functionality is there.

// home-page.js
var visible_people = 0;

function showItems(args) {
  const category = args.object;
  const container = category.parent;
  const people = category.getChildAt(1);

  people.visibility = people.visibility === "collapse" ? "visible" : "collapse";

  container.getChildAt(visible_people).getChildAt(1).visibility = "collapse";

  visible_people = container.getChildIndex(category);
}

exports.showItems = showItems;

// home-page.xml
<Page loaded="pageLoaded" class="page" xmlns="http://www.nativescript.org/tns.xsd">
	<DockLayout stretchLastChild="false">
		<StackLayout dock="top" tap="showItems">
			<Label text="US" class="category"/>
			<StackLayout visibility="collapse" class="people">
				<Label text="Bill, 22"/>
				<Label text="Suzy, 23"/>
			</StackLayout>
		</StackLayout>
		<StackLayout dock="top" tap="showItems">
			<Label text="England" class="category"/>
			<StackLayout visibility="collapse" class="people">
				<Label text="Sarah, 21" />
				<Label text="Barb, 23" />
			</StackLayout>
		</StackLayout>
		<StackLayout dock="top" tap="showItems">
			<Label text="Canada" class="category" />
			<StackLayout visibility="collapse" class="people">
				<Label text="Fred, 30" />
				<Label text="Ted, 31" />
			</StackLayout>
		</StackLayout>
	</DockLayout>
</Page>

// app.css
.category {
    margin: 8;
    padding: 16;
    background-color: rgb(208, 173, 255);
}

.people {
    margin: 3 16;
    padding: 6 16;
    background-color: rgb(204, 198, 255)
}

What use for collapse list or accordion (substitute)