Rendering components with data passed as parameters to a page - blazor-server-side

On the click of a button, I am rendering a component on a page. This works
very well with my code below. The component is an input box that saves
data to the database
<button type="button" #onclick="GenerateComponents" id="btClickMe">
Show Sign Up Window
</button>
<div id="groups-parent" style="display: block; min-height: 500px; position: relative; border: dashed 1px #888; ">
#foreach (var xAlert in sideItems)
{
<div class="draggable obstacle">
#xAlert
</div>
}
</div>
private async Task GenerateComponents()
{
sideItems.Add(
builder =>
{
builder.OpenComponent<AddComponent>(0);
builder.CloseComponent();
});
StateHasChanged();
}
When the page loads and there is data in the database, I will like to pass the database
data as parameters to the component and render on the page. There could be 10 of them.
With the code above, how can I achieve this? This is more like using the same page for the generation
and display of data from the database.
Let me try to explain further about what I am trying to achieve.
sideItems below is a list of renderFragment. It could have been nice if I can add database data to this
list and that will solve my problem but no I cant do that.
public List<RenderFragment> sideItems { get; set; }
When my main page loads, I want to be able to call the GetData method and return data for each component on the page that is hosting
my component
protected override void OnInitialized()
{
sideItems = new List<RenderFragment>();
//Pull GetData() here
}
public List<Tester> GetData()
{
List<Tester> data = new List<Tester>
{
new Tester{FirstName = "JJ", LastName = "PP"},
new Tester{FirstName = "HH", LastName = "KK"},
new Tester{FirstName = "KK", LastName = "LL"},
};
return data;
}
I want to be able to use this Html page to display the component with associated data on my page
and still be able to click on the button to generate new components at the same time so on my page I have
a combination of components with data from the database and new components without data from the database.
<div id="groups-parent" style="display: block; min-height: 500px; position: relative; border: dashed 1px #888; ">
#foreach (var xAlert in sideItems)
{
<div class="draggable obstacle">
#xAlert
</div>
}
</div>

Edit after clarification by OP:
Wanting to render a list of items when each item is displayed with a razor component is a very common pattern. This is how you should do it:
ListItem.razor
// logic to render Value
First Name: #Value.FirstName
Last Name: #Value.LastName
#code
{
[Parameter]
public Tester Value {get;set;}
}
Note that no particular type OR collection of component types (List<RenderFragment> or List<ListItem> Or even List<Tester>) is used.
ListHolder.razor
<div>
#ListComponents
</div>
#code
{
[Parameter]
public RenderFragment ListComponents {get;set;}
}
Index.razor
<ListHolder>
#foreach(var value in values)
{
<ListItem Value="#value />
}
</ListHolder>
#code
{
IEnumerable<Tester> values;
// some data pull function, could sit in initialize/ use click event handler etc
void PullData()
{
values = GetData();
StateHasChanged();
}
}
Earlier response:
Passing your db values as cascading parameter would let you pass data without explicitly assigning to a component. I will also suggest that you don't tinker with the builder if it is not absolutely required.
Try this:
<button type="button" #onclick="GenerateComponents" id="btClickMe">
Show Sign Up Window
</button>
<div id="groups-parent" style="display: block; min-height: 500px; position: relative; border: dashed 1px #888; ">
<CascadingValue Value="#dbValue">,
#foreach (var componentIndex in componentCount)
{
<div class="draggable obstacle">
<PaneComponent/>
</div>
}
</CascadingValue>
</div>
#code
{
int componentCount = 0;
private async Task GenerateComponents()
{
componentCount++;
StateHasChanged();
}
}

Related

ASP.Net Core MVC - Bind to List of Strings With Ability To Add/Delete

I want to bind to list of strings with ability to add new item or delete existing, similar to the UI in Azure DevOps -> Process -> Edit Work Item type... it has an a add text box/button at the top, the list of items, and the ability to delete each item with an X.
I tried to look at the html code but it seems dynamically created with javascript. Any existing solution so i don't need to reinvent the wheel? Thank you
According to your description, when the cursor focuses on the input text element, it will show the select items, and might be it implement the autocomplete function.
To achieve this effect, first, you should use JQuery Ajax to get the selectable items from the controller, then, you could attach the focusin, input and keydown events on the input text element, in these events, you could according to the entered value to add html elements, in order to show/hide the select item.
For the delete icon on each select option, you could use the Font Awesome element, and attach the click event to achieve the delete action.
Finally, to achieve the add new value feature, you could use the Bootstrap Modal to show a popup modal and insert the new value.
More detail steps, you could check the following sample code:
Controller:
//main page
public IActionResult SelectIndex()
{
return View();
}
//this method is used to get the select items.
[HttpGet]
public IActionResult GetSelectItems()
{
//initial data. You could query database and get the real data.
List<string> list = new List<string>() { "Angola", "Anguilla", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Cambodia", "Cameroon", "Canada", "Dominica", "Dominican Republic", "Ecuador", "Egypt" };
return Json(list);
}
View page (SelectIndex.cshtml):
CSS style:
<style>
* {
box-sizing: border-box;
}
body {
font: 16px Arial;
}
/*the container must be positioned relative:*/
.autocomplete {
position: relative;
display: inline-block;
}
input {
border: 1px solid transparent;
background-color: #f1f1f1;
padding: 10px;
font-size: 16px;
}
input[type=text] {
background-color: #f1f1f1;
width: 100%;
}
input[type=submit] {
background-color: DodgerBlue;
color: #fff;
cursor: pointer;
}
.autocomplete-items {
position: absolute;
border: 1px solid #d4d4d4;
border-bottom: none;
border-top: none;
z-index: 99;
/*position the autocomplete items to be the same width as the container:*/
top: 100%;
left: 0;
right: 0;
}
.autocomplete-items div {
padding: 10px;
cursor: pointer;
background-color: #fff;
border-bottom: 1px solid #d4d4d4;
}
/*when hovering an item:*/
.autocomplete-items div:hover {
background-color: #e9e9e9;
}
/*when navigating through the items using the arrow keys:*/
.autocomplete-active {
background-color: DodgerBlue !important;
color: #ffffff;
}
</style>
main html resource:
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/4.7.0/css/font-awesome.min.css">
<link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/css/bootstrap.min.css">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.5.1/jquery.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.16.0/umd/popper.min.js"></script>
<script src="https://maxcdn.bootstrapcdn.com/bootstrap/4.5.2/js/bootstrap.min.js"></script>
<!--Make sure the form has the autocomplete function switched off:-->
<form autocomplete="off">
<div class="autocomplete" style="width:300px;">
<input id="myInput" type="text" name="myCountry" placeholder="Country">
</div>
<button type="button" class="btn btn-primary" data-toggle="modal" data-target="#myModal">
<i class="fa fa-plus"></i> Add Value
</button>
<!-- The Modal -->
<div class="modal" id="myModal">
<div class="modal-dialog">
<div class="modal-content">
<!-- Modal Header -->
<div class="modal-header">
<h4 class="modal-title">Add a New Value</h4>
<button type="button" class="close" data-dismiss="modal">×</button>
</div>
<!-- Modal body -->
<div class="modal-body">
<input id="inputNewValue" type="text" placeholder="Enter a new Country">
</div>
<!-- Modal footer -->
<div class="modal-footer">
<button type="button" class="btn btn-primary" id="btnAddNew" data-dismiss="modal">Submit</button>
</div>
</div>
</div>
</div>
</form>
JavaScript script:
<script>
function autocomplete(inp, arr) {
/*the autocomplete function takes two arguments,
the text field element and an array of possible autocompleted values:*/
var currentFocus;
/*execute a function when focus on the text field*/
inp.addEventListener("focusin", function () {
var a, b, i, val = this.value;
/*close any already open lists of autocompleted values*/
closeAllLists();
if (!val) { arr = countries.slice(0, 5); }
/*create a DIV element that will contain the items (values):*/
a = document.createElement("DIV");
a.setAttribute("id", this.id + "autocomplete-list");
a.setAttribute("class", "autocomplete-items");
/*append the DIV element as a child of the autocomplete container:*/
this.parentNode.appendChild(a);
/*for each item in the array...*/
for (i = 0; i < arr.length; i++) {
/*check if the item starts with the same letters as the text field value:*/
if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
/*create a DIV element for each matching element:*/
b = document.createElement("DIV");
/*make the matching letters bold:*/
b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
b.innerHTML += arr[i].substr(val.length);
/*insert a input field that will hold the current array item's value:*/
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
b.innerHTML += "<span style='font-size: 1em; color: Tomato; float: right'><i class='fa fa-times'></i></span>"
/*execute a function when someone clicks on the item value (DIV element):*/
b.addEventListener("click", function (e) {
/*insert the value for the autocomplete text field:*/
inp.value = this.getElementsByTagName("input")[0].value;
/*close the list of autocompleted values,
(or any other open lists of autocompleted values:*/
closeAllLists();
});
b.getElementsByTagName("span")[0].addEventListener("click", function (e) {
e.stopPropagation();
var deletevalue = this.parentElement.getElementsByTagName("input")[0].value;
//delete item from the data array.
const index = countries.indexOf(deletevalue);
if (index > -1) {
countries.splice(index, 1);
}
//remove item from current
document.getElementsByClassName("autocomplete-items")[0].removeChild(this.parentElement);
//closeAllLists(e.target);
autocomplete(document.getElementById("myInput"), countries.sort());
});
a.appendChild(b);
}
}
});
/*execute a function when someone writes in the text field:*/
inp.addEventListener("input", function (e) {
var a, b, i, val = this.value;
/*close any already open lists of autocompleted values*/
closeAllLists();
if (!val) {return false;}
currentFocus = -1;
/*create a DIV element that will contain the items (values):*/
a = document.createElement("DIV");
a.setAttribute("id", this.id + "autocomplete-list");
a.setAttribute("class", "autocomplete-items");
/*append the DIV element as a child of the autocomplete container:*/
this.parentNode.appendChild(a);
/*for each item in the array...*/
for (i = 0; i < arr.length; i++) {
/*check if the item starts with the same letters as the text field value:*/
if (arr[i].substr(0, val.length).toUpperCase() == val.toUpperCase()) {
/*create a DIV element for each matching element:*/
b = document.createElement("DIV");
/*make the matching letters bold:*/
b.innerHTML = "<strong>" + arr[i].substr(0, val.length) + "</strong>";
b.innerHTML += arr[i].substr(val.length);
/*insert a input field that will hold the current array item's value:*/
b.innerHTML += "<input type='hidden' value='" + arr[i] + "'>";
b.innerHTML += "<span style='font-size: 1em; color: Tomato; float: right'><i class='fa fa-times'></i></span>"
/*execute a function when someone clicks on the item value (DIV element):*/
b.addEventListener("click", function (e) {
/*insert the value for the autocomplete text field:*/
inp.value = this.getElementsByTagName("input")[0].value;
/*close the list of autocompleted values,
(or any other open lists of autocompleted values:*/
closeAllLists();
});
b.getElementsByTagName("span")[0].addEventListener("click", function (e) {
e.stopPropagation();
var deletevalue = this.parentElement.getElementsByTagName("input")[0].value;
//delete item from the data array.
const index = countries.indexOf(deletevalue);
if (index > -1) {
countries.splice(index, 1);
}
//remove item from current
document.getElementsByClassName("autocomplete-items")[0].removeChild(this.parentElement);
//closeAllLists(e.target);
autocomplete(document.getElementById("myInput"), countries.sort());
});
a.appendChild(b);
}
}
});
/*execute a function presses a key on the keyboard:*/
inp.addEventListener("keydown", function (e) {
var x = document.getElementById(this.id + "autocomplete-list");
if (x) x = x.getElementsByTagName("div");
if (e.keyCode == 40) {
/*If the arrow DOWN key is pressed,
increase the currentFocus variable:*/
currentFocus++;
/*and and make the current item more visible:*/
addActive(x);
} else if (e.keyCode == 38) { //up
/*If the arrow UP key is pressed,
decrease the currentFocus variable:*/
currentFocus--;
/*and and make the current item more visible:*/
addActive(x);
} else if (e.keyCode == 13) {
/*If the ENTER key is pressed, prevent the form from being submitted,*/
e.preventDefault();
if (currentFocus > -1) {
/*and simulate a click on the "active" item:*/
if (x) x[currentFocus].click();
}
}
});
function addActive(x) {
/*a function to classify an item as "active":*/
if (!x) return false;
/*start by removing the "active" class on all items:*/
removeActive(x);
if (currentFocus >= x.length) currentFocus = 0;
if (currentFocus < 0) currentFocus = (x.length - 1);
/*add class "autocomplete-active":*/
x[currentFocus].classList.add("autocomplete-active");
}
function removeActive(x) {
/*a function to remove the "active" class from all autocomplete items:*/
for (var i = 0; i < x.length; i++) {
x[i].classList.remove("autocomplete-active");
}
}
function closeAllLists(elmnt) {
/*close all autocomplete lists in the document,
except the one passed as an argument:*/
var x = document.getElementsByClassName("autocomplete-items");
for (var i = 0; i < x.length; i++) {
if (elmnt != x[i] && elmnt != inp) {
x[i].parentNode.removeChild(x[i]);
}
}
}
/*execute a function when someone clicks in the document:*/
document.addEventListener("click", function (e) {
closeAllLists(e.target);
});
}
/*An array containing all the country names in the world:*/
// var countries = ["Angola", "Anguilla", "Bahamas", "Bahrain", "Bangladesh", "Barbados", "Cambodia", "Cameroon", "Canada", "Dominica", "Dominican Republic", "Ecuador", "Egypt"];
var countries;
$.get("/Home/GetSelectItems", function (response) {
countries = response;
/*initiate the autocomplete function on the "myInput" element, and pass along the countries array as possible autocomplete values:*/
autocomplete(document.getElementById("myInput"), countries.sort());
})
document.getElementById("btnAddNew").addEventListener("click", function (e) {
var newvalue = document.getElementById("inputNewValue").value;
var itemindex = countries.indexOf(newvalue);
if (itemindex == -1) {
countries.push(newvalue);
}
autocomplete(document.getElementById("myInput"), countries.sort());
});
</script>
The result like this (when focus on the input element, it will show the Top 5 items):

How to pass function to html

I'm new to vue js, so I have simple function to hide the progress bar created in methods, but doesn't seem to work, I'm wondering if I need to add event or bind it, I think it's something simple, but I can't figure it out.
methods: {
hideProgressBar: function() {
const hideProgress = document.querySelector(".progress-bar");
if (hideProgress) {
hideProgress.classList.add(hide);
} else {
hideProgress.classList.remove(hide);
}
}
}
.progress-bar {
height: 1rem;
color: #fff;
background-color: #f5a623;
margin-top: 5px;
}
.hide.progress-bar {
display: none;
}
<div class="progress-bar" role="progressbar"></div>
If you want to invoke the method when the page is loaded, add the following created option after your methods option
created: function(){
this.hideProgressBar()
}
otherwise if you want to invoke the method based on an event then you would need to add your event.
If you're using vue.js you'd want to use the inbuilt directives as much as you can.
This means you can avoid the whole hideProgressBar method.
<button #click="hideProgressBar = !hideProgressBar">Try it</button>
<div class="progress-bar" v-if="!hideProgressBar">
Progress bar div
</div>
And you script would have a data prop that would help you toggle the hide/show of the progress bar
data () {
return {
hideProgressBar: false
}
}
just try like this:
methods: {
hideProgressBar: function() {
var element = document.getElementsByClassName("progress-bar")[0];
if (element.classList.contains('hide')) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
}
}
<div class="progress-bar">
Progress bar 1 div
</div>
<div class="progress-bar hide">
Progress bar 2 div
</div>
I have used two progress bar for demonstration. Initially,
The first progress bar doesn't contain the hide class so hide class will be added.
Then the second progress already has hide class so it will be removed
DEMO:
//hides first progress bar by adding hide class.
var element = document.getElementsByClassName("progress-bar")[0];
if (element.classList.contains('hide')) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
//display second progress bar by remove hide class
var element = document.getElementsByClassName("progress-bar")[1];
if (element.classList.contains('hide')) {
element.classList.remove('hide');
} else {
element.classList.add('hide');
}
.progress-bar {
height: 1rem;
color: #fff;
background-color: #f5a623;
margin-top: 5px;
}
.hide.progress-bar {
display: none;
}
<div class="progress-bar">
Progress bar 1 div
</div>
<div class="progress-bar hide">
Progress bar 2 div
</div>

VueJS: Why parent components method unable to delete/destroy child's child (`vue2-dropzone`) component entirely?

I am creating a slider in vuejs and am using vue2-dropzone plugin for file uploads where each slide (slide-template.vue) has a vue2-dropzone component.
When app loads, image files are manually added in each vue2-dropzone (manuallyAddFile plugins API) queried from image API (hosted on heroku)
The issue is when I delete the first slide, calling the parent's (slider.vue) method removeSlideFn (passed down to child as prop) from child (slide-template.vue) component first slide is deleted but not entirely the dropzone images of the first slides are not destroyed and remains in the DOM, instead images of slide2, (the next slide) are deleted from the DOM (Pls try it once on codesandbox demo to actually know what I am mean). This does not happen when I delete slide2 or slide3 but only on slide1.
CodeSandBox Demo
App.vue
<template>
<div id="app">
<img width="15%" src="./assets/logo.png">
<slider />
</div>
</template>
<script>
import slider from "./components/slider";
export default {
name: "App",
components: {
slider
}
};
</script>
components\slider.vue (parent)
<template>
<div>
<hooper ref="carousel" :style="hooperStyle" :settings="hooperSettings">
<slide :key="idx" :index="idx" v-for="(slideItem, idx) in slideList">
<slide-template
:slideItem="slideItem"
:slideIDX="idx"
:removeSlideFn="removeCurrSlide" />
</slide>
<hooper-navigation slot="hooper-addons"></hooper-navigation>
<hooper-pagination slot="hooper-addons"></hooper-pagination>
</hooper>
<div class="buttons has-addons is-centered is-inline-block">
<button class="button is-info" #click="slidePrev">PREV</button>
<button class="button is-info" #click="slideNext">NEXT</button>
</div>
</div>
</template>
<script>
import {
Hooper,
Slide,
Pagination as HooperPagination,
Navigation as HooperNavigation
} from "hooper";
import "hooper/dist/hooper.css";
import slideTemplate from "./slide-template.vue";
import { slideShowsRef } from "./utils.js";
export default {
data() {
return {
sliderRef: "SlideShow 1",
slideList: [],
hooperSettings: {
autoPlay: false,
centerMode: true,
progress: true
},
hooperStyle: {
height: "265px"
}
};
},
methods: {
slidePrev() {
this.$refs.carousel.slidePrev();
},
slideNext() {
this.$refs.carousel.slideNext();
},
//Removes slider identified by IDX
removeCurrSlide(idx) {
this.slideList.splice(idx, 1);
},
// Fetch data from firebase
getSliderData() {
let that = this;
let mySliderRef = slideShowsRef.child(this.sliderRef);
mySliderRef.once("value", snap => {
if (snap.val()) {
this.slideList = [];
snap.forEach(childSnapshot => {
that.slideList.push(childSnapshot.val());
});
}
});
}
},
watch: {
getSlider: {
handler: "getSliderData",
immediate: true
}
},
components: {
slideTemplate,
Hooper,
Slide,
HooperPagination,
HooperNavigation
}
};
</script>
components/slide-template.vue (child, with vue2-dropzone)
<template>
<div class="slide-wrapper">
<slideTitle :heading="slideItem.heading" />
<a class="button delete remove-curr-slide" #click="deleteCurrSlide(slideIDX)" ></a>
<vue2Dropzone
#vdropzone-file-added="fileWasAdded"
#vdropzone-thumbnail="thumbnail"
#vdropzone-mounted="manuallyAddFiles(slideItem.zones)"
:destroyDropzone="false"
:include-styling="false"
:ref="`dropZone${ slideIDX }`"
:id="`customDropZone${ slideIDX }`"
:options="dropzoneOptions">
</vue2Dropzone>
</div>
</template>
<script>
import slideTitle from "./slide-title.vue";
import vue2Dropzone from "#dkjain/vue2-dropzone";
import { generate_ObjURLfromImageStream, asyncForEach } from "./utils.js";
export default {
props: ["slideIDX", "slideItem", "removeSlideFn"],
data() {
return {
dropzoneOptions: {
url: "https://vuejs-slider-node-lokijs-api.herokuapp.com/imageUpload",
thumbnailWidth: 150,
autoProcessQueue: false,
maxFiles: 1,
maxFilesize: 2,
addRemoveLinks: true,
previewTemplate: this.template()
}
};
},
components: {
slideTitle,
vue2Dropzone
},
methods: {
template: function() {
return `<div class="dz-preview dz-file-preview">
<div class="dz-image">
<img data-dz-thumbnail/>
</div>
<div class="dz-details">
<!-- <div class="dz-size"><span data-dz-size></span></div> -->
<!-- <div class="dz-filename"><span data-dz-name></span></div> -->
</div>
<div class="dz-progress"><span class="dz-upload" data-dz-uploadprogress></span></div>
<div class="dz-error-message"><span data-dz-errormessage></span></div>
<div class="dz-success-mark"><i class="fa fa-check"></i></div>
<div class="dz-error-mark"><i class="fa fa-close"></i></div>
</div>`;
},
thumbnail: function(file, dataUrl) {
var j, len, ref, thumbnailElement;
if (file.previewElement) {
file.previewElement.classList.remove("dz-file-preview");
ref = file.previewElement.querySelectorAll("[data-dz-thumbnail]");
for (j = 0, len = ref.length; j < len; j++) {
thumbnailElement = ref[j];
thumbnailElement.alt = file.name;
}
thumbnailElement.src = dataUrl;
return setTimeout(
(function(_this) {
return function() {
return file.previewElement.classList.add("dz-image-preview");
};
})(this),
1
);
}
},
// Drag & Drop Events
async manuallyAddFiles(zoneData) {
if (zoneData) {
let dropZone = `dropZone${this.slideIDX}`;
asyncForEach(zoneData, async fileInfo => {
var mockFile = {
size: fileInfo.size,
name: fileInfo.originalName || fileInfo.name,
type: fileInfo.type,
id: fileInfo.id,
childZoneId: fileInfo.childZoneId
};
let url = `https://vuejs-slider-node-lokijs-api.herokuapp.com/images/${
fileInfo.id
}`;
let objURL = await generate_ObjURLfromImageStream(url);
this.$refs[dropZone].manuallyAddFile(mockFile, objURL);
});
}
},
fileWasAdded(file) {
console.log("Successfully Loaded Files from Server");
},
deleteCurrSlide(idx) {
this.removeSlideFn(idx);
}
}
};
</script>
<style lang="scss">
.slide-wrapper {
position: relative;
}
[id^="customDropZone"] {
background-color: orange;
font-family: "Arial", sans-serif;
letter-spacing: 0.2px;
/* color: #777; */
transition: background-color 0.2s linear;
// height: 200px;
padding: 40px;
}
[id^="customDropZone"] .dz-preview {
width: 160px;
display: inline-block;
}
[id^="customDropZone"] .dz-preview .dz-image {
width: 80px;
height: 80px;
margin-left: 40px;
margin-bottom: 10px;
}
[id^="customDropZone"] .dz-preview .dz-image > div {
width: inherit;
height: inherit;
// border-radius: 50%;
background-size: contain;
}
[id^="customDropZone"] .dz-preview .dz-image > img {
width: 100%;
}
[id^="customDropZone"] .dz-preview .dz-details {
color: white;
transition: opacity 0.2s linear;
text-align: center;
}
[id^="customDropZone"] .dz-success-mark,
.dz-error-mark {
display: none;
}
.dz-size {
border: 2px solid blue;
}
#previews {
border: 2px solid red;
min-height: 50px;
z-index: 9999;
}
.button.delete.remove-curr-slide {
padding: 12px;
margin-top: 5px;
margin-left: 5px;
position: absolute;
right: 150px;
background-color: red;
}
</style>
slide-title.vue (not that important)
<template>
<h2 contenteditable #blur="save"> {{ heading }} </h2>
</template>
<script>
export default {
props: ["heading"],
methods: {
save() {
this.$emit("onTitleUpdate", event.target.innerText.trim());
}
}
};
</script>
utils.js (utility)
export async function generate_ObjURLfromImageStream(url) {
return await fetch(url)
.then(response => {
return response.body;
})
.then(rs => {
const reader = rs.getReader();
return new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await reader.read();
// When no more data needs to be consumed, break the reading
if (done) {
break;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
}
// Close the stream
controller.close();
reader.releaseLock();
}
});
})
// Create a new response out of the stream
.then(rs => new Response(rs))
// Create an object URL for the response
.then(response => {
return response.blob();
})
.then(blob => {
// generate a objectURL (blob:url/<uuid> list)
return URL.createObjectURL(blob);
})
.catch(console.error);
}
Technically this is how the app works, slider.vue loads & fetches data from database (firebase) and stores in a data array slideList, loops over the slideList & passes each slideData (prop slideItem) to vue-dropzone component (in slide-template.vue), when dropzone mounts it fires the manuallyAddFiles(slideItem.zones) on the #vdropzone-mounted custom event.
The async manuallyAddFiles() fetches image from an API (hosted on heroku), creates (generate_ObjURLfromImageStream(url)) a unique blob URL for the image (blob:/) and then calls plugins API dropZone.manuallyAddFile() to load the image into the corresponding dropzone.
To delete the current slide, child's deleteCurrSlide() calls parent's (slider.vue) removeSlideFn (passed as prop) method with the idx of current slide. The removeSlideFn use splice to remove the item at the corresponding array idx this.slideList.splice(idx, 1).
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
CodeSandBox Demo
I am not sure what is causing the issue, may it's due to something in the vue's reactivity system OR Vue's Array reactivity caveat that is causing this.
Can anybody pls help me understand & resolve this and if possible point out the reason to the root of the problem.
Your help is much appreciated.
Thanks,
I think you probably missunderstand what is going on:
In VueJS there is a caching method which allow the reusing of existing component generated: - Each of your object are considered equals when rendered (at a DOM level).
So VueJS remove the last line because it is probably ask the least calculation and then recalcul the expected state. There are many side case to this (sometime, the local state is not recalculated). To avoir this: As recommended in the documentation, use :key to trace the id of your object. From the documentation:
When Vue is updating a list of elements rendered with v-for, by default it uses an “in-place patch” strategy. If the order of the data items has changed, instead of moving the DOM elements to match the order of the items, Vue will patch each element in-place and make sure it reflects what should be rendered at that particular index. This is similar to the behavior of track-by="$index" in Vue 1.x.
This default mode is efficient, but only suitable when your list render output does not rely on child component state or temporary DOM state (e.g. form input values).
To give Vue a hint so that it can track each node’s identity, and thus reuse and reorder existing elements, you need to provide a unique key attribute for each item. An ideal value for key would be the unique id of each item. This special attribute is a rough equivalent to track-by in 1.x, but it works like an attribute, so you need to use v-bind to bind it to dynamic values...
new Vue({
el: "#app",
data: {
counterrow: 1,
rows: [],
},
methods: {
addrow: function() {
this.counterrow += 1;
this.rows.push({
id: this.counterrow,
model: ""
});
},
removerows: function(index) {
this.rows.splice(index, 1);
},
},
});
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.5.16/vue.js"></script>
<div id="app">
<table>
<tr>
<td><input type="text" name="test1" /></td>
<td><button class="btn" #click="addrow">add row</button></td>
</tr>
<tr v-for="(row,index) in rows" :key="row.id">
<td><input type="text" name="test2" v-model="row.model" /></td>
<td><button class="btn" #click="removerows(index)">remove </button></td>
</tr>
</table>
</div>
In this code:
I corrected the fact counterrow was never incremented
I added a :key
The documentation of :key
What did you mean by
The problem is when I delete the first slide, first slide is deleted but not entirely, the dropzone images of the first slides are not destroyed and still remains in the DOM, instead the images of slide2, (the next slide) are deleted from the DOM.
From what I see, the elements are no longer in the DOM

building a stock ticker widget on jive api error

I added a real-time stock widget to a client’s Jive site that is using an HTML widget. The code uses the Jive native jQuery library to pull JSONP data from Yahoo using their YQL API. This widget is written to work with only 1 stock symbol. If requested, I can modify it to pull in multiple symbols.
I keep getting an error for if (res.query.results). It says it is undefined. If anybody knows of a good stock HTML widget with just price and name of symbol: Please help. I am digging deep in Google.
Console output:
render-widget.jspa?size=1&frameID=262901&widgetType=7&containerID=1327&containerType=700&inFrame=1:173 Uncaught TypeError: Cannot read property 'results' of undefined
at Object.success (render-widget.jspa?size=1&frameID=262901&widgetType=7&containerID=1327&containerType=700&inFrame=1:173)
at j (jquery.min.js:2)
at Object.fireWith [as resolveWith] (jquery.min.js:2)
at x (jquery.min.js:4)
at HTMLScriptElement.b.onload.b.onreadystatechange (jquery.min.js:4)
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.11.1/jquery.min.js"></script>
<script type="text/javascript">
// This version has been tested to work in Jive 4.0.15 and 5.0. It should work in Jive 4.5 but has not been tested
// Add the stock symbol here
var yourStockSymbol = 'LIFE';
</script>
<div id="stock_miniQuote_head" class="ajaxtrigger"><span id="stockSymbol"></span> (common stock)</div>
<div id="stock_miniQuote">
<div id="stockIndicator"><p>Retrieving stock information...</p></div>
<div class="stock_divider">
<div id="stock_left">
<span class="stock_label">Price</span><br/>
<strong class="stock_strong">$<span id="stockAsk"></span></strong><br/>
</div>
<div id="stock_right">
<span class="stock_label">Change</span><br/>
<strong class="stock_strong"><span id="stockChange"></span></strong><br />
<strong class="stock_strong"><span id="stockChangePercent"></span></strong><br />
</div>
<div style="clear: both;"></div>
</div>
<div id="stock_body">
<div id="stock_body_content">
<span class="stock_label">Volume</span><br/>
<strong class="stock_strong"><span id="stockVolume"></span></strong>
<br /><br />
<span class="stock_label">Average Daily Volume</span><br/>
<strong class="stock_strong"><span id="stockAvgVolume"></span></strong>
<br /><br />
<span class="stock_label">52 Week Range</span><br/>
<strong class="stock_strong"><span id="stockRange"></span></strong>
</div>
<div style="clear: both;"></div>
</div>
</div>
<style>
#stockIndicator {
text-align:left;
padding: 10px;
margin: 5px;
color: red;
}
.ajaxtrigger:hover {
cursor: pointer;
cursor: hand;
}
#stock_miniQuote_head {
background-color:#464A55;
color:#FFFFFF;
font-size:14px;
font-weight:bold;
padding-bottom:10px;
padding-left:10px;
padding-right:10px;
padding-top:10px;
}
#stock_miniQuote {
border-bottom-color:#DDDDDD;
border-bottom-left-radius:5px 5px;
border-bottom-right-radius:5px 5px;
border-bottom-style:solid;
border-bottom-width:1px;
border-left-color:#DDDDDD;
border-left-style:solid;
border-left-width:1px;
border-right-color:#DDDDDD;
border-right-style:solid;
border-right-width:1px;
border-top-color:initial;
border-top-style:none;
border-top-width:initial;
list-style-type:none;
margin-bottom:10px;
padding-bottom:0;
padding-top:10px;
vertical-align:text-top;
height: 100%;
width: 99%;
}
.stock_divider {
border-bottom:1px solid #B2B0AD; padding-bottom:5px;
}
#stock_left {
float:left; width:35%; height:50px; border-right:1px solid #B2B0AD; padding:0 15px;
}
#stock_right {
float:right; width:*; padding:0 20px; vertical-align:text-top;
}
.stock_label {
font-size:14px;
}
.stock_strong {
font-size:17px;
}
#stock_body {
padding:10px 0 15px;
}
#stock_body_content {
float:left; width:170px; padding:0 15px;
}
</style>
<script type="text/javascript">
if ($('#jive-widgets-browser').css('display') == 'block') {
// Do Nothing as we are in edit mode
} else {
// Build the URL to Yahoo YQL services
var q = escape('select * from yahoo.finance.quotes where symbol in ("' + yourStockSymbol + '")');
var theURL = "https://query.yahooapis.com/v1/public/yql?q=" + q + "&format=json&diagnostics=false&env=http%3A%2F%2Fdatatables.org%2Falltables.env&callback=?";
$(document).ready(function(){
// Load function on launch
$("#stockIndicator").show();
doAjax(theURL);
// Function for refreshing the stock by clicking on the title header
$('.ajaxtrigger').click(function(){
$("#stockIndicator").show();
doAjax(theURL);
return false;
});
// Function to add commas to numbers for volume
function numberWithCommas(x) {
return x.toString().replace(/\B(?=(?:\d{3})+(?!\d))/g, ",");
}
// Main function to make JSON request to Yahoo for stock information
function doAjax(url){
$.ajax({
url: url,
dataType: 'jsonp',
success: function(data){
var s = data.query.results;
if(s){
if(s.quote.Change > 0) {
// Change the change text to green
$('#stockChange').css({'color': 'green'});
$('#stockChangePercent').css({'color': 'green'});
} else {
// Change the change text to red
$('#stockChange').css({'color': 'red'});
$('#stockChangePercent').css({'color': 'red'});
}
// This is where we add the JSON values back into the HTML above
$('#stockSymbol').html(s.quote.symbol);
$('#stockAsk').html(s.quote.LastTradePriceOnly);
$('#stockChange').html(s.quote.Change);
$('#stockChangePercent').html(s.quote.ChangeinPercent);
$('#stockVolume').html(numberWithCommas(s.quote.Volume));
$('#stockAvgVolume').html(numberWithCommas(s.quote.AverageDailyVolume));
$('#stockRange').html(s.quote.YearRange);
$("#stockIndicator").hide();
} else {
var errormsg = '<p>Error: could not load the page.</p>';
$("#stockIndicator").show();
$("#stockIndicator").html(errormsg);
}
}
});
}
}); //end ready function
} //end first else
</script>
The Yahoo Finance APi was turned off earlier this year, including accessing this information via YQL queries. You will need to find an alternate service and update our code accordingly.
There was another thread about this here, Yahoo Finance API changes (2017)

Tab switching causes a little deformation during animation

I made a simple vertical content slider based on tabs and found optical issue when animating . If you click on different tab, current content slides up and in the same time new one slides down. All contents have same height. The problem is that the height is changing a little bit during the animation (focus on bottom border). I don't really know how to fix it. Is there any way to prevent it?
Here is the complete code:
http://jsfiddle.net/YGY26/8/
JS:
var active = 1;
function item(id) {
if (id !== active) {
$("#description" + active).slideUp("slow");
if (active !== id) {
$("#description" + id).slideDown("slow");
active = id;
}
}
}
HTML:
<div class='slider_box' onclick='item(1);'>
<h3>Content1</h3>
<div id='description1' class='slider_content' style='display: block'>
some content to show
</div>
</div>
<div class='slider_box' onclick='item(2);'>
<h3>Content2</h3>
<div id='description2' class='slider_content'>
some content to show
</div>
</div>
<div class='slider_box' onclick='item(3);'>
<h3>Content3</h3>
<div id='description3' class='slider_content'>
some content to show
</div>
</div>
Basic CSS:
.slider_box {
position: relative;
width: 330px;
}
.slider_box h3 {
color: white;
background: black;
}
.slider_content {
height: 330px;
display: none;
}