Hello and Happy holidays !!
I need advice on how I could get data from an external function that generate a zipfile into my vuejs component, to create a progress bar for JSZip plugin: https://stuk.github.io/jszip/documentation/api_jszip/generate_async.html
I import my file :
import { generateZIP } from "#/utils/downloader.js";
and call it inside vuejs from a method trigger by a button:
<template>
...
<div v-for="result of results" :key="result.item.refID">
<section class="row" #click="selectByRow(result.item)">
<input
type="checkbox"
:id="result.item.refID"
:value="result.item.refID"
v-model="checkedItems"
class="checkbox"
/>
</div>
<!-- FOUND RESULTS -->
<div class="name">{{ result.item.marketingName }}</div>
</section>
</div>
<!-- Download all checked items -->
<div>
<button
v-if="checkedItems.length > 1"
#click="downloadAll(checkedItems)"
class="button"
>
Download Selection
</button>
</template>
...
<script>
import { mapState, mapActions, mapMutations, mapGetters } from "vuex";
import { generateZIP } from "#/utils/downloader.js";
...
export default {
data() {
return {
// Path to point to pictures storage
domainName: this.$domainName,
// Array to gather search results
searchArray: [],
checkedItems: [],
// make a special array for row selection
checkedRow: []
};
},
methods:
downloadAll(files) {
// Prepare path
const fullPath = `${this.domainName}/files/${this.reqPath}/`;
const filesArray = [];
files.forEach(fileID => {
let obj = this.results.find(value => value.item.refID == fileID);
if (obj.item.images !== undefined) {
filesArray.push(obj.item.images);
}
});
generateZIP(filesArray.flat(), fullPath);
},
selectByRow(resultID) {
// Check if select resultID.refID is already in checkedItems and store it in variable if its present.
const isInArray = this.checkedItems.find(name => name === resultID.refID);
// if the ref not in array, add it
if (!isInArray) {
this.checkedItems.push(resultID.refID);
// Add checkedRow full information object
this.checkedRow.push(resultID);
} else {
// if already in array, remove it
this.checkedItems = this.checkedItems.filter(
name => name !== resultID.refID
);
this.checkedRow = this.checkedRow.filter(
name => name.refID !== resultID.refID
);
}
...
Everything working, now I add some feedback showing the zip progress. There is an available callback function "updateCallback" that I'll call in my downloader.js
zip.generateAsync({type:"blob"}, function updateCallback(metadata) {
console.log("progression: " + metadata.percent.toFixed(2) + " %");
if(metadata.currentFile) {
console.log("current file = " + metadata.currentFile);
}
})
...
export {
generateZIP
}
Cool it displays progression in my console log.
But HOW could I import this metadata object into vue to display it in my app ?
Thanks a lot !
Use data properties in your vue component.
Inside the callback, link your instance (this) to local var, to pass value between callback data and reactive property. For example: let var=this
Related
I have an app with plain JS and Vue in one file. I need to pass a variable value from JS to Vue.
0. All code in one file:
<script>
plain js
var plainJS = 100;
</script>
<script>
var app = new Vue({
el: '#vue-app',
....
</script>
main functionality of the app is on plain js. Vue does a small part with UI.
with js I can catch if one of my elements changed position (a dot on the screen)
I need fire popup(some alert) if checkBox is selected but the Dot wasn't moved.
checkBox is a Vue element
I can pass data from Django to Vue
this.vueVar = {{ djangoVar|safe }}
So how to pass
*var plainJS = 100;*
to vue app from plain JS part of the code?
Can you give me a simple way to set vueVar = plainJS?
UPDATE:
function from plain JS
function isDotMoved(length){
if(length != 0){
console.log(length)
return true;
}
return false;
};
so this function works when I grab and move my dot on the screen.
As well, I have a function in Vue part:
isDotsMoved(){
this.dotMoved = isDotMoved(length); // function from plain JS
console.log('moved', this.dotMoved)
if(!this.dotMoved){
toastr.info('Dot Moved');
}
},
I call this function onClick. It should fire Alert if dots were moved.
I use another function the same way:
function videoPause() {
inputVideo.pause();
};
And called it inside of my Vue part:
videoPauseVue() {
videoPause(); //function from plain JS
};
How can I do the same for isDotsMoved()?
First, i add isDotMoved function in the index.html script tag and declare it on window variable that can access anywhere in your code because it is global scope.
<script>
window.plainFunc = (function () {
return {
isDotMoved: function (length) {
if (length != 0) {
console.log(length);
return true;
}
return false;
}
};
})();
</script>
Then in vue I access it throught window variable
<template>
<div id="app">
<h1>Use Function from plainJS</h1>
<button #click="handleClick">Click here to invoke function isDotMove from script</script></button>
</div>
</template>
<script>
function isDotsMoved() {
let length = 10;
let dotMoved = window.plainFunc.isDotMoved(length); // function from plain JS
console.log("moved", dotMoved);
if (!dotMoved) {
alert("Dot Moved");
}
}
export default {
name: "App",
methods: {
handleClick: function () {
isDotsMoved();
},
},
};
</script>
<style>
</style>
Check my example on codebox: https://codesandbox.io/embed/vue-js-font-awesome-1--getting-started-forked-d8xist?fontsize=14&hidenavigation=1&theme=dark
You can access a ref on the root component if you store a variable of what createApp returns. Then each time you would update your plainJS var, also reassign a matching property (ref) on the "app" object. For the initial value you may use a "root prop" which is the 2nd param of the createApp function.
main.js
import { createApp } from "vue";
import App from "./App.vue";
var plainJS = 100;
const myApp = createApp(App, { plainJS: plainJS }).mount("#app");
setInterval(() => {
//interval used here to simulate a value that changes at arbitrary times
plainJS++;
myApp.varFromOutsideVue = plainJS; // š this updates the ref
}, 500);
App.vue
<template>
<h1>{{ varFromOutsideVue }}</h1>
</template>
<script>
import { onMounted, onUnmounted, ref } from "vue";
export default {
name: "App",
props: {
plainJS: { type: Number },
},
setup(props) {
const varFromOutsideVue = ref(props.plainJS);
return {
varFromOutsideVue,
};
},
};
</script>
https://codesandbox.io/s/eager-rubin-6fv7p7?file=/src/main.js
Another option (see my other answer for a more direct solution) is to use the browser's native event system to "subscribe" to changes to the variable from within your vue app. Each time the value changes you emit a custom event and there is an event listener within your vue app set up to listen to those changes and update a reactive ref.
main.js
import { createApp } from "vue";
import App from "./App.vue";
var plainJS = 100;
function fireVarChangeEvent() {
const newEvent = new CustomEvent("varchanged", {
detail: plainJS
});
window.dispatchEvent(newEvent);
}
setInterval(() => {
//interval used here to simulate a value that changes at arbitrary times
plainJS++;
fireVarChangeEvent(); // call this function after each time plainJs var is updated
}, 500);
createApp(App, { plainJS: plainJS }).mount("#app"); //pass in the first value of plainJS as a prop, this will not stay reactive, hence the custom event
App.vue
<template>
<h1>{{ varFromOutsideVue }}</h1>
</template>
<script>
import { onMounted, onUnmounted, ref } from "vue";
export default {
name: "App",
props: {
plainJS: { type: Number },
},
setup(props) {
const varFromOutsideVue = ref(props.plainJS);
function updateVar(e) {
varFromOutsideVue.value = e.detail;
}
onMounted(() => {
window.addEventListener("varchanged", updateVar);
});
onUnmounted(() => {
window.removeEventListener("varchanged", updateVar);
});
return {
varFromOutsideVue,
};
},
};
</script>
I have a class that extends Array, and as part of it, I want to intercept changes that are made to its properties, so I use Proxy, which is what I return from its constructor. It works just fine until I try to use it in my Vue component. See this example.
When the page first loads, you'll see the console log for Collection 1 in the watchEffect, which is the expected result. Then when you click the Add Filter button, you'll see that the display doesn't update and the watchEffect doesn't fire... expectation is that we'd get the console log like when the page loaded. However, if you inspect collection1, you'll see that the value was added.
Does anyone know why this doesn't work and how I can fix it? It feels like maybe my proxy is being tripped up with Vue's proxy wrapper, but I don't know enough about the internals to say that confidently.
Collection.js
export class MyCollection extends Array {
constructor(data) {
super();
this.add(data);
return new Proxy(this, {
set(target, prop, value) {
target[prop] = value;
if (prop === 'filters') {
const add = []
target.records.forEach((item) => {
if (item.id === target.filters) {
add.push(item)
}
})
target.add(add);
}
return true;
}
})
}
addFilters() {
this.filters = 1
}
add(items) {
this.length = 0;
items = Array.isArray(items) ? items : [items];
this.records = items;
console.log('here', this.records, this);
items.forEach((item) => this.push(item))
}
}
App.vue
<script setup>
import {watchEffect, computed, ref, toRaw} from "vue";
import {MyCollection} from "./Collection.js";
const collection1 = $ref(new MyCollection([{id: 1, display: 'one'}, {id: 2, display: 'two'}]));
watchEffect(() => {
console.log("wow", collection1);
});
const onClickUpdate1 =() => {
collection1.addFilters();
}
</script>
<template>
<div>
Collection 1
<button #click='onClickUpdate1'>
Add Filter
</button>
</div>
<div v-for="item in collection1" :key="item.id">
{{item.display}}
</div>
</template>
Try this edit I made to your code.
I have changed a few things:
changed to script setup for better readability
changed computed to reactive using the $red syntax from reactivity transform
You were adding items with name: 'three' and displaying item.display. I changed that bit to add with display: 'three'.
It works now and I suspect the difference is in having changed from computed to reactive though I'm not sure. I'm going to read about it a bit more and update the answer accordingly.
I think I found a solution, but I may have also found a bug in Vue, which I've reported. What I had to change was calling the receiver's method instead of the target's method in the set trap of MyCollection. Fiddle
MyCollection.js
export class MyCollection extends Array {
constructor(data) {
super();
this.add(data);
return new Proxy(this, {
set(target, prop, value, receiver) {
target[prop] = value;
if (prop === 'filters') {
const add = []
target.records.forEach((item) => {
if (item.id === target.filters) {
add.push(item)
}
})
// IMPORTANT: Had to use receiver here instead of target
receiver.add(add);
}
return true;
}
})
}
addFilters() {
this.filters = 1
}
add(items) {
this.length = 0;
items = Array.isArray(items) ? items : [items];
this.records = items;
items.forEach((item) => this.push(item))
}
}
The second issue, which I think is the bug, is that I still can't use a computed method for this. However, I can use a ref and watchEffect to achieve the same thing.
App.vue
<script setup>
import {watchEffect, computed, ref, toRaw} from "vue";
const props = defineProps({
options: {
type: Array,
default: [{id: 1, display: 'one'}, {id: 2, display: 'two'}]
}
})
import {MyCollection} from "./Collection.js";
const collection1 = ref(null);
const collection2 = computed(() => new MyCollection(props.options))
// Workaround for not being able to use computed
watchEffect(() => {
collection1.value = new MyCollection(props.options)
})
watchEffect(() => {
console.log("collection1", collection1.value.length);
});
// THIS WILL NOT FIRE WHEN ADD FILTER IS CLICKED
watchEffect(() => {
console.log("collection2", collection2.value.length);
});
const onClickUpdate1 =() => {
collection1.value.addFilters();
collection2.value.addFilters();
}
</script>
<template>
<div>
<button #click='onClickUpdate1'>
Add Filter
</button>
</div>
<div style="display: flex">
<div style="margin-right: 1rem;">
Collection 1
<div v-for="item in collection1" :key="item.id">
{{item.display}}
</div>
</div>
<div>
Collection 2
<div v-for="item in collection2" :key="item.id">
{{item.display}}
</div>
</div>
</div>
</template>
I am working with Vue.js 3. I got a problem, let us see the code first.
Code
ChildComponent.vue
<template>
<div>
{{ modelValue }}
<input v-model="resultString"/>
<button #click="showModelValue">show model value</button>
</div>
</template>
<script lang="ts">
import {defineComponent, PropType, ref, watch} from "vue";
export default defineComponent({
props: {
modelValue: {
type: Object as PropType<number>,
required: true,
}
},
emits:['update:modelValue'],
setup(props) {
const resultString = ref<string>("");
watch(() => props.modelValue, (newVal:number, oldVal:number) => {
if (newVal % 2 == 0) {
resultString.value = 'even';
} else {
resultString.value = 'odd';
}
}, {deep: true});
const showModelValue = () => {
console.log(props.modelValue);
}
return { resultString, showModelValue }
}
})
</script>
<style scoped>
</style>
ParentComponent.vue
<template>
<div class="main-container">
<child-component v-model="test1" />
<button #click="increaseTest1">increase test1</button>
<hr/>
Cannot use v-model within v-for!
<!--
<div v-for="(testNum, index) in test2">
<child-component v-model="testNum" /> <button #click="increaseTest2(index)">increase test2</button>
</div>
-->
<hr/>
<div v-for="(testNumWrapper, index) in test3">
<child-component v-model="testNumWrapper.val" /> <button #click="increaseTest3(index)">increase test3</button>
</div>
</div>
</template>
<script lang="ts">
import {defineComponent, onMounted, ref} from "vue";
import ChildComponent from "#/main/components/pc/ChildComponent.vue";
export default defineComponent({
components: {ChildComponent},
setup() {
const test1 = ref<number>(1);
const increaseTest1 = () => test1.value++;
/*
const test2 = ref<number[]>([3,1,4,1,5,9]);
const increaseTest2 = (index:number) => test2.value[index]++;
const updateTest2 = (e:any) => {
console.log(e);
};
*/
const test3 = ref<{val:number}[]>([]);
const increaseTest3 = (index:number) => test3.value[index].val++;
onMounted(() => {
// This triggers watch() in childComponent.
test1.value = 4;
// But these do NOT trigger watch() in childComponent.
test3.value = [{val: 3},{val: 1},{val: 4},{val: 1},{val: 5},{val: 9}];
});
return {
test1, increaseTest1,
//test2, increaseTest2, updateTest2,
test3, increaseTest3,
}
}
});
</script>
<style scoped>
</style>
The above codes are modified for sharing my problem, let me explain.
The ChildComponent decides if the value of modelValue is odd or even, automatically.
The ParentComponent...
binds a ref variable, test1 to ChildComponent,
tries to bind each primitive typed member of ref array variable, test2 to ChildComponents but this is not compiled because v-model cannot be used within v-for, so that I commented out the code lines and try the next test,
binds each non-primitive typed member of ref array variable, test3 to ChildComponent.
And it initiates the variables in onMounted().
However, I've found that watch() in ChildCompoent works fine for test1 but not for test3. The watch() also does not work for test3 when I push a value into test3 or delete a value from test3. (It works when I click increase button.)
Please, show me a way to trigger the watch() function for test3.
Thank you.
Of course the watch in child component is not triggered by pushing or deleting elements from the array. Child component is not watching whole array but just single element (it's val property).
If you push new element into the array, child component for that element does not exist yet. If you delete an element, the child component rendered for that element is destroyed immediately. Only thing that can trigger that watch is indeed mutation of the val property...
Problem with test2 is that testNum is local temporary variable - instead of v-model="testNum", use v-model="test2[index]"
Anyway your ChildComponent.vue does not need watch at all. Just use computed:
const resultString = computed(() => props.modelValue % 2 === 0 ? 'even' : 'odd')
...and btw you should be using key with v-for - docs
I created a small module as a validator inspired from vee-validate and I would like to use this in conjunction with the composition api.
I have a list of errorMessages that are stored in a reactive array, however when I retrieve this variable in my vue component, despite the error messages being stored in the array accordingly, the variable is not updating in the vue template.
Iām not very savvy with this so I might not be concise with my explanation. The refs in the module seem to be working properly.
Can someone kindly indicate what I might be doing wrong? I'm completely stuck and I don't know how else I can proceed.
Validator.js (Npm module - located in node_modules)
import Vue from 'vue';
import VueCompositionAPI from '#vue/composition-api'
import {ref} from '#vue/composition-api'
Vue.use(VueCompositionAPI)
class Validator {
ā¦.
register({fieldName, rules, type}) {
if (!fieldName || rules === null || rules === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
let errorMessages = ref([]);
// define callback for pub-sub
const callback = ({id, messages}) => {
if (fieldId === id) {
errorMessages.value = Object.assign([], messages);
console.log(errorMessages.value); // this contains the value of the error messages.
}
};
return {
errorMessages,
};
}
ā¦ā¦
InputField.vue
<template>
<div :style="{'width': fieldWidth}" class="form-group">
<label :for="fieldName">
<input
ref="inputField"
:type="type"
:id="fieldName"
:name="fieldName"
:class="[{'field-error': apiError || errorMessages.length > 0}, {'read-only-input': isReadOnly}]"
#input="handleInput"
v-model="input"
class="form-input"/>
</label>
<div>
<p class="text-error">{{errorMessages}}</p> // Error messages not displaying
</div>
</div>
</template>
<script>
import {ref, watch} from '#vue/composition-api';
import Validator from "validator";
export default {
props: {
fieldTitle: {
required: true
},
fieldName: {
required: true
},
type: {
required: true
},
rules: {
default: 'required'
}
},
setup(props) {
// The error messages are returned in the component but they are not reactive. Therefore they only appear after its re-rendered.
const {errorMessages, handleInput, setFieldData} = Validator.register(props);
return {
errorMessages,
handleInput,
}
}
}
</script>
You should be using Vue.Set(), it directly triggers related values to be updated
The problem is Validator.register() directly destructures props, which removes the reactivity from the resulting values.
Solution
Use toRefs(props) to create an object of refs for each prop, and pass that to Validator.register():
import { toRefs } from 'vue'
š
Validator.register(toRefs(props))
Then update Validator.register() to unwrap the refs where needed:
class Validator {
register({ fieldName, rules, type }) {
š š š
if (!fieldName.value || rules.value === null || rules.value === undefined) {
console.error('Please pass in fieldName and rules');
return false;
}
ā®
}
}
I'm new to Vue and having trouble with when / how to assign a prop value calculated using the a store object. I want to grab the id from the url (e.g. /location?locationid) and compare that against the locations object in the store to find the correct location (by location_id) and pass that matching object as prop to the child location component.
I'm not sure when to do the curLoc calculation currently in created(). I've tried doing it as a computed prop, with no luck. I've tried putting the calculation into a method and calling it on created() and update() and it works with a webpack update but not a page refresh.
<template>
<div class="location-wrap">
<main id="main" aria-label="content">
<h2 class="c-section__title">{{ this.curLoc.location_name // get get location_name of undefined }}</h2>
<div class="location">
<Location v-bind:loc="curLoc" /> // I want curLoc to be accessible to this child Location component.
</div>
</main>
</div>
</template>
<script>
import Location from "#/components/Location";
import { mapState } from "vuex";
export default {
name: "location",
components: {
Location,
},
computed: mapState(["locations"]),
data() {
return {
curLoc: {},
locationId: "",
locationName: ""
};
},
created() {
// curLoc calculation
let ref = location.href;
this.locationId = ref.substring(ref.indexOf("?") + 1);
this.locations.forEach(loc => {
if (loc.location_id === this.locationId) {
this.curLoc = loc;
console.log(this.curLoc); // nope
}
});
},
updated() {},
methods: {}
};
</script>
You can create a computed property like this
currLoc() {
/* remove currLoc from data */
let currLoc = {};
let ref = location.href;
this.locationId = ref.substring(ref.indexOf("?") + 1);
/* put check when initially locations isn't defined and can even use break */
this.locations.forEach(loc => {
if (loc.location_id === this.locationId) {
curLoc = loc;
console.log(curLoc); // should print your currLoc
}
});
return currLoc;
}
By making currrLoc a computed property we've ensured that it runs everytime the location changes. You can now use computed property in the template (currLoc and pass it as a prop)