How to ngFor an array with an object that contains a key with an array that contains objects? - angular2-template

I am trying to loop through an array that contains a key with an array with objects as the value. Here is the component that contains an array
sidemenu-links.ts
import { SideMenuLink } from './sidemenu-link';
export const SIDEMENULINKS: SideMenuLink[] = [
{
"linkTitle": "Getting Started",
"linkRoute": "introduction",
"subLinks": [
{
"linkTitle": "Introduction",
"linkRoute": "introduction"
},
{
"linkTitle": "Download",
"linkRoute": "download"
},
{
"linkTitle": "Contents",
"linkRoute": "contents"
},
{
"linkTitle": "Browser & devices",
"linkRoute": "browser-and-devices"
},
{
"linkTitle": "JavaScript",
"linkRoute": "javascript"
},
{
"linkTitle": "Theming",
"linkRoute": "theming"
},
{
"linkTitle": "Build tools",
"linkRoute": "build-tools"
},
{
"linkTitle": "Webpack",
"linkRoute": "webpack"
},
{
"linkTitle": "Accessibility",
"linkRoute": "accessibility"
}
]
},
{
"linkTitle": "Layout",
"linkRoute": "layout",
"subLinks": []
},
{
"linkTitle": "Content",
"linkRoute": "content",
"subLinks": []
},
{
"linkTitle": "Components",
"linkRoute": "components",
"subLinks": []
},
{
"linkTitle": "Utilities",
"linkRoute": "utilities",
"subLinks": []
},
{
"linkTitle": "Extend",
"linkRoute": "extend",
"subLinks": []
},
{
"linkTitle": "Migration",
"linkRoute": "migration",
"subLinks": []
},
{
"linkTitle": "About",
"linkRoute": "about",
"subLinks": []
},
]
This is the sidemenu.component
sidemenu.component.ts
import { Component, OnInit } from '#angular/core';
import { NavLink } from '../nav-link';
import { SIDEMENULINKS } from '../sidemenu-links';
#Component({
selector: 'app-sidemenu',
templateUrl: './sidemenu.component.html',
styleUrls: ['./sidemenu.component.scss']
})
export class SidemenuComponent implements OnInit {
sideMenuLinks = SIDEMENULINKS;
linkSelected: NavLink;
constructor() { }
expandLink(link:any): void {
this.linkSelected = link;
}
ngOnInit() {
}
}
Here is the sidemenu.component.html that where I am trying to use the *ngFor
sidemenu.component.html
<div class="side-menu-container #sidemenucontainer">
<ul>
<li *ngFor="let link of sideMenuLinks">
<div class="nav-link" [class.selected]="link === linkSelected" (click)="expandLink(link)">{{link.linkTitle}}</div>
<div *ngIf="link === linkSelected">
<div class="nav-link" *ngFor="let link of sideMenuLinks">{{link.subLinks.linkTitle}}</div>
</div>
</li>
</ul>
</div>
When I run it, and I click on any of the side links it opens up with the for the linktitle, but the text for the 's are all blank.

You just need to include the subLinks array in your nested *ngFor. Here is a simplified StackBlitz Demo of the code working with your object structure and html.
<div class="side-menu-container #sidemenucontainer">
<ul>
<li *ngFor="let link of sideMenuLinks">
<div class="nav-link" [class.selected]="link === linkSelected" (click)="expandLink(link)">{{link.linkTitle}}</div>
<div *ngIf="link === linkSelected">
<div class="nav-link" *ngFor="let sub of link.subLinks">{{sub.linkTitle}}</div>
</div>
</li>
</ul>
</div>

Related

Search bar filter vue 3

I just want to add a search bar filter to my vue 3 project but I don't know why is not working as good as I wanted.
Here is my code:
App.vue
<template>
<div id="app">
<div class="header">
<img class="logo" alt="UOC logo" src="./assets/uoc-logo.png" />
<div class="app-name">Recipe book</div>
</div>
<search-bar #search="setSearchTerm" />
<recipe-list :recipeList="filteredData" #delete-recipe="deleteRecipe" />
<recipe-form
v-if="showModal"
#add-recipe="addRecipe"
#close-modal="showModal = false"
/>
</div>
</template>
<script>
import RecipeList from "./components/RecipeList.vue";
import RecipeForm from "./components/RecipeForm.vue";
import SearchBar from "./components/SearchBar.vue";
import { defineComponent } from "vue";
export default defineComponent({
name: "App",
components: {
RecipeList: RecipeList,
RecipeForm,
SearchBar,
},
data: () => ({
recipeList: [
{
id: 1,
servings: 4,
time: "30m",
difficulty: "Easy",
title: "Spaghetti",
ingredients: ["noodles", "tomato sauce", "cheese"],
directions: ["boil noodles", "cook noodles", "eat noodles"],
imageUrl:
"https://imagesvc.meredithcorp.io/v3/mm/image?q=60&c=sc&poi=face&w=2000&h=1000&url=https%3A%2F%2Fstatic.onecms.io%2Fwp-content%2Fuploads%2Fsites%2F21%2F2018%2F02%2F14%2Frecetas-4115-spaghetti-boloesa-facil-2000.jpg",
},
{
id: 2,
servings: 2,
time: "15m",
difficulty: "Medium",
title: "Pizza",
ingredients: ["dough", "tomato sauce", "cheese"],
directions: ["boil dough", "cook dough", "eat pizza"],
imageUrl:
"https://www.saborusa.com/wp-content/uploads/2019/10/Animate-a-disfrutar-una-deliciosa-pizza-de-salchicha-Foto-destacada.png",
featured: true,
},
{
id: 3,
servings: 6,
time: "1h",
difficulty: "Hard",
title: "Salad",
ingredients: ["lettuce", "tomato", "cheese"],
directions: ["cut lettuce", "cut tomato", "cut cheese"],
imageUrl:
"https://www.unileverfoodsolutions.es/dam/global-ufs/mcos/SPAIN/calcmenu/recipes/ES-recipes/In-Development/american-bbq-beef-salad/main-header.jpg",
},
],
showModal: false,
recipesData: RecipeList,
filteredData: RecipeList,
}),
methods: {
deleteRecipe(id) {
this.recipeList.splice(id, 1);
},
addRecipe(recipe) {
this.recipeList.push(recipe);
},
toggleForm() {
if (this.showModal === false) {
this.showModal = true;
}
},
setSearchTerm(value) {
console.log(value);
if (value && value.length > 0) {
this.filteredData = this.recipesData.filter((i) => {
const val = value.toLowerCase();
const title = i.title && i.title.toLowerCase();
if (val && title.indexOf(val) !== -1) {
return true;
}
return false;
});
} else {
this.filteredData = this.recipesData;
}
},
},
});
</script>
SearchBar.vue
<template>
<div class="search">
<input
type="text"
placeholder="Search for a recipe"
id="search"
#change="search"
v-model="searchText"
/>
<button #click="clearSearch">Clear search</button>
<button #click="showForm">Add a new recipe</button>
</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
name: "SearchBar",
data() {
return { searchText: "" };
},
methods: {
showForm() {
console.log("show");
this.$emit("show-form");
},
clearSearch() {
this.clearInput = "";
},
search() {
this.$emit("search", this.searchText);
},
},
});
</script>
The component of search bar is working but when I acces to my website the array object is broken and I can not see any recipe. I just see recipes If I find it.
Thanks for your help

Vue.js Accordion should not close if another accordion item is clicked

I want to make an Accordion menu in vuejs. The Accordion should not close if another div is clicked. It should close when the Accordion item itself is clicked. How do I achieve this?
vue code
new Vue({
el: '#demo',
data () {
return {
isOpen: false,
selected: '',
headerDesktopMenu: {
menu: {
menu_items: [{item_name:11111, childrens: [{item_name: 11}, {item_name: 12}]},{item_name:22222, childrens: [{item_name: 21}, {item_name: 22}]},{item_name:33333, childrens: [{item_name: 31}, {item_name: 32}]},{item_name:44444, childrens: [{item_name: 41}, {item_name: 42}]}]
}
}
}
},
methods: {
toggleAccordion (item) {
item == this.selected ? this.isOpen = !this.isOpen : this.isOpen = true
this.selected = item
}
},
computed: {
accordionClasses () {
return {
'is-closed': !this.isOpen,
'is-primary': this.isOpen,
'is-dark': !this.isOpen
};
}
}
})
Vue.config.productionTip = false
Vue.config.devtools = false
HTML code
<div id="demo">
<ul class="level-0-wrp" v-if="headerDesktopMenu.menu.menu_items">
<li class="level-0" v-for="(menu, index) in headerDesktopMenu.menu.menu_items" :key="index" :class="accordionClasses" v-if="headerDesktopMenu.menu.menu_items">
<a class="title" #click="toggleAccordion(menu.item_name)">{{ menu.item_name }}</a>
<ul class="level-1-wrp" v-if="menu.childrens">
<li class="level-1" v-for="(submenuone, indexone) in menu.childrens" :key="indexone" v-if="isOpen && menu.item_name === selected">
<a class="title">{{ submenuone.item_name }}</a>
</li>
</ul>
</li>
I think you need to keep a list of which top level menu items to display or not display. Something like this.
https://codesandbox.io/s/jovial-carson-n2exv?file=/src/App.vue
<template>
<div id="app">
<ul class="level-0-wrp" v-if="headerDesktopMenu.menu.menu_items">
<li
class="level-0"
v-for="(menu, index) in headerDesktopMenu.menu.menu_items"
:key="index"
:class="accordionClasses"
>
<a class="title" #click="toggleAccordion(menu.item_name)">{{
menu.item_name
}}</a>
<ul
class="level-1-wrp"
v-if="menu.childrens && displayArray[menu.item_name]"
>
<li
class="level-1"
v-for="(submenuone, indexone) in menu.childrens"
:key="indexone"
>
<a class="title">{{ submenuone.item_name }}</a>
</li>
</ul>
</li>
</ul>
</div>
</template>
<script>
export default {
name: "App",
components: {},
data() {
return {
isOpen: false,
selected: "",
displayArray: {},
headerDesktopMenu: {
menu: {
menu_items: [
{
item_name: 11111,
childrens: [{ item_name: 11 }, { item_name: 12 }],
},
{
item_name: 22222,
childrens: [{ item_name: 21 }, { item_name: 22 }],
},
{
item_name: 33333,
childrens: [{ item_name: 31 }, { item_name: 32 }],
},
{
item_name: 44444,
childrens: [{ item_name: 41 }, { item_name: 42 }],
},
],
},
},
};
},
methods: {
toggleAccordion(item) {
if (this.displayArray[item] === undefined) {
this.$set(this.displayArray, item, true);
} else {
this.$set(this.displayArray, item, !this.displayArray[item]);
}
},
},
computed: {
accordionClasses() {
return {
"is-closed": !this.isOpen,
"is-primary": this.isOpen,
"is-dark": !this.isOpen,
};
},
},
};
</script>

Vuejs get dynamic form values

I have select and input component with made by buefy. Everything is ok till I realize how can I get the data.
I'm sort of new on vuejs. So I will be glad if you help me out.
I'm getting dynamic form from backend
So my question is how can get values these inputs and submit to backend again with getOffer() methot.
Here is my codes;
Input.vue
<template>
<b-field :label="fieldLabel">
<b-input
:name="inputName"
:type="inputType"
:maxlength="inputType == 'textarea' ? 200 : null"
></b-input>
</b-field>
</template>
<script>
export default {
name: "Input",
props: {
inputType: {
type: String,
required: true,
default: "text",
},
inputName: {
type: String,
required: true,
},
fieldLabel: {
type: String,
required: true,
}
}
};
</script>
Home.vue
<template>
<div class="container is-max-desktop wrapper">
<div v-for="element in offer" :key="element.id">
<Input
v-model="element.fieldValue"
:value="element.fieldValue"
:fieldLabel="element.fieldLabel"
:inputType="element.fieldType"
:inputName="element.fieldName"
v-if="element.fieldType != 'select'"
class="mb-3"
/>
<Select
v-model="element.fieldValue"
:fieldLabel="element.fieldLabel"
:options="element.infoRequestFormOptions"
:selectName="element.fieldName"
v-if="element.fieldType == 'select'"
class="mb-3"
/>
</div>
<b-button type="is-danger" #click="getOffer()">GET</b-button>
</div>
</template>
<script>
import axios from "axios";
import Select from "../components/Select.vue";
import Input from "../components/Input.vue";
export default {
name: "Home",
data() {
return {
offer: [],
};
},
components: {
Select,
Input,
},
methods: {
getOfferForm() {
axios({
method: "get",
url: `/GETDYNAMICFORM`,
})
.then((response) => {
this.offer = response.data;
})
.catch(() => {
this.$buefy.toast.open({
duration: 3000,
message: "oops",
position: "is-bottom",
type: "is-danger",
});
});
},
getOffer() {
console.log(this.offer);
},
},
created() {
this.getOfferForm();
},
};
</script>
Example Dynamic Form Response like;
[
{
"id": 58,
"fieldLabel": "Name Surname",
"providerLabel": "Name Surname",
"fieldName": "nmsrnm",
"fieldType": "text",
"fieldValue": null,
},
{
"id": 60,
"fieldLabel": "E-mail",
"providerLabel": "E-mail",
"fieldName": "e_mail_60",
"fieldType": "email",
"fieldValue": null,
},
{
"id": 2,
"fieldLabel": "Budget",
"providerLabel": "Budget",
"fieldName": "bdget",
"fieldType": "select",
"fieldValue": "",
"infoRequestFormOptions": [
{
"id": 1,
"orderNum": 0,
"optionValue": 0,
"optionText": "Select",
"minValue": null,
"maxValue": null
},
{
"id": 2,
"orderNum": 1,
"optionValue": 1,
"optionText": "10-30",
"minValue": 10,
"maxValue": 30
}
]
}
]

Vue URL's not working when navigating directly to a page

I have a series of products, and I'm trying to set my app up in a way that let's me send someone a link directly to a product.
Everything works fine when you try to navigate to a product directly, but if you open that same url directly (without navigating there through the app), it doesn't work.
The issue is coming from subcategoryItems being undefined in the single item view
Router snippet:
{
path: '/categories',
name: 'categories',
components: { default: Categories, header: StarterNavbar, footer: StarterFooter },
props: {
header: { colorOnScroll: 400 },
footer: { backgroundColor: 'black' }
}
},
{
path: '/categories/:catname',
name: 'products',
components: { default: Products, header: StarterNavbar, footer: StarterFooter },
props: {
header: { colorOnScroll: 400 },
footer: { backgroundColor: 'black' }
}
},
{
path: '/categories/:catname/:productname',
name: 'singleproduct',
components: { default: SingleProduct, header: StarterNavbar, footer: StarterFooter },
props: {
header: { colorOnScroll: 400 },
footer: { backgroundColor: 'black' }
}
},
Product View
<template>
<div class="">
<section class="subcategory-container" v-for="(category, index) in subcats" v-bind:key="index">
<h2>{{category.subcategoryTitle}}</h2>
<card class="card-shell" v-for="(product, index) in category.subcategoryItems" v-bind:key="index">
<div class="image-container">
<img slot="image" class="card-img-top" :src="product.image" alt="Card image cap">
</div>
<div>
<h4 class="card-title">{{product.title}}</h4>
<p class="card-text">{{product.sku}}</p>
<div>
<router-link :to="{ name: 'singleproduct', params: { productname: product.title, subcatTitle: category.subcategoryTitle } }" class="text-white">
<n-button type="primary">{{buttonText}}</n-button>
</router-link>
</div>
</div>
</card>
</section>
</div>
</template>
<script>
import { Card, Button, Modal } from '#/components';
import axios from 'axios'
export default {
name: 'products',
components: {
Card,
Modal,
[Button.name]: Button
},
async created() {
const url = this.$route.params.catname;
try {
const res = await axios.get(`/products/${url}.json`);
this.subcats = res.data;
this.catname = url;
} catch (e) {
console.log(e);
}
},
data() {
return {
subcats: [],
modals: {
classic: false
},
showModal(product) {
this.modals.classic = true;
this.selectedItem = product;
},
buttonText: "Product Info",
selectedItem: '',
catname: ''
}
}
};
single item view:
<template>
<card class="card-nav-tabs text-center" header-classes="card-header-warning">
<div slot="header" class="mt-2">
<img src="" alt="">
</div>
<h4 class="card-title">{{product.title}}</h4>
<p class="card-text">{{product.description}}</p>
<div slot="footer" class="card-footer text-muted mb-2">
{{product.sku}}
</div>
</card>
</template>
<script>
import { Card, Button } from '#/components';
import axios from 'axios';
import { async } from 'q';
export default {
name: 'singleproduct',
components: {
Card,
[Button.name]: Button
},
async created() {
const { catname, productname, subcatTitle } = this.$route.params;
//console.log(this.$route.params)
try {
const res = await axios.get(`/products/${catname}.json`);
const data = res.data;
const items = data.find(product => product.subcategoryTitle === subcatTitle).subcategoryItems;
const item = items.find(item => item.title === productname);
console.log(item);
this.product = item;
} catch (e) {
console.log(e);
}
},
data () {
return {
product: []
}
}
}
</script>
Json sample:
[
{
"subcategoryTitle": "sub cat title 1",
"subcategoryItems": [
{
"id": 1,
"title": "name 1",
"sku": "sku 1",
"image": "img/path to image",
"description": "Cream beans medium rich breve cinnamon latte. White pumpkin spice kopi-luwak sugar foam frappuccino dark. Brewed arabica, dripper arabica as milk turkish medium."
}
]
},
{
"subcategoryTitle": "sub cat title 2",
"subcategoryItems": [
{
"id": 1,
"title": "name 2",
"sku": "sku 2",
"image": "img/path to image"
"description": "Cream beans medium rich breve cinnamon latte. White pumpkin spice kopi-luwak sugar foam frappuccino dark. Brewed arabica, dripper arabica as milk turkish medium."
},
{
"id": 2,
"title": "name 2",
"sku": "sku 2",
"image": "img/path to image",
"description": "Cream beans medium rich breve cinnamon latte. White pumpkin spice kopi-luwak sugar foam frappuccino dark. Brewed arabica, dripper arabica as milk turkish medium."
}
]
}
]
Thank you
This requires history mode enabled in the router. The router documentation has an explanation and example for this:
https://router.vuejs.org/guide/essentials/history-mode.html
If you look at the code I posted, the issue was that I was missing a slug in the URL for the final product page.
When I navigated there through the app, the product info was all there, but on reload, it didn't match the route anymore, so the details vanished.

Going back to previous object in array with back button in v-for

I have a component with a v-for div. each item has a click function access their respective children object. I need to have a back button that would refresh the v-for div but using the ParentId of the current item I'm in.
Scan view:
<template>
<div p-0 m-0>
<div v-show="!currentItem" class="scanBreadcrumbs">
<h2>Show location</h2>
</div>
<div v-for="item in items" :key="item.id" :item="item">
<SubScan
v-show="currentItem && currentItem.id === item.id"
:item="item"
></SubScan>
<p
class="locationBox"
#click="swapComponent(item)"
v-show="path.length === 0"
>
{{ item.title }}
</p>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
import { SubScan } from "#/components/scan";
export default {
name: "Scan",
components: {
SubScan
},
computed: {
...mapGetters(["getResourceHierarchy", "getIsDarkMode", "getFontSize"])
},
methods: {
swapComponent(item) {
this.path.push(item.title);
this.currentItem = item;
}
},
data: () => ({
currentItem: null,
path: [],
items: [
{
parentId: null,
id: 11,
title: "Location 1",
items: [
{
parentId: 11,
id: 4324,
title: "Row 1",
items: [
{
parentId: 4324,
id: 4355,
title: "Row 1.1",
items: [
{
parentId: 4355,
id: 64645,
title: "Row 1.2",
items: [
{
parentId: 64645,
id: 7576657,
title: "Row 1.3",
items: [
{
parentId: 7576657
id: 8686,
title: "Row 1.4",
items: [
{
parentId: 8686,
id: 234324,
title: "QR Code"
}
]
}
]
}
]
}
]
}
]
}
]
}
]
})
};
</script>
SubScan component where the back button is:
<template>
<div>
<div class="scanBreadcrumbs">
<h2 v-show="path">{{ path.join(" / ") }}</h2>
</div>
<div>
<div class="showList" v-for="item in itemChildren" :key="item.id">
<p class="locationBox" #click="swapComponent(item)">
{{ item.title }}
</p>
<div class="backButton">
<v-icon #click="swapPrevious(item)" class="arrow"
>fa-arrow-left</v-icon
>
</div>
</div>
</div>
</div>
</template>
<script>
export default {
name: "SubScan",
props: {
item: {
type: Object,
required: true
}
},
data: () => ({
currentItem: null,
secondaryPath: [],
parentPath: []
}),
methods: {
swapComponent(item) {
console.log(item.parentId);
this.path.push(item.title);
this.parentPath.push(this.currentItem);
this.currentItem = item;
},
swapPrevious(item) {
console.log(item);
this.path.pop(this.currentItem.title);
this.currentItem = item.id;
}
},
computed: {
items(currentItem) {
return this.currentItem ? this.item.items : this.item.items;
},
itemChildren(currentItem) {
return this.currentItem ? this.currentItem.items : this.item.items;
},
path() {
return this.secondaryPath.concat(this.item.title);
}
}
};
</script>
I can only go back to the children of the object I clicked on in Scan view.
Thank you for your time.
I managed to fix my problem by assigning parent objects to each children. Then I
moved everything to Scan.vue for simplicity. This is my first project using Vue
so things might not be optimal. Scan.vue
<template>
<div p-0 m-0>
<div class="scanBreadcrumbs">
<h2 v-show="path">{{ path.join("/") }}</h2>
<h2 v-if="path.length === 0">Show location</h2>
</div>
<div>
<div v-for="item in items">
<p class="locationBox" #click="swapComponent(item)">
{{ item.title }}
</p>
</div>
<div v-if="path.length > 0">
<div class="backButton">
<v-icon #click="swapPrevious()" class="arrow">fa-arrow-left</v-icon>
</div>
</div>
</div>
</div>
</template>
<script>
import { mapGetters } from "vuex";
export default {
name: "Scan",
computed: {
...mapGetters(["getResourceHierarchy", "getIsDarkMode", "getFontSize"])
},
methods: {
swapComponent(item) {
this.path.push(item.title);
this.currentItem = item;
this.items = this.currentItem.items;
},
assignParent(children, parent) {
children.forEach(item => {
item.Parent = parent;
var parentTitle = "";
if (parent) parentTitle = parent.title;
if (item.items) {
this.assignParent(item.items, item);
}
});
},
swapPrevious() {
if (this.currentItem.parentId === null) {
this.items = this.initialItems;
this.path.pop(this.currentItem.title);
} else {
this.currentItem = this.currentItem.Parent;
this.items = this.currentItem.items;
this.path.pop(this.currentItem.title);
}
}
},
mounted: function() {
this.assignParent(this.items, null);
this.initialItems = this.items;
},
data: () => ({
currentItem: null,
path: [],
initialItems: [],
items: [
{
parentId: null,
id: 11,
title: "Location 1",
items: [
{
parentId: 11,
id: 4324,
title: "Row 1",
items: [
{
parentId: 4324,
id: 4355,
title: "Row 1.1",
items: [
{
parentId: 4355,
id: 64646,
title: "Row 1.2",
items: [
{
parentId: 64646,
id: 7576657,
title: "Row 1.3",
items: [
{
parentId: 7576657,
id: 8686,
title: "Row 1.4",
items: [
{
parentId: 8686,
id: 12313,
title: "Row 1.5",
items: [
{
parentId: 12313,
id: 234324,
title: "QR Code"
}
]
}
]
}
]
}
]
}
]
}
]
}
]
}
]
})
};
</script>
<style lang="scss" scoped></style>