javascript promise operation to update HTML - javascript

I think I understand promises, but I can't seem to get my head around it correctly. I have an application built in SharePoint 2013 that does a REST call via sprLib to populate an object. The object can be manipulated on the page, either entries added/deleted/updated, basically full CRUD. Then the changes are pushed back to the list. So far, this is all working smoothly. What I am trying to do now is on the update, empty the div tag, then call the initial load function again to populate the div with the new current list. Right now, either I get an error on the promise syntax or race condition where the initial load function runs again before the update finishes, so the populated list is the old list. I created a basic version to simulate the idea, minus the REST updates since I couldn't find a way to replicate that in a sandbox.
https://jsfiddle.net/36muca0g/
Long story short, 3 things should happen: 1) initial load 2) changes made 3) re-load data and empty/append html accordingly.
const fullList = [ //In real environment, this is populate via REST operation.
{ id: "12", title: "A", code: "110" },
{ id: "23", title: "B", code: "120" },
{ id: "13", title: "C", code: "130" },
{ id: "43", title: "D", code: "140" },
{ id: "52", title: "E", code: "150" },
]
$(document).ready(function () {
initialLoad(fullList);
$('#updateList').click(function() {
let item = $('#txtValue').val();
addToList(item).then(function(result){
console.log('add complete');
initialLoad(fullList);
console.log('reloaded');
})
.catch(function(){
console.log('fail');
})
});
})
function initialLoad(list) {
//In real environment, this is a getItem operation using sprLib.
$('#itemList').empty();
let listHTML = ''
for (i = 0; i < list.length; i++) {
listHTML += '<p>'+list[i].title+' ' +list[i].code+'</p>'
}
$('#itemList').append(listHTML);
}
function addToList(entry) {
return new Promise(function(resolve, reject) {
console.log('I go first');
fullList.push({ 'id': "", 'title': entry, 'code': ""}); //In real environment, this is an CRUD operation using sprLib.
setTimeout(() => resolve("done"), 5000);
});
}
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Promise</title>
<script src="https://cdn.jsdelivr.net/npm/bootstrap#5.2.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-pprn3073KE6tl6bjs2QrFaJGz5/SUsLqktiwsUTF55Jfv3qYSDhgCecCxMW52nD2" crossorigin="anonymous"></script>
<script src="https://code.jquery.com/jquery-3.6.0.min.js" type="text/javascript"></script>
<script src="https://cdn.datatables.net/1.12.1/js/jquery.dataTables.min.js" type="text/javascript"></script>
<script src="/scripts/promise.js"></script>
<link href="https://cdn.datatables.net/1.12.1/css/jquery.dataTables.min.css" rel="stylesheet">
<link href="https://cdn.datatables.net/select/1.4.0/css/select.dataTables.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap#5.2.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-0evHe/X+R7YkIZDRvuzKMRqM+OrBnVFBL6DOitfPri4tjfHxaWutUpFmBp4vmVor" crossorigin="anonymous">
</head>
<body><br><br>
<div class="container text-center row">
<div class="card col-2 bg-primary text-light" id="itemList">
</div>
<div class="card col-2 bg-secondary"><input type="text" class="form" id="txtValue">
<button type="button" class="btn btn-success" id="updateList">Submit</button></div>
</div>
</body>
</html>

The way i see it, you need a proper "trigger" to replace that ugly setTimeout() in a way that you allow to refresh the view only when the new data is accessible.
If you're working your application around a MCV pattern, what you could do is attach the state of your data onto an element inside the DOM, and listen for this element's state changes and use that to trigger the reload your fresh data.
I handled the case recently by building a drag-&-drop functionality where i wanted to display the content of the file only and only when the reader terminated the load task.
controller.js
const loadState = async function (type, input) {
return new Promise((resolve, reject) => {
try {
const reader = new FileReader();
if (type === "drop") {
const data = state.fileItem[0];
for (let i = 0; i < data.length; i++) {
if (data[i].kind === "file") {
const file = data[i].getAsFile();
reader.onload = async function (e) {
resolve((state.partNumber = createArr(e.target.result)));
};
reader.readAsText(file);
}
}
}
} catch (err) {
console.log(err);
}
});
};
eventHandlers.dropZone.addEventListener("drop", handleDropImport, false);
async function handleDropImport(e) {
try {
e.preventDefault();
if (!e.dataTransfer.items) return;
state.fileItem.push(e.dataTransfer.items);
await loadState("drop");
eventHandlers.queryPreview.classList.add("uploaded");
} catch (err) {
console.log(err);
}
}
As soon as the handleDropImport() is managed and only after the data is loaded inside the state (thanks to async/await), we add this class to the element of your choice...
eventHandlers.queryPreview.classList.add("uploaded");
You can then scan the DOM for that classList.add in your view.js using a mutation observer, and make it reachable using a publisher/subscriber pattern.
view.js
_addHandlerRender(handler) {
const options = { attributes: true, childList: true, subtree: true };
const observer = new MutationObserver((mutations) =>
mutations.forEach((mut) => {
if (mut.type === "attributes" && mut.attributeName === "class") {
handler();
}
})
);
observer.observe(this._parentElement, options);
}
model.js
const controlPreview = async function () {
try {
previewViews._clearPreview();
previewViews.render(model.state.partNumber);
} catch (err) {
console.log(err);
}
};
const init = function () {
previewViews._addHandlerRender(controlPreview);
};
init();

Related

vue.js - watch when disabled element and return data

I have two style sheets, light and dark themes, with dark disabled by default.
When clicking on a button, this toggles the themes (activating one when disabling the other) and this works well.
Now I want a function that can monitor when the #themelight stylesheet is disabled and return { themeLight: false } in the data (that will later be used with some "v-if" in the template). It probably needs to be watched as any changes in the #themelight need to be reflected in the data again.
My code seems wrong
<head>
<link rel="stylesheet" href="themelight.css" id="themelight">
<link rel="stylesheet" href="themedark.css" id="themedark" disabled>
</head>
<body>
<button v-on:click="themeToggle"></button>
</body>
<script>
export default {
data() {
return {
themeLight: true
}
},
methods: {
themeToggle() {
document.querySelector('#themelight').toggleAttribute('disabled');
document.querySelector('#themedark').toggleAttribute('disabled');
},
currentTheme() {
document.querySelector("#themelight");
addEventListener("change", function (event) {
if (event.target.disabled) {
this.themeLight = false;
} else {
this.themeLight = true;
}
});
}
},
watch: {
themeLight() {
this.currentTheme();
}
},
created() {
this.currentTheme();
}
}
</script>
First of all if you are using Vue.js then try to avoid the vanilla javascript by that i mean you should not use the document.querySelector etc..
<head>
<link rel="stylesheet" href="themedark.css" :disabled="themeLight">
<link rel="stylesheet" href="themelight.css" :disabled="!themeLight">
</head>
<body>
<button #click="themeToggle"></button>
</body>
<script>
export default {
data: () => ({
themeLight: true
}),
methods: {
themeToggle() {
this.themeLight = !this.themeLight
}
}
}
</script>
Now you do not need to monitor changed in the themeLight variable since you are changing it yourself on the button click
If the above code do not works than notify me, i have another solution.
Your use case seems weird to me, but you can use computed property.
computed: {
themeLight: function () {
return window.themeLight.disabled
}
}
Enhance your watch method and prevent it to be blocked for infinite loop.
It is a good practice to compare newValue with the oldValue.
watch: {
themeLight(newValue, oldValue) {
if(newValue && newValue!=oldValue) {
this.currentTheme();
}
}
}

Vuex getters + dev tools

I started working on a large project that uses extensive number of vuex getters. The problem is, that large amount of them rely to be called in specific order or at specific time to have correct data from other getters. Looks like vue dev tools is trying to access those values, calling the getters not in expected order so some values are not correct and the app is throwing errors. Because of this I cannot use the dev tools and cannot find a simple way around it.
Here is simple snippet reproducing the issue.
This code will run just fine, but once you open dev tools, it tries to access the getter selselectedPrice which returns error until we have some data.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vuex#3.1.1/dist/vuex.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
</head>
<body>
<div id="app">
<ul>
<li v-for="item in goodThings" :key="item.title">{{ item.title }}</li>
</ul>
{{totalPrice}}
</div>
<script>
const getSomeRandomData = () => {
return new Promise((resolve) => {
setTimeout(() => {
resolve([
{
title: 'fasfasf',
price: 20
},
{
title: 'asgfasgasg',
price: 30
},
{
title: 'asgasgasg',
price: 40
}
])
}, 1000)
})
}
const store = new Vuex.Store({
state: {
selectedItem: undefined,
warehouse: []
},
actions: {
fetchData: async function ({ commit }) {
const response = await getSomeRandomData()
commit("SET_WAREHOUSE", response)
}
},
mutations: {
SET_WAREHOUSE: (state, payload) => {
state.warehouse = [...payload]
}
},
getters: {
selselectedPrice: (state) => {
return state.warehouse[state.selectedItem].price
},
goodThings: (state) => {
return state.warehouse
},
totalPrice: (state) => {
return state.warehouse.reduce((a, item) => a+item.price, 0)
}
}
})
Vue.use(Vuex)
let app = new Vue({
el: '#app',
store,
created() {
this.$store.dispatch('fetchData')
},
computed: {
goodThings() {
return this.$store.getters.goodThings
},
totalPrice() {
return this.$store.getters.totalPrice
}
}
})
</script>
</body>
</html>

VueJS / JS DOM Watch / Observer in a multi phase render scenario

Scenario:
I’m developing a Vue scroll component that wraps around a dynamic number of HTML sections and then dynamically builds out vertical page navigation allowing the user to scroll or jump to page locations onScroll.
Detail:
a. In my example my scroll component wraps 3 sections. All section id’s start with "js-page-section-{{index}}"
b. The objective is to get the list of section nodes (above) and then dynamically build out vertical page (nav) navigation based on the n number of nodes found in the query matching selector criteria. Therefore, three sections will result in three page section navigation items. All side navigation start with “js-side-nav-{{index}}>".
c. Once the side navigation is rendered I need to query all the navigation nodes in order to control classes, heights, display, opacity, etc. i.e document.querySelectorAll('*[id^="js-side-nav"]');
EDIT
Based on some research here are the options for my problem. Again my problem being 3 phase DOM state management i.e. STEP 1. Read all nodes equal to x, then STEP 2. Build Side Nav scroll based on n number of nodes in document, and then STEP 3. Read all nav nodes to sync with scroll of document nodes:
Create some sort of event system is $emit() && $on. In my opinion this gets messy very quickly and feels like a poor solution. I found myself quickly jumping to $root
Vuex. but that feels like an overkill
sync. Works but really that is for parent child property state management but that again requires $emit() && $on.
Promise. based service class. This seems like the right solution, but frankly it became a bit of pain managing multiple promises.
I attempted to use Vue $ref but frankly it seems better for managing state rather than multi stage DOM manipulation where a observer event approach is better.
The solution that seems to work is Vues $nextTick(). which seems to be similar to AngularJS $digest. In essence it is a . setTimeout(). type approach just pausing for next digest cycle. That said there is the scenario where the tick doesn’t sync the time requires so I built a throttle method. Below is the code update for what is worth.
The refactored watch with nextTick()
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
The REFACTORED Vue component
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="sideNavPrefix + '-' + (index + 1)"
v-for="(item, key,index) in page.sections">
<a :href="'#' + getAttribute(item,'id')">
<p class="nav__counter" v-text="('0' + (index + 1))"></p>
<h3 class="nav__title" v-text="getAttribute(item,'data-title')"></h3>
<p class="nav__body" v-text="getAttribute(item,'data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
import ScrollPageService from '../services/ScrollPageService.js';
const _S = "section", _N = "sidenavs";
export default {
name: "ScrollSection",
props: {
nodeId: {
type: String,
required: true
},
sideNavActive: {
type: Boolean,
default: true,
required: false
},
sideNavPrefix: {
type: String,
default: "js-side-nav",
required: false
},
sideNavClass: {
type: String,
default: "active",
required: false
},
sectionClass: {
type: String,
default: "inview",
required: false
}
},
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
}
},
},
data: function () {
return {
scrollService: {},
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
getAttribute: function(element, key) {
return element.getAttribute(key);
},
updateViewPort: function() {
if (this.scrollService.isInCurrent(window.scrollY)) return;
[this.page.sections, this.page.sidenavs] = this.scrollService.updateNodeList(window.scrollY);
},
handleScroll: function(evt, el) {
if ( !(this.isScrollInstance()) ) {
return this.$nextTick(this.inViewportInit);
}
this.updateViewPort();
},
getNodeList: function(key) {
this.page[key] = this.scrollService.getNodeList(key);
},
isScrollInstance: function() {
return this.scrollService instanceof ScrollPageService;
},
sideNavInit: function() {
if (this.isScrollInstance() && this.scrollService.navInit(this.sideNavPrefix, this.sideNavClass)) this.getNodeList(_N);
},
inViewportInit: function() {
if (!(this.isScrollInstance()) && ((this.scrollService = new ScrollPageService(this.nodeId, this.sectionClass)) instanceof ScrollPageService)) this.getNodeList(_S);
},
isNodeList: function(nodes) {
return NodeList.prototype.isPrototypeOf(nodes);
},
},
watch: {
'page.sections': {
handler(nodeList, oldNodeList){
if (this.isNodeList(nodeList) && _.size(nodeList) && this.sideNavActive) {
return this.$nextTick(this.sideNavInit);
}
},
deep: true
},
},
mounted() {
return this.$nextTick(this.inViewportInit);
},
}
</script>
END EDIT
ORIGINAL POST
Problem & Question:
PROBLEM:
The query of sections and render of navs work fine. However, querying the nav elements fails as the DOM has not completed the render. Therefore, I’m forced to use a setTimeout() function. Even if I use a watch I’m still forced to use timeout.
QUESTION:
Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them? Example in AngularJS we might use $observe
HTML EXAMPLE
<html>
<head></head>
<body>
<scroll-section>
<div id="js-page-section-1"
data-title="One"
data-body="One Body">
</div>
<div id="js-page-section-2"
data-title="Two"
data-body="Two Body">
</div>
<div id="js-page-section-3"
data-title="Three"
data-body="THree Body">
</div>
</scroll-section>
</body>
</html>
Vue Compenent
<template>
<div v-scroll="handleScroll">
<nav class="nav__wrapper" id="navbar-example">
<ul class="nav">
<li role="presentation"
:id="[idOfSideNav(key)]"
v-for="(item, key,index) in page.sections.items">
<a :href="getId(item)">
<p class="nav__counter">{{key}}</p>
<h3 class="nav__title" v-text="item.getAttribute('data-title')"></h3>
<p class="nav__body" v-text="item.getAttribute('data-body')"></p>
</a>
</li>
</ul>
</nav>
<slot></slot>
</div>
</template>
<script>
export default {
name: "ScrollSection",
directives: {
scroll: {
inserted: function (el, binding, vnode) {
let f = function(evt) {
_.forEach(vnode.context.page.sections.items, function (elem,k) {
if (window.scrollY >= elem.offsetTop && window.scrollY <= (elem.offsetTop + elem.offsetHeight)) {
if (!vnode.context.page.sections.items[k].classList.contains("in-viewport") ) {
vnode.context.page.sections.items[k].classList.add("in-viewport");
}
if (!vnode.context.page.sidenavs.items[k].classList.contains("active") ) {
vnode.context.page.sidenavs.items[k].classList.add("active");
}
} else {
if (elem.classList.contains("in-viewport") ) {
elem.classList.remove("in-viewport");
}
vnode.context.page.sidenavs.items[k].classList.remove("active");
}
});
if (binding.value(evt, el)) {
window.removeEventListener('scroll', f);
}
};
window.addEventListener('scroll', f);
},
},
},
data: function () {
return {
page: {
sections: {},
sidenavs: {}
}
}
},
methods: {
handleScroll: function(evt, el) {
// Remove for brevity
},
idOfSideNav: function(key) {
return "js-side-nav-" + (key+1);
},
classOfSideNav: function(key) {
if (key==="0") {return "active"}
},
elementsOfSideNav:function() {
this.page.sidenavs = document.querySelectorAll('*[id^="js-side-nav"]');
},
elementsOfSections:function() {
this.page.sections = document.querySelectorAll('*[id^="page-section"]');
},
},
watch: {
'page.sections': function (val) {
if (_.has(val,'items') && _.size(val.items)) {
var self = this;
setTimeout(function(){
self.elementsOfSideNavs();
}, 300);
}
}
},
mounted() {
this.elementsOfSections();
},
}
</script>
I hope I can help you with what I'm going to post here. A friend of mine developed a function that we use in several places, and reading your question reminded me of it.
"Is there a promise or observer in Vue or JS I can use to check to see when the DOM has finished rendering the nav elements so that I can then read them?"
I thought about this function (source), here below. It takes a function (observe) and tries to satisfy it a number of times.
I believe you can use it at some point in component creation or page initialization; I admit that I didn't understand your scenario very well. However, some points of your question immediately made me think about this functionality. "...wait for something to happen and then make something else happen."
<> Credits to #Markkop the creator of that snippet/func =)
/**
* Waits for object existence using a function to retrieve its value.
*
* #param { function() : T } getValueFunction
* #param { number } [maxTries=10] - Number of tries before the error catch.
* #param { number } [timeInterval=200] - Time interval between the requests in milis.
* #returns { Promise.<T> } Promise of the checked value.
*/
export function waitForExistence(getValueFunction, maxTries = 10, timeInterval = 200) {
return new Promise((resolve, reject) => {
let tries = 0
const interval = setInterval(() => {
tries += 1
const value = getValueFunction()
if (value) {
clearInterval(interval)
return resolve(value)
}
if (tries >= maxTries) {
clearInterval(interval)
return reject(new Error(`Could not find any value using ${tries} tentatives`))
}
}, timeInterval)
})
}
Example
function getPotatoElement () {
return window.document.querySelector('#potato-scroller')
}
function hasPotatoElement () {
return Boolean(getPotatoElement())
}
// when something load
window.document.addEventListener('load', async () => {
// we try sometimes to check if our element exists
const has = await waitForExistence(hasPotatoElement)
if (has) {
// and if it exists, we do this
doThingThatNeedPotato()
}
// or you could use a promise chain
waitForExistence(hasPotatoElement)
.then(returnFromWaitedFunction => { /* hasPotatoElement */
if (has) {
doThingThatNeedPotato(getPotatoElement())
}
})
})

How to use Neutralinojs os.runCommand in multiple platforms

I was trying to get network information on Windows using Nuetralinojs. How can I make my app cross platform? I want to run ifconfig command when users execute this on Linux.
I have posted my HTML and JS codes below.
let work = () => {
Neutralino.os.runCommand('ipconfig',
(data) => {
document.getElementById('neutralinoapp').innerHTML = data.stdout.replace(/\n/g, '</br>');
},
() => {
console.error('error');
}
);
}
Neutralino.init({
load: () => {
work();
},
pingSuccessCallback : () => {
},
pingFailCallback : () => {
}
});
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>NeutralinoJs</title>
<link rel="stylesheet" href="/assets/app.css">
</head>
<body>
<div id="neutralinoapp">
</div>
<script src="/neutralino.js"></script>
<script src="/assets/app.js"></script>
</body>
</html>
You can check os simply using NL_OS global variable of Neutralinojs.
If you are running cloud mode on a server window.navigator is not a solution.
Here is the modified JS function.
let work = () => {
let command = NL_OS == 'Windows' ? 'ipconfig' : 'ifconfig';
Neutralino.os.runCommand(command,
(data) => {
document.getElementById('neutralinoapp').innerHTML = data.stdout.replace(/\n/g, '</br>');
},
() => {
console.error('error');
}
);
}

Aframe Emitting Custom Events Between Components

I'm trying to create a custom event dataReady in a-frame which will emit when a few different data sources have successfully downloaded. I would like for multiple other components to listen for this event and then cause their element parents to become visible or perform other actions.
In the sample code below, the eventListener is being added before the event is emitted (tested using console.log()), but the listener function on the element is not being called when the event is emitted. I feel like all the pieces are in place, but it just isn't working correctly. Any thoughts would be greatly appreciated?
Component 1 attached to <a-camera data-controller></a-camera>
AFRAME.registerComponent('data-controller', {
init: function () {
var el = this.el;
},
update: function () {
async.eachSeries(['api-1', 'api-2'],
function (item, cb) {
console.log(item);
cb();
},
function (err) {
if (err) {
throw new Error('Error retrieving data from server');
}
console.log("emitting the event");
this.el.emit("dataReady", {value: 2}, false);
}.bind(this));
}
});
Component 2 attached to <a-entity geometry="primitive:plane" road></a-entity>
AFRAME.registerComponent('road', {
schema: {
width: {type: 'number', default: 4},
height: {type: 'number', default: 4}
},
init: function () {
var data = this.data;
var el = this.el;
// Update geometry props
el.setAttribute('geometry', 'width', data.width);
el.setAttribute('geometry', 'height', data.height);
console.log('adding an event listener');
el.addEventListener('click', function (event) {
console.log("we are ready to blast off b/c of event listened w/ detail value: " + event.detail.value);
});
}
});
index.html contents:
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<!--Vendor Scripts-->
<script src="./js/vendor/aframe.min.js"></script>
<script src="./js/vendor/async.min.js"></script>
<!--AFRAME Components-->
<script src="./js/components/road.js"></script>
<script src="./js/data-controller.js"></script>
</head>
<body>
<a-scene>
<a-entity geometry="primitive:plane" road></a-entity>
<a-camera data-controller></a-camera>
</a-scene>
</body>
</html>
You could rely on the event bubbling and listen on the scene, and store the entity in the event detail so you know where it came from:
// data-controller component
this.el.emit('dataready', {value: 2, el: this.el})
// Event will bubble to the scene.
// road component
this.el.sceneEl.addEventListener('dataready', function () { // ... });
Or you can pass a reference with the selector property type to which entity to want to listen to:
// road component
schema: {
dataEl: {type: 'selector'},
width: {type: 'number', default: 4},
height: {type: 'number', default: 4}
},
init: function () {
// ...
this.data.dataEl.addEventListener('dataready', function () { // ... });
}
// <a-entity road="dataEl: [data-controller]"></a-entity>

Categories

Resources