I have array of items which are rendered on UI as angular material cards. I have a search box where user input for items. once the search is implemented I get the searchItems as another array. Now I want to highlight (changing the background color of the card or a rectangular animation) items array angular material cards which are matched with searched items. I was stuck at implementing this css part although I am able to render and match the items with searchdItems
<div *ngIf="searchedItems">
<div class="alert alert-danger alert-dismissible" *ngIf="searchedItems.length === 0">
×
<strong>{{data.value}}</strong> not found
</div>
<div *ngIf="searchedItems.length > 0" class='searchitem'>
{{data.value}} found in
<div *ngFor="let item of searchedItems; let i = index">
{{item}} {{i}}
</div>
</div>
</div>
<mat-grid-list cols="3" rowHeight="100px">
<div *ngFor="let item of items; let x = index">
<mat-grid-tile [ngClass]="item.name == item ? 'searchexample-card': 'example-card'" routerLink="/inventory/items/{{item.name}}">
<mat-card>
<mat-card-header>
<b>Item{{item.name}}</b>
</mat-card-header>
</mat-card>
</mat-grid-tile>
</div>
</mat-grid-list>
If you are unable to change the items array I have modified the answer for the pipe which will not impact your original array
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'highlight', pure: false
})
export class HighlightPipe implements PipeTransform {
transform(items: any, filtered?: any[]): any {
let newArray = [];
if(Array.isArray(items) && Array.isArray(filtered)) {
for(let item of items){
newArray.push({label: item, highlight: filtered.indexOf(item)>-1})
}
} else {
newArray = items.map(item => ({label: item}));
}
console.log(newArray)
return newArray;
}
}
But your html template will be modified to accommodate the highlight
<div *ngFor="let item of items | highlight: searchedItems" [ngClass]="{card: true, highlight: item.highlight}">
{{item.label}}
</div>
Here I am passing the array that you have after you search searchedItems to my highlight Pipe
Hope this answers your question
Updated Stackblitz: https://stackblitz.com/edit/angular-oyhva7
The problem is with the condition that you have implemented for activating [ngClass]. You are comparing an object with object property, this comparison for item with item.name will always return false.
consider you have the searched result in searchedItems[] array. Now you want to highlight those items in the loop of whole items. then your code should be something like this
<div *ngFor="let item of items; let x = index">
<mat-grid-tile [ngClass]="item in searchedItems ? 'searchexample-card': 'example-card'">
<mat-card>
<mat-card-header>
<b>Item{{item.name}}</b>
</mat-card-header>
</mat-card>
</mat-grid-tile>
</div>
Considering your items array and searchedItems[] is of same type.
You can do it using a pipe from Angular
import { Pipe, PipeTransform } from '#angular/core';
#Pipe({
name: 'highlight',
pure: false
})
export class HighlightPipe implements PipeTransform {
transform(items: any, search: string, clip: boolean): any {
const regex = new RegExp(search, 'ig');
if(Array.isArray(items) && search) {
for(let item of items) {
item.highlight = regex.test(item.label);
}
}
return clip ? items.filter(item => item.highlight) : items;
}
}
I have passed in the search term in the pipe and the full array always returning from the pipe, but using a regular expression or maybe your custom logic of search add another attribute to the list item highlight boolean which is true if your logic says it matches the item, else false
I have modified the items array from string[] to {label:string, highlight?:boolean}[] to accommodate the pipe change so instead we are using the array as
items: {label:string, highlight?:boolean}[] = [
{label: 'INDIA'},
{label: 'USA'},
{label: 'RUSSIA'},
{label: 'UK'},
{label: 'ITALY'},
];
Now to all put it into the template
<div
*ngFor="let item of items | highlight: search"
[ngClass]="{highlight: item.highlight, card: true}"
>
{{item.label}}
</div>
Here when you search, if it matches with any of the item from items array will have a highlight:true attribute as true and also adds a class called 'highlight` to your existing div.card so in css defining the highlight card as
.card.highlight{
background-color: yellow;
}
Notice that we are sending the search term to the pipe that we created as an argument, so we have to define the variable as a class level variable which is set when we click search in your method devicesearch which you are already calling.
search: string;
devicesearch(input) {
this.search = input;
}
Edit 2:
If you wanted the clipped array you can use the Pipe in your class to get the clipped array
search: string;
searchedItems: any[];
highlightPipe = new HighlightPipe();
devicesearch(input) {
this.search = input;
this.searchedItems = this.highlightPipe.transform(this.items, input, true);
// only names --> const names = this.searchedItems.map(item => item.label);
}
Updated StackBlitz for your reference: https://stackblitz.com/edit/angular-usswkn
Related
I have a dynamic table. Table and table columns are separated components. And I am using columns as slot in table components.
main file
<DynamicTable :contents="contents">
// columns finding and displaying data from props dynamically. it works without a problem
<Col th="name" td="name"/>
<Col th="information" td="information"/>
// here I am giving custom slot inside last column, but button not displaying additionally I can't reach the content data via slotProps //
<Col>
<template #content="slotProps">
<button #click="getData(slotProps.content)">click me</button>
</template>
</Col>
</DynamicTable>
dynamicTable component
<slot></slot>
<div v-for="content in contents" :key="content.id">
<div v-for="(td, idx) in body" :key="idx">
// if there is slot given as template render <slot>
<slot v-if="td.children" name="content" :content=content></slot>
// else render dynamic value
<span v-else>{{ content[td.name] }}</span>
</div>
</div>
const slots = useSlots() ? useSlots().default : null;
const body = ref([])
slots().forEach(slot => {
body.value.push({
children: slot.children,
name: slot.props?.td,
})
})
additionally Col template
<script>
export default {
props: {
th: {
type: String,
default: null
},
td: {
type: String,
default: null
},
},
render(){
return null
}
}
</script>
In a situation like above, how can I display button element inside given <Col> component as slot and getting the :content="content" data as slotProps.
And if you need to know, content Array looks like below.
const contents = ref([
{
id: 341,
order_id: 1,
name: "susan",
information: "inf context",
},
{
id: 453,
order_id: 2,
name: "jack",
information: "info context",
}
])
Solved
You're trying to access slotProps in Col component,
But the Col component is not passing any props.
The required data is available in DynamicTable component
And you're already passing it to the slot of DynamicTable
You can access the slotProps as below
//App.vue
<DynamicTable :contents="contents" v-slot="slotProps">
<Col th="name" td="name" />
<Col>
Access slotProps directly, no need to use template
{{slotProps.content}}
</Col>
</DynamicTable>
And you need to remove slot name
//DynamicTable.vue
<slot v-if="td.children" :content="content"></slot>
Also refactor the Col component
//Col.vue
/* remove render function */
...
<template>
<slot />
</template>
Solution
You can find a working demo below.
https://stackblitz.com/edit/vue-cntmmb?file=src/App.vue
I have a GET request in my home.vue component.
This query allows me to get an array of objects.
To display all the objects, I do a v-for loop and everything works fine.
<div class="commentaires" v-for="(com, index) of coms" :key="index">
My concern is that I want to display an image by clicking on it (coms[index].imageUrl), in a modal (popup).
The modal is displayed fine but not with the correct image, i.e. the modal displays the last image obtained in the loop, which is not correct.
Here is the full code of my home.vue component
<template>
<div class="container">
<div class="commentaires" v-for="(com, index) of coms" :key="index">
<modale :imageUrl="com.imageUrl_$this.index" :revele="revele" :toggleModale="toggleModale"></modale>
<img class="photo" :src=""" alt="image du commentaire" #click="toggleModale">
</div>
</div>
</template>
<script>
//import axios from "axios";
import axios from "axios";
import Modale from "./Modale";
export default {
name: 'HoMe',
data() {
return {
coms: [],
revele: false
}
},
components: {
modale: Modale
},
methods: {
toggleModale: function () {
this.revele = !this.revele;
},
</script>
Here is my modale.vue component
<template>
<div class="bloc-modale" v-if="revele">
<div class="overlay" #click="toggleModale"></div>
<div class="modale card">
<div v-on:click="toggleModale" class="btn-modale btn btn-danger">X</div>
<img :src=""" alt="image du commentaire" id="modal">
</div>
</div>
</template>
<script>
export default {
name: "Modale",
props: ["revele", "toggleModale", "imageUrl"],
};
</script>
I've been working on it for 1 week but I can't, so thank you very much for your help...
in your v-for loop you're binding the same revele and toggleModale to every modal. When there is only one revele then any time it's true, all modals will be displayed. It's therefore likely you're actually opening all modals and simply seeing the last one in the stack. You should modify coms so that each item has it's own revele, e.g.:
coms = [
{
imageUrl: 'asdf',
revele: false
},
{
imageUrl: 'zxcv',
revele: false
},
{
imageUrl: 'ghjk',
revele: false
}
];
then inside your v-for:
<modale
:image-url="com.imageUrl"
:revele="com.revele"
#toggle-modale="com.revele = false"
></modale>
<img class="photo" :src=""" alt="image du commentaire" #click="com.revele = true">
passing the same function as a prop to each modal to control the value of revele is also a bad idea. Anytime a prop value needs to be modified in a child component, the child should emit an event telling the parent to modify the value. Notice in my code snippet above I replaced the prop with an event handler that turns the revele value specific to that modal to false. Inside each modal you should fire that event:
modale.vue
<div class="btn-modale btn btn-danger" #click="$emit('toggle-modale')">
X
</div>
This way you don't need any function at all to control the display of the modals.
With the help of Lit library we have implemented the component that should render the list with items, where each item is rendered with a separate component:
<div>
<slot name="label"> Here goes the title </slot>
<slot name="list"></slot>
</div>
We pass data to the component like following:
<webc-list ?divided=${true}>
<span slot="label">Title</span>
<ul slot="list">
${items.map(
item =>
html`<webc-list-item
>${item}</webc-list-item
>`,
)}
</ul>
</webc-list>
My question is how can I pass the divided property to the <webc-list-item>.
I tried to access the elements
firstUpdated() {
const dividedProperty = this.divided;
this.renderRoot.querySelector('slot[name=list]')?.assignedElements({ flatten: true })
?.forEach(el => {
if (el && el.tagName && el.tagName.toLowerCase().includes('webc-list-item')) {
el.setAttribute('divided', `${dividedProperty}`);
}
});
But it doesn't work like this, any help would be appreciated!
In vue3 I am passing an array of options from parent component to child component in order to use it as options for a select.
At the moment, I am not able to use it to initialize my select.
Here is the child component SmartNumberInput
<template>
<div>
<div>Selected: {{ selected }} Initial:{{ initial }}</div>
{{ options }}
<div v-for="option in options" :key="option.value">
{{ option.text }}
</div>
<input type="number" v-model="input_value" />
<select v-model="selected">
<option
v-for="option in options"
:value="option.value"
:key="option.value"
>
{{ option.text }}
</option>
</select>
</div>
</template>
<script>
export default {
props: ["initial", "options"],
data() {
return {
selected: "",
input_value: "",
};
},
};
</script>
Here is the parent component
<template>
<div>
<h1>Hi! I am the root component</h1>
<div class="smart-number-input">
<smart-number-input
initial="B"
options="[{text:'Liters',value:'A'},{text:'Gallons',value:'B'},{text:'Pints',value:'C'}]"
/>
</div>
</div>
</template>
<script>
import SmartNumberInput from "./SmartNumberInput.vue";
export default {
data() {
return {
initial: "salut",
};
},
components: { SmartNumberInput },
};
</script>
<style>
.smart-number-input {
width: 100%;
background:#EEE;
}
</style>
In the result I get (see picture) there is no option visible in the select though when the small arrow is clicked it expands with a long empty list.
The {{options}} statement in the child displays what I pass as prop i.e. an array of objects but nothing is displayed in the div where I use a v-for loop.
When I declare the options as data in the child both loops (div and select) work fine.
Could somebody explain what is wrong in the way I pass or use the array of options ?
change options to :options (add colon symbol)
.
if you not put colon, it will treat the value as a String...
I'm getting starting with Vue.js and I have a simple page set up to experiment with conditional css.
<div id="app">
<div class="demo" #click="handleClick(0)" :class="{ 'red': attachRed[0] }">
</div>
<div class="demo" #click="handleClick(1)" :class="{ 'red': attachRed[1] }">
</div>
<div class="demo" #click="handleClick(2)" :class="{ 'red': attachRed[2] }">
</div>
</div>
and then my js
new Vue({
el: "#app",
data: {
attachRed: [false, false, false]
},
methods: {
handleClick: function(index) {
this.attachRed[index] = !this.attachRed[index];
console.log(this.attachRed)
}
}
});
Each div is a grey block. When attaching the "red" class, the block turns red. attachRed array is updating every time a demo div is clicked. But the class is never added. If I start the attachRed property off as being true, then the red class is attached initially, but this isn't toggled when clicked. This works if these values aren't stored in an array though.
Is it possible to make the view bindings watch for these changes or to manually trigger one? Or is there some sort of gotcha when it comes to array properties?
It is a gotcha. This page goes into it a bit: https://vuejs.org/2016/02/06/common-gotchas/
In short, you want to do
var val = this.attachedRed[index]
this.attachedRed.$set(index, !val);