Vuejs3/Vuex4 conditional render on promise fulfillment - javascript

I have custom objects for holding child objects full of data. The child objects are initiated with null values for all their properties, so the objects can be referenced and their properties filled from remote sources. This creates a lazy-loading setup.
This code is going to be extremely trimmed down, but everything relevant should be here:
class Collection extends Object {
constructor(){
this.loaded = false;
var allLoaders = [];
var loaderPropmises = [];
var resolver;
const $this = this;
var trackLoaders = function(){
$this.loaded = false;
loaderPromises.push(Promise.all(allLoaders).then(() => {
//... irrelevant logic in here to ensure only the latest promise sets loaded to true
$this.loaded = true; //This is getting called where I expect
resolver();
}));
}
//hook for outside things to watch the promise if they want
this.loader = new Promise((resolve) => {
//this only gets resolved once, which is fine
resolver = resolve;
});
//... bunch of code around adding child objects, but the important part:
this.add(child){
this[child.id] = child;
this.allLoaders.push(child.loader);
trackLoaders();
}
}
}
The child then looks like:
class Child extends Object {
constructor(){
this.loaded = false;
var resolver;
const $this = this;
this.loader = new Promise((resolve) => {
resolver = resolve;
}).then((){
$this.loaded = true;
});
this.populate(data){
//bunch of stuff to set data to properties on this object
resolver();
}
}
}
In Vuex 4 I have these Collections as properties on an "AppData" object in the store:
const store = createStore({
state: function(){
AppData: {}
},
mutations: {
setupCollection(state, name){
if (!Object.hasOwnProperty.call(state.AppData, name){
state.AppData[name] = new Collection();
}
}
},
actions: {
//this is called on each row of data returned from an Axios call
add (context, {name, data}){
context.state.AppData[name][data.id].populate(data);
}
}
});
The idea is that whenever a Child is added to a Collection, the collection loaded property will be false until all the Child loader promises resolve. This all executes perfectly... Except that the loaded bools aren't reactive.
Right now, I have a Promise.all in each component's Created function that flags the component as "loaded" once all the objects needed for the component have had their "loader" promises resolved. This absolutely works, but isn't ideal as different data will be available at different times, and there are sometimes hundreds or more of these classes on screen at once. What I'm trying to accomplish is:
<div v-if="!myCollection.loaded">
Loading...
</div>
<div v-else>
Show the data I want here {{myCollection.property}}
</div>
So I have two thoughts on overcoming this, either of which would be great:
VueJS3 no longer has a need for Vue.set(), because Proxies. How would I make the loaded bools here reactive then? Or more specifically, what am I doing that prevents this from working?
Alternatively, is there a practical way to use the loader promise directly in a template?

It looks like Vue's ref is what I needed:
this.loaded = ref(false);
This works, at least on the Child class. I have some sort of circular referencing issue going on and haven't been able to test on the Collection class yes, but it should work the same.

Related

Javascript Class Property setter with jquery ajax response value

With JavaScript, when creating a class, while instantiating that class, I want to populate a public property. I can do this using a setter, however, the value I want comes from an external website which I retrieve via an ajax get call. The issue becomes that the new class object does not have the appropriate property value when I create it.
Here's some sample code:
class MyTestClass {
constructor() {
this._ipaddress = "";
}
get ip() {
return this._ipaddress;
}
set ip(value) {
createIpAddr();
}
createIpAddr() {
var myIpAddr = "";
var strUrl = "https://api.ipify.org/?format=json";
$.ajax({
url: strUrl,
success: function(data) {
this._ip = data.ip;
},
//async:false //Performing this in sync mode to make sure I get an IP before the rest of the page loads.
});
return myIpAddr;
}
}
var testobj = new MyTestClass();
console.log(testobj.ip);
The problem here is that I can't be sure the IP will be populated in time to use after creating the new instance of the class. I've tried promises and deffered, but they have the same problem, I can't populate the variable before I need it. I'm trying to adjust the way I am looking at this and adding callbacks, but the issue is that I need the correct value in the class before I can use the class for the next call, where I am passing this object to it.
Is there a simple solution I am over looking? I have been through a million of these threads about async: false, and I don't want to start a new one, but what is a better choice in this case?
I want to set a class property from an ajax response when instantiating the class object.
You could have your constructor return an async IIFE, allowing you to then await the creation of a new class instance.
That would look something like this:
class MyTestClassAsync {
constructor() {
return (async() => {
this._ip = (await this.createIpAddrAsync()).ip;
return this;
})();
}
get ip() {
return this._ip;
}
set ip(value) {
this._ip = value;
}
createIpAddrAsync = () => $.get("https://api.ipify.org/?format=json");
}
async function Main() {
var testobj = await new MyTestClassAsync();
console.log(testobj.ip);
}
Main();
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
I've made the personal decision to append "Async" to the method and class names, just so that it's clear they need to be awaited.

Calling the vue method from inside an event from VueDraggable

I am trying to get drag and drop function working in the vue.js app using vue-draggable https://vuejsexamples.com/vuejs-drag-and-drop-library-without-any-dependency/
The library has few events you can listen to and I would like to execute some logic once the item is dropped. However, I am not able to access vue component 'this' along with the data and methods. I've tried to use this.$dispatch('symDragged', event); but it is not working for the same reason. 'this' is not a vue instance but rather instance of draggable element.
Here is the code:
export default {
components: {
ICol,
SymptomsChooser, MultiSelectEditor, TempPressureChooser, BodyPartsEditor, MandatorySymptomsChooser},
data() {
return {
// data ommited...
options: {
dropzoneSelector: 'ul',
draggableSelector: 'li',
excludeOlderBrowsers: true,
showDropzoneAreas: true,
multipleDropzonesItemsDraggingEnabled: true,
onDrop(event) {
// delete symptom from old basket and add it to new one
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
//this is not working
//this.symDragged(this.draggedSymId, oldBasket, newBasket);
},
onDragstart(event) {
this.draggedSymId = event.items[0].accessKey;
}
}
}
},
methods: {
symDragged(symId, oldBasketId, newBasketId) {
console.log("symDragged!");
let draggedSym = this.getSymById(symId);
let basketOld = this.getBasketById(oldBasketId);
this.delSym(basketOld, draggedSym);
this.addSym({baskedId: newBaskedId, sym: draggedSym});
}
//other methods ommited
}
}
So, what is the correct way to call the vue component method from callback event? Or maybe I need to create another event so that vue instance could listen to it?
Thanks for you help!
The problem you are facing is that with this you are referencing to the returned data object scope and not component scope. The best way to solve this is to make reference to the component instance, so later on you can call anything attached to that instance. You can also take a look at codesandbox example https://codesandbox.io/embed/7kykmmmznq
data() {
const componentInstance = this;
return {
onDrop() {
let oldBasket = event.owner.accessKey;
let newBasket = event.droptarget.accessKey;
let draggedItemsAccessKeys = event.items.map(element => element.accessKey);
componentInstance.symDragged(
draggedItemsAccessKeys,
oldBasket,
newBasket
);
}
}
}

Force Knockout computed to re-evaluate after replacement of observable inside

A co-worker ran into the problem that a computed he wanted to test was not returning the expected output. This happens because we want to stub other computeds (which again are dependent on other computeds). After stubbing there are 0 observables left in the computed and the computed keeps returning the cached result.
How can we force a computed to re-evaluate which no no longer has the original observables inside?
const ViewModel = function() {
this.otherComputed = ko.computed(() => true);
this.computedUnderTest = ko.computed(() => this.otherComputed());
};
const vm = new ViewModel();
function expect(expected) {
console.log(vm.computedUnderTest() === expected);
}
// Init
expect(true);
// Stub dependent computed
vm.otherComputed = () => false;
// Computed no longer receives updates :(
expect(false);
// Can we force re-evaluation?
// vm.computedUnderTest.forceReEval()
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
The only solution I can think of that doesn't involve changing the code of ViewModel, is to stub ko.computed first...
In the example below I replace ko.computed by an extended version. The extension exposes a property, .stub, that allows you to write a custom function. When this function is set, the computed will re-evaluate using the provided logic.
In your test file, you'd need to be able to replace the global reference to ko.computed in your preparation code, before instantiating a ViewModel instance.
// Extender that allows us to change a computed's main value getter
// method *after* creation
ko.extenders.canBeStubbed = (target, value) => {
if (!value) return target;
const stub = ko.observable(null);
const comp = ko.pureComputed(() => {
const fn = stub();
return fn ? fn() : target();
});
comp.stub = stub;
return comp;
}
// Mess with the default to ensure we always extend
const { computed } = ko;
ko.computed = (...args) =>
computed(...args).extend({ canBeStubbed: true });
// Create the view model with changed computed refs
const ViewModel = function() {
this.otherComputed = ko.computed(() => true);
this.computedUnderTest = ko.computed(() => this.otherComputed());
};
const vm = new ViewModel();
function expect(expected) {
console.log("Test succeeded:", vm.computedUnderTest() === expected);
}
expect(true);
// Replace the `otherComputed`'s code by another function
vm.otherComputed.stub(() => false);
expect(false);
<script src="https://cdnjs.cloudflare.com/ajax/libs/knockout/3.4.2/knockout-min.js"></script>
In my own projects, I tend to use a completely different approach for testing my computeds which focuses on separating the logic from the dependencies. Let me know if the example above doesn't work for you. (I'm not going to write another answer if this already satisfies your needs)

Initialize Backbone Models based on another Backbone Model

I have a config.json that I am going to load into my app as a Backbone Model like:
var Config = Backbone.Model.extend({
defaults: {
base: ''
},
url: 'config.json'
});
Other models should be dependent on some data contained in Config like:
var ModelA = Backbone.Collection.extend({
initialize: function(){
//this.url should be set to Config.base + '/someEndpoint';
}
});
In above example, ModelA's url property is dependent on Config's base property's value.
How do I go about setting this up properly in a Backbone app?
As I see it, your basic questions are:
How will we get an instance of the configuration model?
How will we use the configuration model to set the dependent model's url?
How can we make sure we don't use the url function on the dependent model too early?
There are a lot of ways to handle this, but I'm going to suggest some specifics so that I can just provide guidance and code and "get it done," so to speak.
I think the best way to handle the first problem is to make that configuration model a singleton. I'm going to provide code from backbone-singleton GitHub page below, but I don't want the answer to be vertically long until I'm done with the explanation, so read on...
var MakeBackboneSingleton = function (BackboneClass, options) { ... }
Next, we make a singleton AppConfiguration as well as a deferred property taking advantage of jQuery. The result of fetch will provide always(callback), done(callback), etc.
var AppConfiguration = MakeBackboneSingleton(Backbone.Model.extend({
defaults: {
base: null
},
initialize: function() {
this.deferred = this.fetch();
},
url: function() {
return 'config.json'
}
}));
Now, time to define the dependent model DependentModel which looks like yours. It will call AppConfiguration() to get the instance.
Note that because of MakeBackboneSingleton the follow is all true:
var instance1 = AppConfiguration();
var instance2 = new AppConfiguration();
instance1 === instance2; // true
instance1 === AppConfiguration() // true
The model will automatically fetch when provided an id but only after we have completed the AppConfiguration's fetch. Note that you can use always, then, done, etc.
var DependentModel = Backbone.Model.extend({
initialize: function() {
AppConfiguration().deferred.then(function() {
if (this.id)
this.fetch();
});
},
url: function() {
return AppConfiguration().get('base') + '/someEndpoint';
}
});
Now finally, putting it all together, you can instantiate some models.
var newModel = new DependentModel(); // no id => no fetch
var existingModel = new DependentModel({id: 15}); // id => fetch AFTER we have an AppConfiguration
The second one will auto-fetch as long as the AppConfiguration's fetch was successful.
Here's MakeBackboneSingleton for you (again from the GitHub repository):
var MakeBackboneSingleton = function (BackboneClass, options) {
options || (options = {});
// Helper to check for arguments. Throws an error if passed in.
var checkArguments = function (args) {
if (args.length) {
throw new Error('cannot pass arguments into an already instantiated singleton');
}
};
// Wrapper around the class. Allows us to call new without generating an error.
var WrappedClass = function() {
if (!BackboneClass.instance) {
// Proxy class that allows us to pass through all arguments on singleton instantiation.
var F = function (args) {
return BackboneClass.apply(this, args);
};
// Extend the given Backbone class with a function that sets the instance for future use.
BackboneClass = BackboneClass.extend({
__setInstance: function () {
BackboneClass.instance = this;
}
});
// Connect the proxy class to its counterpart class.
F.prototype = BackboneClass.prototype;
// Instantiate the proxy, passing through any arguments, then store the instance.
(new F(arguments.length ? arguments : options.arguments)).__setInstance();
}
else {
// Make sure we're not trying to instantiate it with arguments again.
checkArguments(arguments);
}
return BackboneClass.instance;
};
// Immediately instantiate the class.
if (options.instantiate) {
var instance = WrappedClass.apply(WrappedClass, options.arguments);
// Return the instantiated class wrapped in a function so we can call it with new without generating an error.
return function () {
checkArguments(arguments);
return instance;
};
}
else {
return WrappedClass;
}
};

calling render() after teardown() does not display list data

I have list of menu options, and each menu item has it's own Ractive instance with different template but same shared data. When each selection is changed I am calling teardown() on rendered view instance and render(domElement) on current selection's Ractive instance.
An example Instance is like below, and all follow the same structure.
var View = new Ractive({
template: '#contacts',
data: {
name: 'Contacts',
contacts : dummyData // array data
}
});
And I render them like below
var isRendered = false;
channel.subscribe("menu", function(msg) {
if(msg === "contacts") {
contentHolder.innerHTML = "";
View.render(contentHolder);
isRendered = true;
} else {
if(isRendered) {
View.teardown();
isRendered = false;
console.log(View.get('contacts')); // Here I can see the data.
}
}
});
In first render() call view is rendered as expected, but after calling teardown(), again if I call render() it does not render contacts list data and only displays name property, but was rendered on initial call.
Please help me to fix this.
Just for reference, the question was answered on GitHub
teardown() is a non-reversible call that completely destroys the ractive instance. What you want is detach() function, which will remove the ractive instance from the DOM but not destroy it. You can use it later by calling insert().

Categories