Stencil component not rendering the updated tabs - stenciljs

import { Component, h, State } from '#stencil/core';
// import '#webcomponents/custom-elements';
import '#clr/core/icon/register';
import { ClarityIcons, plusIcon } from '#clr/core/icon';
ClarityIcons.addIcons(plusIcon);
#Component({
tag: 'tabs-component',
styleUrl: 'tabs-component.css',
shadow: false,
})
export class TabsComponent {
#State() tabs: Array<object> = [
(
<li role="presentation" class="nav-item">
<button id="tab3" class="btn btn-link nav-link" aria-controls="panel3"
aria-selected="false" type="button">Cloud</button>
</li>
)
];
addTab(onHead = true) {
// debugger
const tab = (
<li role="presentation" class="nav-item">
<button id="tab3" class="btn btn-link nav-link" aria-controls="panel3"
aria-selected="false" type="button">Dashboard</button>
</li>
);
if (onHead) {
this.tabs.unshift(tab);
} else {
this.tabs.push(tab);
}
console.log(this.tabs);
}
render() {
return (
<div>
<ul id="demoTabs" class="nav" role="tablist">
<li role="presentation" class="nav-item" onClick={() => this.addTab()}>
<cds-icon shape="plus" class="cursor"></cds-icon>
</li>
{this.tabs}
</ul>
<section id="panel1" role="tabpanel" aria-labelledby="tab1">
tab1
</section>
<section id="panel2" role="tabpanel" aria-labelledby="tab2" aria-hidden="true">
tab2
</section>
<section id="panel3" role="tabpanel" aria-labelledby="tab3" aria-hidden="true">
tab3
</section>
</div>
);
}
}

This is a matter of referential equality. Objects are always passed by reference not by value and therefore two objects are never equal (reference-wise, even if they contain the same values).
The array is a special kind of object and therefore is also passed by reference. Modifying an array's value does not change its reference.
Some examples to illustrate this:
const foo = ['a', 'b'];
console.log(foo === ['a', 'b', 'c']); // false
foo.push('c');
console.log(foo === ['a', 'b', 'c']); // still false
console.log(['a', 'b', 'c'] === ['a', 'b', 'c']); // actually always false
console.log(foo === foo); // always true because it is the same reference
Stencil compares #State() decorated class members using the same strict equality operator === (same goes for #Prop()). If the value is the same, then the component is not re-rendered.
In the case of your tabs state, the value of this.tabs is a reference to the array that you assign to it. Modifying the array (e. g. this.tabs.push(...)) only changes the value of the array referenced by this.tabs but not the actual reference that is stored in this.tabs.
Therefore you need to re-assign this.tabs in order to let Stencil know that this member has changed. The easiest way to do that is
this.tabs = [...this.tabs];
which spreads the values of the array into a new array (which returns a new reference). Alternatively something like this.tabs = this.tabs.slice() would also do the trick (anything that returns a new array works).
In your case it's easiest to change your addTab method to
addTab(onHead = true) {
const tab = (
<li role="presentation" class="nav-item">
<button id="tab3" class="btn btn-link nav-link" aria-controls="panel3"
aria-selected="false" type="button">Dashboard</button>
</li>
);
this.tabs = onHead ? [tab, ...this.tabs] : [...this.tabs, tab];
}
(i. e. either spread the original value before or after the new item).

Stencil performs strict equality checks (===) to determine whether a Prop/State variable has changed which is why it doesn't detect push and unshift as changes. You have to make sure to replace the array with a new one. The quickest way to do this in your example is to manually replace the array with a copy after the manipulation:
if (onHead) {
this.tabs.unshift(tab);
} else {
this.tabs.push(tab);
}
this.tabs = [...this.tabs];
See the Stencil docs for updating arrays.

Look like temp variable works the trick, strange.
const tempTabs = [...this.tabs];
if (onHead) {
tempTabs.unshift(tab);
} else {
tempTabs.push(tab);
}
this.tabs = tempTabs;

Related

View binded :key values in Vue3

<li v-for="(size, index) in sizes" :key="index">{{ size }}</li>
I'm new to VueJS and I'm playing around with Vue Directives. I wanna know where to get the list of :key values in the console log or developer tools. For now, I'm setting it to id attribute and reading it. Appreciate any kind of help
If you're just playing around with it and want to be able to log it to console, you could add a log function to your methods
methods:{
log(..args){
console.log(...args)
}
}
then you can use the log function anywhere and pass it the same value
<li v-for="(size, index) in sizes" :key="index">{{ size }}{{log(index)}}</li>
...but that only works if you can pass the same value to both
Example:
Vue.createApp({
data: () => ({
items: ['a', 'b', 'c']
}),
methods: {
log(...args) {
console.log(...args)
},
},
}).mount("#app");
<script src="https://unpkg.com/vue#3.2.0/dist/vue.global.prod.js"></script>
<div id="app">
<li v-for="(item, index) in items" :key="index">{{ item }}{{log(index)}}</li>
</div>

Vue: Adding class to one item in v-for

I have a list of items in a v-for loop. I have a function on #click that will delete the item but I want to give it a class when I click it to change the background color for a short time so the user knows they clicked that item and it's deleting. In my deleteItem function, I set deletingItem to true but that obviously creates an issue because it will apply that class to all the divs in the in v-for. What is the best way to solve it so that it only gets applied to the div I click on?
<div :class="{'box': true, 'deleting-item': deletingItem}" v-for="(item,index) in items">
<div #click="deleteItem(item,index)>Delete</div>
</div>
You need to save the clicked item in a data property
<template>
<div>
<div v-for="(item, index) in items">
<button #click="deleteItem(index)" :class="{'delete-in-progress-class': index === indexOfDeletedItem}> {{item}} </button>
</div>
</div>
</template>
<script>
export default {
data() {
return {
items: ['a', 'b', 'c']
indexOfDeletedItem: null
}
},
methods: {
deleteItem(index) {
this.indexOfDeletedItem = index;
setTimeout(() => { this.items.splice(index, 1); this.indexOfDeletedItem = null }, 1000); //delete item after 1s
}
}
}
</script>
<style>
.delete-in-progress-class {
background: red;
}
</style>
Obviously the solution above is naive. It'll probably go crazy if the user wants to delete many items in a short time, since the indices of an array shift around when you delete something.
But hopefully it'll give you an idea of how to conditionally apply styles in a list.

Show on several elements in the same component in vuejs

Looping out a number of boxes within the same component in vuejs.
Each box has a button that reveals more text using v-on:click.
The problem is that all the boxes respond to this at the same time.
Is there a way to target the specific button being clicked if there are several buttons in a component?
Is there some way to isolate each button so they all dont activate at once?
<div class="filter-list-area">
<button #click="show =!show"> {{text}} </button>
<ul class="filter-list-item-area">
<li class="filter-list-item " v-for="(items, key) in packages">
<div>
<img class="fit_rating">
</div>
<div class="filter-list-item-info" >
<h3>{{items.partner_university}}</h3>
<p> Match: {{items.match_value}}</p>
<div v-for="(courses, key) in courses">
<transition name="courses">
<div class="courses2" v-show="show">
<p v-if="courses.Pu_Id === items.pu_Id">
{{courses.Course_name}}
</p>
</div>
</transition>
</div>
</div>
</li>
</ul>
</div>
</template>
<script>
import testdata from '../App.vue'
export default {
data (){
return{
text: 'Show Courses',
testFilter: 'Sweden',
show: false
}
},
props: {
title: String,
likes: Number,
isPublished: Boolean,
commentIds: Array,
author: Object,
testuni: Array,
list: Array,
packages: Array,
courses: Array
},
methods:{
afunction(){
console.log(this.show);
},
changeText(){
if(this.show){
this.text = 'Hide Courses'
}
else{
this.text = "Show Courses"
}
}
},
mounted() {
this.afunction();
},
watch: {
show:
function() {
this.afunction()
this.changeText()
}
},
}
EDIT: I've created this before you posted the code example, but you could use same principle:
In your data add showMoreText, which will be used to track if show more data should be shown.
I would agree with #anaximander that you should use child components here
Simple example how to show/hide
<template>
<div>
<div v-for="(box, index) in [1,2,3,4,5]">
<div>
Box {{ box }} <button #click="toggleMore(index)">More</button>
</div>
<div v-show="showMoreText[index]">
More about box {{ box }}
</div>
</div>
</div>
</template>
<script>
export default {
data() {
return {
showMoreText: {},
}
},
methods: {
toggleMore(index) {
/*
Adds a property to a reactive object, ensuring the new property is
also reactive, so triggers view updates.
https://vuejs.org/v2/api/#Vue-set
*/
this.$set(this.showMoreText, index, ! this.showMoreText[index])
}
}
}
</script>
This sounds like an ideal situation for a new child component, which will allow each instance of the new component to have its own separate state.
The child components can emit events to the parent, if cross-component communication is necessary.

How to create a clickable anchor tag in VueJS with filtered/ transformed data

I have a VueJS app that has the following style of (externally provided) data:
data: function() {
return {
posts: ['1:foo bar oof rab', '2:bar oof rab foo', '3:oof rab foo bar']
}
}
I want my template to loop through posts and make everything to the left of the semi-colon a clickable anchor tag:
<li><a id="1" href="#1">1</a>:foo bar oof rab</li>
<li><a id="2" href="#2">2</a>:bar oof rab foo</li>
<li><a id="3" href="#3">3</a>:oof rab foo bar</li>
Using a filter it's easy to split the text from the anchor position:
<template>
<div>
<v-for="post in posts">
<li>{{ post | trimAnchor }}:{{ post | trimPost }}</li>
</v-for>
</div>
</template>
filters: {
trimPost: function(value) {
value = value.toString();
return value.split(':')[1]
},
trimAnchor: function(value) {
value = value.toString();
return value.split(':')[0]
},
hashAnchor: function(value) {
value = value.toString();
return '#'+value.split(':')[0]
}
But filters don't work in v-bind or router-link:
<li>
<div :id="{{ post | hashAnchor }}">
<router-link="{{ post | hashAnchor }}">
{{ post | trimAnchor }}
</router-link>
</div>
:{{ post | trimPost }}
</li>
What is the correct approach to getting the output I'm after? Should I be using computed & if so, how?
Any help appreciated.
In these kind of cases I always recommend using a computed property. It keeps your template clean, and allows much freedom in preparing your data. Due to it being a computed property, it will automatically recalculate if your data were to change.
The first part is to create some object with all the necessary parts you require:
computed: {
anchors () {
if (!this.posts) {
return [];
}
return this.posts.map(
identifier => {
const [anchor, text] = identifier.split(':', 2);
return {
anchor,
text
}
}
)
}
}
Then you destructure it where you need it. I also added a key to your v-for, assuming that the first part is guaranteed to be unique. It would need to be if your anchors are going to work.
<template>
<div>
<v-for="{ anchor, text } in anchors" :key="anchor">
<li><a :href="`#${anchor}`">{{ anchor }}</a>:{{ text }}</li>
</v-for>
</div>
</template>
Of course you can use filters if needed on the link or li body if you need.

VueJS Computed Data Search Not Functioning

I am attempting to create a search function in my Vue.js 2 application. However, even though my algorithm is giving me the right results, I am not getting the proper filter. Right now, whenever I run a search, I get nothing on the page. Here is my code:
computed:{
filteredSmoothies: function(){
return this.smoothies.filter(smoothie => {
var stuff = smoothie.title.split(" ").concat(smoothie.description.split(" ")).concat(smoothie.category)
var sorted = [];
for (var i = 0; i < stuff.length; i++) {
sorted.push(stuff[i].toLowerCase());
}
console.log(sorted)
if(this.search != null){
var indivthings = this.search.split(" ")
indivthings.forEach(item => {
if(sorted.includes(item.toLowerCase())){console.log("true")} else {console.log("false")}
if(sorted.includes(item.toLowerCase())){return true}
})
} else {
return true
}
})
}
}
Here is my relevant HTML:
<div class="container">
<label for="search">Search: </label>
<input type="text" name="search" v-model="search">
</div>
<div class="index container">
<div class="card" v-for="smoothie in filteredSmoothies" :key="smoothie.id">
<div class="card-content">
<i class="material-icons delete" #click="deleteSmoothie(smoothie.id)">delete</i>
<h2 class="indigo-text">{{ smoothie.title }}</h2>
<p class="indigo-text">{{ smoothie.description }}</p>
<ul class="ingredients">
<li v-for="(ing, index) in smoothie.category" :key="index">
<span class="chip">{{ ing }}</span>
</li>
</ul>
</div>
<span class="btn-floating btn-large halfway-fab pink">
<router-link :to="{ name: 'EditSmoothie', params: {smoothie_slug: smoothie.slug}}">
<i class="material-icons edit">edit</i>
</router-link>
</span>
</div>
</div>
As the discussions in the comments, the root cause should be:
you didn't return true/false apparently inside if(this.search != null){}, it causes return undefined defaultly.
So my opinion is use Javascript Array.every or Array.some. Also you can use for loop + break to implement the goal.
Below is one simple demo for Array.every.
computed:{
filteredSmoothies: function(){
return this.smoothies.filter(smoothie => {
var stuff = smoothie.title.split(" ").concat(smoothie.description.split(" ")).concat(smoothie.category)
var sorted = [];
for (var i = 0; i < stuff.length; i++) {
sorted.push(stuff[i].toLowerCase());
}
console.log(sorted)
if(this.search != null){
var indivthings = this.search.split(" ")
return !indivthings.every(item => { // if false, means item exists in sorted
if(sorted.includes(item.toLowerCase())){return false} else {return true}
})
} else {
return true
}
})
}
or you can still use forEach, the codes will be like:
let result = false
var indivthings = this.search.split(" ")
indivthings.forEach(item => {
if(sorted.includes(item.toLowerCase())){result = true}
})
return result
filteredSmoothies is not returning a list. You iterate over to see if indivthings is contained within sorted, but you don't do anything with that information. I think you need to modify your sorted list based on the results of indivthings.