I'm trying to build a reusable Image Loader component in Vue.js, which should:
Manage its own thumbnail data
Take src from parent as prop
Display different thumbnails based on prop, using same instance without being destroyed
So it may take data from two places (own thumbnail state || src prop), and I have a very difficult time wrapping my head around how to manage this. Not too sure if this is the right approach to the problem either.
At this point I am getting an infinite update loop warning in the console.
[Vue warn]: You may have an infinite update loop in a component render function.
Here is my code. Any help whatsoever would be greatly appreciated.
<template>
<div>
<label class="fileContainer">
<span class="icon is-large">
<i class="fa fa-camera"></i>
</span>
<input type="file" :index="index" #change="updateThumbnail"/>
</label>
<object
:data="pdfURL"
type="application/pdf"
:class="{visible: pdfURL != ''}">
</object>
<img :src="getSrc()" />
</div>
</template>
<script>
export default {
props: ["index", "srcProp"],
data() {
return {
imageSrc: '',
imageDataURI: '',
pdfURL: '',
}
},
methods: {
getSrc() {
if (typeof this.srcProp !== "undefined") {
this.imageSrc = ''
if (this.srcProp !== '') {
this.imageSrc = this.srcProp
} else {
this.imageSrc = this.imageDataURI
}
} else {
this.imageSrc = this.imageDataURI
}
return this.imageSrc
},
updateThumbnail(event) {
this.$emit('change')
const fileTypes = ['jpg', 'jpeg', 'png']
const imgFile = event.target.files[0] || event.srcElement.files[0]
const extension = imgFile.name.split('.').pop().toLowerCase()
const isImg = fileTypes.indexOf(extension) > -1
if (extension === 'pdf') {
const pdfURL = URL.createObjectURL(imgFile);
this.pdfURL = pdfURL
this.height = 200
return
} else if (isImg) {
var reader = new FileReader();
reader.readAsDataURL(imgFile);
reader.onload = () => {
this.imageDataURI = reader.result
return
}
} else {
alert("Please submit images or PDFs only.")
}
},
}
}
</script>
I was facing the same problem. Let me explain to you what I know.
When :src="anyFunction()" is used it re-renders upto infinite time even after it gets the result.
At this point we get same array for infinite times. Try displaying console.log('this.imgSrc'), you will get infinite number of array.
Here we cannot use slice or splice to get the first array. I haven't found solution but I managed to keep variables in a src rather than rendering whole function and getting url.
<img :src="'https://maps.googleapis.com/maps/api/staticmap?zoom=15&size=500x250&markers=color:red%7Clabel:L%7C'+this.leadIpinfo.loc.split(',').slice(0,1)+','+this.Ipinfo.loc.split(',').slice(1,2) alt="loc">
Here I have fetched the array and splited and sliced into 2 values.
Hope it could help in some ways.
Related
EDIT:
I've updated my watcher and recall the function to update the percentage live, but it's still not verbose enough to check and update all changed objs.
watch: {
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(`changed obj`, changedObjIndex)
this.handleDisplayStatus(changedObjIndex)
console.log(
`obj that changed`,
this.tableData[changedObjIndex].completion_progress
)
},
},
Problem
I have a table that shows the details and progress of a video(s) upload. The progress bar is where I am having trouble updating it live (more on this below).
I have a blank tableData array in my data that is filled when the component is rendered with a handleDisplayStatus function. This method only runs once obviously, so I was planning on putting a watcher on the tableData array and then rerunning the handleDisplayStatus function if there is a change. I initially went with:
this.tableData.every((item) =>
console.log(
`completion progress from watcher`,
item.completion_progress
)
)
However .every only returns a boolean and while it would rerun the handleDisplayStatus function, that function takes an index, _row so that doesn't seem to be the fix.
Also putting this.handleDisplayStatus(index, _row) in the watcher will give this error in the console Error in callback for watcher "tableData": "ReferenceError: index is not defined".
Which makes sense because that function needs the indexes (_row param is not being used I think).
From there I went with .findIndex, but again that just gives me the first element that satisfies the condition. Since the table can have multiple videos loading at the same time, this is not the correct fix either.
tableData(newVal, oldVal) {
const changedObjIndex = this.tableData.findIndex((obj) => {
return obj.status === "In Progress"
})
console.log(changedObjIndex)
}
As I mentioned, everything works fine, but I need a page refresh to show the updated number. We are purposely not using socket.io for this; even though I think it's probably a good idea.
Any ideas of how I can get this function to call properly and get the percentages to update in real time? Even the old :key to force a rerender doesn't want to work here.
Any help or guidance would be greatly appreciated!
NOTE:
my progress bar just takes in a percentage prop as a number to render and is pretty straightforward. If you need to see that code as well I can edit this post!
tableData Array of Objs with key/values
(NOTE: the task_status should be: "IN PROGRESS", but I couldn't edit that on the mySQL workbench)
Method that needs to be recalled
handleDisplayStatus(index, _row) {
if (this.tableData[index].task_status == "IN PROGRESS") {
let trainingProcessId = this.tableData[index].training_process_id
this.tableData[index].intervalId = setInterval(() => {
if (
this.tableData[index].completion_progress != 100 &&
this.tableData[index].task_status == "IN PROGRESS"
) {
getTrainingProcessInfo({
trainingProcessId: trainingProcessId,
}).then((res) => {
let jsonObj = JSON.parse(JSON.stringify(res.data))
this.tableData[index].completion_progress =
jsonObj.completion_progress
})
} else {
clearInterval(this.tableData[index].intervalId)
}
}, 6000)
}
return this.tableData[index].task_status
}
template (fwiw)
<template>
<div>
<el-card class="box-card">
<div style="margin-left: 5px" class="_table-wrapper">
<el-table
ref="multipleTable"
v-loading="listLoading"
:data="tableData"
id="el-table"
>
<el-table-column
:label="$t('training_monitor_content.table_column_5')"
align="center"
>
<template slot-scope="scope">
<ProgressBar
:key="scope.row.user"
style="width: 100%; height: 20px; flex-shrink: 0"
:percentage="scope.row.completion_progress"
/>
</template>
</el-table-column>
</el-table>
</div>
</el-card>
</div>
</template>
If I understood you correctly try with deep watch:
watch: {
tableData: {
handler(newValue) {
newValue.forEach((obj, idx) => {
if (obj.status === 'In Progress') this.handleDisplayStatus(idx)
});
},
deep: true
}
}
I'm having troubles loading the content of an HTML file in a Vue component. Basically i have a Django backend that generates an HTML file using Bokeh and a library called backtesting.py. My frontend is using Nuxt/Vue, so i can't just load the HTML on the page dynamically.
Here is what the HTML file looks like (it was too long to post here): https://controlc.com/aad9cb7f
The content of that file should be loaded in a basic component:
<template>
<div>
<h1>Some content here</h1>
</div>
</template>
<script>
export default {
components: {
},
data() {
return {
}
},
mounted() {
},
methods: {
}
}
</script>
The problem is that i really don't know how to do that. If i just copy and paste the content in the vue component, i'll get a lot of error due to the fact that i'm using a <script> tag in a component. The only thing i managed to do was to load the BokehJS CDN in my index.html file, but even after that i'll get a Bokeh is undefined error in the component.
What can i do to accomplish this? Any kind of advice is appreciated
Tao's answer is spot on and is very similar to how I've solved this issue for myself in the past.
However, I'd like to throw in an alternative iframe approach that could work in case reactivity is important. Here's a codesandbox link
The only difference is that this approach loads the code/HTML via XHR and writes it manually into the iframe. Using this approach, you should be able to add some reactivity if necessary.
<script>
export default {
components: {},
data() {
return {};
},
async mounted() {
this.initialize();
},
methods: {
async initialize() {
const html = await this.loadHTML();
const doc = this.htmlToDocument(html);
this.updateIframe(doc);
},
async loadHTML() {
const response = await fetch("/plot");
const text = await response.text();
return text;
},
htmlToDocument(html) {
const parser = new DOMParser();
const doc = parser.parseFromString(html, "text/html");
return doc;
},
updateIframe(doc) {
const iframe = this.$refs.frame;
const iframeDocument = iframe.contentWindow.document;
iframeDocument.open();
iframeDocument.write(doc.documentElement.innerHTML);
iframeDocument.close();
}
},
};
</script>
In the codesandbox, I've thrown in two additional methods to give you an example of how reactivity can work with this approach:
modify() {
if (this.orig) {
// Only for the purpose of this example.
// It's already been modified. Just short-circuit so we don't overwrite it
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
// Store old data so we can reset it
this.orig = data.Close[idx];
data.Close[Math.floor(data.Close.length / 2)] = 0;
modelWithData.change.emit();
},
reset() {
if (!this.orig) {
return;
}
const bokehDoc = this.$refs.frame.contentWindow.Bokeh.documents[0];
// Get access to the data..not sure if there's a better/proper way
const models = [...bokehDoc._all_models.values()];
const modelWithData = models.find((x) => x.data);
const { data } = modelWithData;
const idx = Math.floor(data.Close.length / 2);
data.Close[idx] = this.orig;
modelWithData.change.emit();
delete this.orig;
}
Probably the simplest way is to make your HTML available at the URL of your choice, on your server (regardless of Vue).
Then, in your app, use an <iframe> and point its src to that html. Here's an example, using codesandbox.io, where I placed what you posted into the index.html. Below you can see it working with both <iframe> and <object> tags:
Vue.config.productionTip = false;
Vue.config.devtools = false;
new Vue({
'el': '#app'
})
body {
margin: 0;
}
h1, h3 {padding-left: 1rem;}
object, iframe {
border: none;
height: 800px;
width: 100%;
min-height: calc(100vh - 125px);
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.17/vue.js"></script>
<div id="app">
<h1>This content is placed in Vue</h1>
<h3>Vue doesn't really care.</h3>
<iframe src="https://1gk6z.csb.app/"></iframe>
<h1><code><object></code> works, too:</h1>
<object type="text/html" data="https://1gk6z.csb.app/"></object>
</div>
Note: if the domain serving the graph and the one displaying it differ, you'll need server-side configuration to allow the embed (most domains have it turned off by default).
Strategy:
insert and init bokeh in head tag of public/index.html
read file in a string via ajax/xhr and parse as dom tree
extract each needed dom element from the parsed tree
recreate and append each element
No iframe needed. window.Bokeh is directly accessible.
A skeletal example of reactivity is suggested through the method logBkh that logs the global Bokeh object when clicking on the graph
<template>
<div id="app">
<div id="page-container" #click="logBkh"></div>
</div>
</template>
<script>
// loaded from filesystem for test purposes
import page from 'raw-loader!./assets/page.txt'
// parse as dom tree
const extDoc = new DOMParser().parseFromString(page, 'text/html');
export default {
methods: {
logBkh(){
console.log(window.Bokeh)
}
},
mounted() {
const pageContainer = document.querySelector('#page-container')
// generate and append root div
const dv = document.createElement('div')
const { attributes } = extDoc.querySelector('.bk-root')
for(const attr in attributes) {
dv.setAttribute(attributes[attr].name, attributes[attr].value)
}
pageContainer.append(dv)
for(const _scrpt of extDoc.body.querySelectorAll('script')) {
// generate and append each script
const scrpt = document.createElement('script')
for(const attr in _scrpt.attributes) {
scrpt.setAttribute(
_scrpt.attributes[attr].name,
_scrpt.attributes[attr].value
)
}
scrpt.innerHTML = _scrpt.innerHTML
pageContainer.append(scrpt)
}
}
}
</script>
result:
I want to do some code execution after all images are loaded (need the set scroll in a specific position). I use nextTik() but Codes are processed before loading images . We also can't use mounted and created and methods like this because codes should be executed after clicking on a button.
is there any method or trick?
thanks
You can use the load event on each image to check if all images have been loaded.
example: https://jsfiddle.net/jacobgoh101/e5bd86k6/1/
<div id="app">
<img #load="handleLoad" #error="handleLoad" v-for="i in 10" :src="`https://picsum.photos/${Math.random()*300}/700/?random`"/>
</div>
Javascript
new Vue({
el: "#app",
data: {
imgLoaded: 0
},
methods: {
handleLoad: function(todo){
this.imgLoaded++;
if(this.imgLoaded === 10) {
alert('all image loaded')
}
}
}
})
Based on ( https://stackoverflow.com/a/50481269 enter link description here) answer I did something like this:
<img class="card-img-top" :src=" 'images/' + (data.src || 'image-default.png') "
:alt="data.alt" #load="handleImagesLoad" #error="handleImagesLoad">
(#load and #error on each image on my page) and
handleImagesLoad: function($event){
var images = $('img:not([loaded])');
var target = $($event.target);
$(target).attr('loaded', true);
if(images.length <= 1){
$('#preloader').fadeOut();
}
},
Here is my approach for a component having both "static" images and an an arbitray number of images fetched from database :
Append two items to the component data: 'imgsLength' and 'loadedImgsLength'
Append a 'load' event handler to every img element, that increments 'loadedImgsLength'
After all fetched data are loaded, set 'imgsLength' to the number of all img elements
So, you know that all images are loaded when 'loadedImgsLength' > 0 and 'loadedImgsLength' == 'imgsLength'
In this exemple, my component won't be visible until all images are loaded:
<template>
<div v-show="ready" ref="myComponent">
...
<img src="..." #load="loadedImgsLength++" /> (static image)
...
<img v-for="img in fetchedImg" #load="loadedImgsLength++" /> (dynamic images)
</div>
</template>
<script>
...
data(){
return {
...,
imgsLength : 0,
loadedImgsLength: 0,
...,
}
},
computed: {
ready(){
return (this.loadedImgsLength > 0 && this.loadedImgsLength == this.imgsLength)
}
},
async created(){
... fetch all data
this.$nextTick(() => this.$nextTick(this.imgsLength = this.$refs.myComponent.getElementsByTagName("img").length))
}
My problem is my grid won't remain repsonsive at all, or if I try to create a grid not as many columns the whole grid just looks weird. I'd like for it to stretch out to its containers width. Which is what I'd expect sizeColumsToFit To do. I'm getting a warning : ag-Grid: tried to call sizeColumnsToFit() but the grid is coming back with zero width, maybe the grid is not visible yet on the screen? But how do i even work around this. For example I load data from my reducer hence a rerender will happen, or i have a loading spinner when my data is in the state of fetching. Whats some patterns you guys take to tackle this?
Heres a example:
onGridReady(params) {
const { mode } = this.props;
this.gridApi = params.api;
this.gridApi.mode = mode;
this.columnApi = params.columnApi;
this.gridApi.addDispatcher = this.props.add;
this.gridApi.deleteDispatcher = this.props.delete;
this.gridApi.updateDispatcher = this.props.update;
console.log(window.innerWidth);
this.sizeToFit();
window.onresize = () => {
this.gridApi.sizeColumnsToFit();
};
}
Heres my Render method:
determineRender() {
const {isWaiting, columnDefs, schema } = this.props;
let { data } = this.props;
data = denormalize(data.result, schema, data.entities);
data = _.orderBy(data, ['id'], ['desc']);
if (isWaiting) {
return (
<div className={cx('grid-loading')}>
<CircularProgress className={cx('grid-progress')} color="accent" size={500} />
</div>
);
}
return (
<div className={cx('grid-full')}>
<div
className={cx('grid-full') + ' ag-theme-material'}>
<AgGridReact
columnDefs={columnDefs}
enableSorting
enableColResize
deltaRowDataMode
getRowNodeId={nodeData => nodeData.id}
rowData={data}
pagination
paginationPageSize={15}
rowSelection={this.state.rowSelection}
defaultColDef={this.state.defaultColDef}
enableFilter
enableCellChangeFlash
getRowHeight={this.state.getRowHeight}
onGridReady={this.onGridReady} />
</div>
</div>
);
}
EDIT: I suspect with all the things happening, the grid legit doesnt know the width in onGridReady since the dom hasn't finished loading however everything else is initialized. If i do something like this: setTimeout(() => this.gridApi.sizeColumnsToFit(), 500); in onGridReady(params) {...} it'll work but there will be a delay and not everyones browser runs a the same speed. Any paradigms on handling this?
You could use the onFirstDataRendered event and call sizeColumnsToFit then. You can check the docs here.
I have the following code
<marker-popup v-for="point in pointsArray" :position="point.latlng" :title="point.name" > </marker-popup>
with marker-popup defined here:
<template>
<l-marker :position="position" :title="title" :draggable="false">
<l-popup :content="text"></l-popup>
</l-marker>
</template>
<script>
export default {
name: 'MarkerPopup',
props: ['position','title'],
computed: {
text: function(){
return "<b>" + this.title + "</b><br>"
+ this.position[0] + ", " + this.position[1];
}
}
}
</script>
<style lang="scss">
</style>
pointsArray is updated here:
addPoint: function(data) {
let alreadyExists = false;
if(this.pointsDictionary[data.uid]!=undefined){
alreadyExists = true;
}
this.pointsDictionary[data.uid] = {};
this.$set(this.pointsDictionary,data.uid,{
'name': data.name,
'latlng': data.latlng,
'uid': data.uid
});
// this.pointsDictionary[data.uid]['name'] = data.name;
// this.pointsDictionary[data.uid]['latlng'] = data.latlng;
// this.points[data.uid]["marker"] = null;
if(alreadyExists){
console.log("exists");
var index = this.pointsArray.find(function(point){
return point.uid == data.uid;
});
//this.$set(this.pointsArray,index,this.pointsDictionary[data.uid]);
this.pointsArray.splice(index,1,this.pointsDictionary[data.uid]);
}
else {
this.pointsArray.push(this.pointsDictionary[data.uid]);
}
console.log(JSON.stringify(this.pointsDictionary));
console.log(JSON.stringify(this.pointsArray2()));
}
However, it does nothing to affect the v-for statement. Whenever the addPoint() method runs, it alters pointsArray in one of two ways
It pushes to the array - this works fine, v-for is perfect
It changes an element in the array according to what the Vue.js docs recommend here. This does not work at all. My console.log statements tell me that the change occurs in pointsArray - Vue does not react to that change despite me trying their recommended approach for changing arrays.
I could, I suppose, remove the element and then just push it but that approach seems clumsy and this should work according to their docs.
Turns out that I was doing everything right but the vue-leaflet package doesn't play nicely with Vue 2.0 and refuses to properly react. Switching over to this one solved all the problems.