How to Watch changes inside a div in Vue 3 - javascript

How can I Watch for any changes like classes or text change inside a div in Vue 3.
from
<p class="font-bold">My text</p>
to:
<p class="font-bold color-red">My updated text.</p>
I have tried the Vue 3 Watch method but the Watch method is not looking for changes inside a div.
watch(myDiv, (newValue, oldValue) => {
// not working for changes inside a myDiv.
})

To watch some properties you need to bind it first, like <p :class="classes">{{ text }}</p>, also, you can use mutationObserver:
const {ref, onMounted, onBeforeUnmount} = Vue
const app = Vue.createApp({
data() {
return {
text: 'My text',
classes: 'font-bold'
};
},
watch: {
text(newValue, oldValue) {
console.log(newValue)
},
classes(newValue, oldValue) {
console.log(newValue)
}
},
methods: {
addClass() {
this.classes = 'font-bold color-red'
}
},
setup() {
let observer = null
let target = ref(null)
let options = {subtree: true, childList: true, attributes: true}
const callback = (mutationList, observer) => {
for (const mutation of mutationList) {
if (mutation.type === 'childList') {
console.log('A child node has been added or removed.');
} else if (mutation.type === 'attributes') {
console.log(`The ${mutation.attributeName} attribute was modified.`);
}
}
};
onMounted(() => {
target = document.querySelector(".mydiv")
observer = new MutationObserver(callback);
observer.observe(target, options);
});
onBeforeUnmount(() => observer.disconnect())
},
})
app.mount('#demo')
.color-red {
color: red;
}
.font-bold {
font-weight: 700;
}
<script src="https://unpkg.com/vue#3/dist/vue.global.prod.js"></script>
<div id="demo">
<p class="mydiv" :class="classes">{{ text }}</p>
<input v-model="text" />
<button #click="addClass">class</button>
</div>

In Vue3 you can use refs in template. Docs
<p ref="myDiv" class="test">{{text}}</p>
Now you can define them in template. myDiv.value will hold DOM element
const myDiv = ref(null)
const text= computed(() => {
if(myDiv.value.classList ... rest of your logic for checking classes)
return "Text"
else return "Updated Text"
})

you can create a SFC component with your paragraph and catch every change with a hook onUpdated
onUpdated(() => {
console.log('updated');
});
example: https://stackblitz.com/edit/vue-5qtyjg

Related

Vue js: How to filter an array where every tag in a nested array is included in an array of user entered tags?

I have a search function that filters an array if any one of the tags in the search tags array are included in the item's tags array. Instead of using some() to return true if any tag is present, how can I return true if every tag is present?
Here's an example of what I have (which works). If I search "tag1;" and "tag3;", it returns all 3 items. What I want is only item1 and item2 to return true.
<template>
<div class="right-content">
<div class="nav-margin">
<p>Filter array</p>
<input type="text" v-model="searchTag" #keyup="addSearchTag" />
<div class="spacer-20"></div>
<div v-for="searchTag in searchTags" :key="searchTag" class="items">{{searchTag}}</div>
<div class="spacer-20"></div>
<div v-for="item in searchedItems" :key="item.name" class="items">
<div class="title">{{item.name}}:</div>
<div v-for="tag in item.tags" :key="tag" class="tag">{{tag}}</div>
</div>
</div>
</div>
</template>
<script>
import { ref } from "#vue/reactivity";
import { computed } from '#vue/runtime-core';
export default {
name: "TemplatesDash",
setup() {
const searchTag = ref("");
const searchTags = ref([])
const filteredItems = ref(null)
let items = [
{ name: "item1", tags: ["tag1;", "tag2;", "tag3;"] },
{ name: "item2", tags: ["tag1;", "tag3;"] },
{ name: "item3", tags: ["tag2;", "tag3;"] },
];
const addSearchTag = (e) => {
if (e.key === ";" && searchTag.value) {
if (!searchTags.value.includes(searchTag.value)) {
searchTags.value.push(searchTag.value);
}
searchTag.value = "";
}
};
const searchedItems = computed(() => {
filteredItems.value = items;
if (searchTags.value.length) {
filteredItems.value = filteredItems.value.filter((item) => {
return item.tags.some(
(r) => searchTags.value.indexOf(r) !== -1
);
});
}
return filteredItems.value
});
return { searchTag, searchedItems, searchTags, addSearchTag, };
},
};
</script>
I'm brand new to javascript so any help or pointing in the right direction would be great.
Thanks
I believe the .every() function may be useful here. When you filter the items in your searchedItems computed function, return an .every() call on searchTags, returning whether item.tags .includes() every searchTag array member.
const searchedItems = computed(() => {
filteredItems.value = items;
if (searchTags.value.length) {
filteredItems.value = filteredItems.value.filter((item) => {
return searchTags.value.every(t => item.tags.includes(t));
});
}
return filteredItems.value
});
You're off to a great start with javascript! Hope this helps!

element not updating value on html Polymer V3

I want to update the todoList in my PARENT COMPONENT after I have added a new item in my child using the AddItem() method. Nothing gets added the first time.
EX. if I add "take test" doesn't get render, then if I add "take shower" doesn't get rendered but now "take test" does. Then if I add "take a leak" "take shower" gets rendered.
PARENT COMPONENT
firstUpdated(changedProperties) {
this.addEventListener('addItem', e => {
this.todoList = e.detail.todoList;
});
}
render() {
return html`
<p>Todo App</p>
<add-item></add-item>//Child item that triggers the add
<list-items todoList=${JSON.stringify(this.todoList)}></list-items>
`;
}
CHILD COMPONENT
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: storedLocalList }
})
);
this.todoItem = '';
}
}
render() {
return html`
<div>
<input .value=${this.todoItem} #keyup=${this.inputKeyup} />
<button #click="${this.AddItem}">Add Item</button>
</div>
`;
}
You need to set properties for todoItem
static get properties() {
return {
todoItem: {
type: Array,
Observer: '_itemsUpdated'
}
}
constructor(){
this.todoItem = []
}
_itemsUpdated(newValue,oldValue){
if(newValue){
-- write your code here.. no event listeners required
}
}
In above code., We need to initialise empty array in constructor.
Observer observe the changes to array & triggers itemsUpdated function which carries oldValue & NewValue. In that function., you can place your logic.
No Event Listeners required as per my assumption
Found my error. I was passing to detail: { todoList : storedLocalList } which is the old array without the updated value.
AddItem() {
if (this.todoItem.length > 0) {
let storedLocalList = JSON.parse(localStorage.getItem('todo-list'));
storedLocalList = storedLocalList === null ? [] : storedLocalList;
const todoList = [
...storedLocalList,
{
id: this.uuidGenerator(),
item: this.todoItem,
done: false
}
];
localStorage.setItem('todo-list', JSON.stringify(todoList));
this.dispatchEvent(
new CustomEvent('addItem', {
bubbles: true,
composed: true,
detail: { todoList: todoList }
})
);
this.todoItem = '';
}
}

this.$el of Vue components always reference the same dom element

I have a component “MText”,the main code is as follows :
<template>
<vue-draggable-resizable #click="deleteFun">
</vue-draggable-resizable>
</template>
export default {
method:{
deleteFun () {
this.$el.remove();
}
}}
and in another file,I have a function like this
function createText(){
let MyComponent =Vue.extend({
template:"<MText></MText>",
components:{MText},
data () {
return {}
}})
return new MyComponent(); }
and I have a button,click event bind a function “addText”,like this
addText(){
let text = createText();
let panel = document.getElementById("palette");
let tp_dom = document.createElement("div");
tp_dom.setAttribute("id","id");
panel.appendChild(tp_dom);
text.$mount(tp_dom);
}
the quesition is that when I run “addText” twice, the dom “#palette”
have two “MText” elements,then,I click the second “MText” element,why
the first “MText” is deleted;“this.$el” always reference the first
“MText”
I have no idea what your problem is, but here's a working example:
https://jsfiddle.net/oddswe36/
let i = 0;
// Register your component globally
Vue.component('MText', {
template: `
<div #click="removeMe">click to remove me {{ counter }}</div>
`,
data() {
return {
counter: i++
}
},
methods: {
removeMe() {
this.$el.remove()
}
}
})
function createText() {
const MyComponent = Vue.extend({
template:"<MText></MText>",
})
return new MyComponent();
}
function addText() {
const text = createText();
const panel = document.getElementById("palette");
const tp_dom = document.createElement("div");
tp_dom.setAttribute("id","id");
panel.appendChild(tp_dom);
text.$mount(tp_dom);
};
addText();
addText();
addText();
addText();
<script src="https://cdn.jsdelivr.net/npm/vue#2.5.16/dist/vue.js"></script>
<div id="palette"></div>

VueJs watching deep changes in object

I have this 3 components in VueJS. The problem i want to solve is: When i click at vehicle component, it needs to be selected (selected = true) and other vehicles unselected.
What i need to do for two-way data binding? Because i'm changing this selected property in VehiclesList.vue component and it also need to be changed in Monit.vue (which is a parent) and 'Vehicle.vue' need to watch this property for change class.
Also problem is with updating vehicles. In Monit.vue i do not update full object like this.vehicles = response.vehicles, but i do each by each one, and changing only monit property.
Maybe easier would be use a store for this. But i want to do this in components.
EDITED:Data sctructure
{
"m":[
{
"id":"v19",
"regno":"ATECH DOBLO",
"dt":"2017-10-09 13:19:01",
"lon":17.96442604,
"lat":50.66988373,
"v":0,
"th":0,
"r":0,
"g":28,
"s":"3",
"pow":1
},
{
"id":"v20",
"regno":"ATECH DUCATO_2",
"dt":"2017-10-10 01:00:03",
"lon":17.96442604,
"lat":50.6698494,
"v":0,
"th":0,
"r":0,
"g":20,
"s":"3"
},
]
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
getMonitData(opt){
let self = this;
if (this.getMonitDataTimer) clearTimeout(this.getMonitDataTimer);
this.axios({
url:'/monit',
})
.then(res => {
let data = res.data;
console.log(data);
if (!data.err){
self.updateVehicles(data.m);
}
self.getMonitDataTimer = setTimeout(()=>{
self.getMonitData();
}, self.getMonitDataDelay);
})
.catch(error => {
})
},
updateVehicles(data){
let self = this;
if (!this.vehicles){
this.vehicles = {};
data.forEach((v,id) => {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
});
} else {
data.forEach((v,id) => {
if (self.vehicles[v.id]) {
self.vehicles[v.id].monit = v;
} else {
self.vehicles[v.id] = {
monit: v,
no: Object.keys(self.vehicles).length + 1
}
}
});
}
},
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehiclesList.vue
<template>
<div class="vehicles-list" :class="{'vehicles-list--short': isShort}">
<ul>
<vehicle
v-for="v in vehicles"
:key="v.id"
:data="v"
#click.native="select(v)"
></vehicle>
</ul>
</div>
</template>
<script>
import Vehicle from '#/components/modules/monit/VehiclesListItem.vue';
export default {
data: function(){
return {
isShort: true
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
},
components:{
Vehicle
}
}
</script>
Vehicle.vue
<template>
<li class="vehicle" :id="data.id" :class="classes">
<div class="vehicle-info">
<div class="vehicle-info--regno font-weight-bold"><span class="vehicle-info--no">{{data.no}}.</span> {{ data.monit.regno }}</div>
</div>
<div class="vehicle-stats">
<div v-if="data.monit.v !== 'undefined'" class="vehicle-stat--speed" data-name="speed"><i class="mdi mdi-speedometer"></i>{{ data.monit.v }} km/h</div>
</div>
</li>
</template>
<script>
export default {
props:{
data: Object
},
computed:{
classes (){
return {
'vehicle--selected': this.data.selected
}
}
}
}
</script>
Two-way component data binding was deprecated in VueJS 2.0 for a more event-driven model: https://v2.vuejs.org/v2/guide/components.html#One-Way-Data-Flow
This means, that changes made in the parent are still propagated to the child component (one-way). Changes you make inside the child component need to be explicitly send back to the parent via custom events: https://v2.vuejs.org/v2/guide/components.html#Custom-Events or in 2.3.0+ the sync keyword: https://v2.vuejs.org/v2/guide/components.html#sync-Modifier
EDIT Alternative (maybe better) approach:
Monit.vue:
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles="vehicles" v-on:vehicleSelected="onVehicleSelected"></vehicles-list>
</div>
</div>
</template>
<script>
import VehiclesList from '#/components/modules/monit/VehiclesList.vue';
export default {
name: "Monit",
data (){
return {
vehicles: null
}
},
components: {
VehiclesList
},
methods: {
onVehicleSelected: function (id) {
_.forEach((v, id) => {
v.selected = false;
});
this.vehicles[id].selected = true;
}
...other methods
},
mounted: function(){
this.getMonitData();
}
};
</script>
VehicleList.vue:
methods:{
select(vehicle){
this.$emit('vehicleSelected', vehicle.monit.id)
}
},
Original post:
For your example this would probably mean that you need to emit changes inside the select method and you need to use some sort of mutable object inside the VehicleList.vue:
export default {
data: function(){
return {
isShort: true,
mutableVehicles: {}
}
},
props:{
vehicles: {}
},
methods:{
select(vehicle){
let id = vehicle.monit.id;
console.log("Select vehicle: " + id);
_.forEach((v, id) => {
v.selected = false;
});
this.mutableVehicles[id].selected = true;
this.$emit('update:vehicles', this.mutableVehicles);
},
vehilcesLoaded () {
// Call this function from the parent once the data was loaded from the api.
// This ensures that we don't overwrite the child data with data from the parent when something changes.
// But still have the up-to-date data from the api
this.mutableVehilces = this.vehicles
}
},
components:{
Vehicle
}
}
Monit.vue
<template>
<div class="module-container">
<div class="module-container-widgets">
<vehicles-list :vehicles.sync="vehicles"></vehicles-list>
</div>
</div>
</template>
<script>
You still should maybe think more about responsibilities. Shouldn't the VehicleList.vue component be responsible for loading and managing the vehicles? This probably would make thinks a bit easier.
EDIT 2:
Try to $set the inner object and see if this helps:
self.$set(self.vehicles, v.id, {
monit: v,
no: Object.keys(self.vehicles).length + 1,
selected: false
});

Adding externally handled elements in Vue.js?

I found lots of libraries that somehow marry an external library (and their DOM elements) with Vue.js. All of them though seem to only add child elements to the Vue.js-managed DOM node.
I wrote Vue-Stripe-Elements to make the use of the new Stripe V3 easier but struggled to mount Stripes elements to the Vue component.
The obvious way would be a .vue component like this:
<template>
</template>
<script>
export default {
// could also be `mounted()`
beforeMount () {
const el = Stripe.elements.create('card')
el.mount(this.$el)
}
}
</script>
This would work if Stripe only adds children to the element it is mounted too but it looks like Stripe instead transcludes or replaces the given DOM node. Stripe of course also doesn't support any VNodes.
My current solution to the problem is to create a real DOM node and add it as a child:
<template>
</template>
<script>
export default {
mounted () {
const dom_node = document.createElement('div')
const el = Stripe.elements.create('card')
el.mount(dom_node)
this.$el.appendChild(el)
}
}
</script>
It works but it feels like I'm fighting against Vue.js here and I might create odd side effects here. Or am I just doing what other appending libraries do manually and it is the best way to go?
Is there an "official" way to do this?
Thanks in advance for any helpful comment about it.
Stripe Elements Vuejs 2
Use refs to get DOM elements in vuejs.
HTML
<div ref="cardElement"></div>
JS
mounted() {
const stripe = Stripe('pk');
const elements = stripe.elements();
const card = elements.create('card');
card.mount(this.$refs.cardElement);
}
I faced the same problem, so the method mounted is correct to add, but for the big applications where u call a specific vuejs i got the error
"please make sure the element you are attempting to use is still mounted."
HTML Snippet :
<div style="min-height:100px;">
<div class="group">
<h4><span class="label label-default"> Enter Card Details</span> </h4>
<label class="cardlabel">
<span>Card number</span>
<div id="card-number-element" class="field"></div>
<span class="brand"><i class="pf pf-credit-card" id="brand-icon"></i></span>
</label>
<label class="cardlabel">
<span>Expiry date</span>
<div id="card-expiry-element" class="field"></div>
</label>
<label class="cardlabel">
<span>CVC</span>
<div id="card-cvc-element" class="field"></div>
</label>
</div>
</div>
Vue.js
(function () {
let stripe = Stripe('keyhere');
elements = stripe.elements(),
cardNumberElementStripe = undefined;
cardExpiryElementStripe = undefined;
cardCvcElementStripe = undefined;
var style = {
base: {
iconColor: '#666EE8',
color: '#31325F',
lineHeight: '40px',
fontWeight: 300,
fontFamily: 'Helvetica Neue',
fontSize: '15px',
'::placeholder': {
color: '#CFD7E0',
},
},
};
var purchase= new Vue({
el: '#purchase',
mounted() {
cardNumberElementStripe = elements.create('cardNumber', {
style: style
});
cardExpiryElementStripe = elements.create('cardExpiry', {
style: style
});
cardCvcElementStripe = elements.create('cardCvc', {
style: style
});
cardNumberElementStripe.mount('#card-number-element');
cardExpiryElementStripe.mount('#card-expiry-element');
cardCvcElementStripe.mount('#card-cvc-element');
cardNumberElementStripe.on('change', function (event) {
// Switch brand logo
if (event.brand) {
if (event.error) { setBrandIcon("unknown"); } else { setBrandIcon(event.brand); }
}
//setOutcome(event);
});
function setBrandIcon(brand) {
var brandIconElement = document.getElementById('brand-icon');
var pfClass = 'pf-credit-card';
if (brand in cardBrandToPfClass) {
pfClass = cardBrandToPfClass[brand];
}
for (var i = brandIconElement.classList.length - 1; i >= 0; i--) {
brandIconElement.classList.remove(brandIconElement.classList[i]);
}
brandIconElement.classList.add('pf');
brandIconElement.classList.add(pfClass);
}
var cardBrandToPfClass = {
'visa': 'pf-visa',
'mastercard': 'pf-mastercard',
'amex': 'pf-american-express',
'discover': 'pf-discover',
'diners': 'pf-diners',
'jcb': 'pf-jcb',
'unknown': 'pf-credit-card',
}
},
created() {
//on the buttn click u are calling this using v-on:click.prevent="payment"
payment: function (e) {
stripe.createToken(cardNumberElementStripe).then(function (result) {
debugger;
if (result.token) {
cardId = result.token.id;
// $("#paymentform").get(0).submit();
} else if (result.error) {
errorElement.textContent = result.error.message;
return;
}
});
}
}

Categories