I have a tooltip control I've written that works very nicely in Vue 3, but I need a mechanism to fire off to all other instances to tell them to close. There are delays on close, so I'm occasionally getting two tooltips to show up at the same time.
This method, which was a crutch I've used in the past, is not allowed by the compiler / build tools. I can full well understand why, but I don't know the right way:
tooltipManager: function() {
if (!window.TooltipManager) {
function tooltipManager() {
let _data = {
tooltipIndex: 0,
callbacks: {}
};
return {
register: function (callback) {
let id = "tooltip_" + _data.tooltipIndex;
_data.tooltipIndex++;
_data.callbacks[id] = callback;
return id;
},
closeOpenPopups: function (id) {
Object.keys(_data.callbacks).forEach(key => {
if (id !== key) {
_data.callbacks[key]();
}
});
},
destroy: function (id) {
delete _data.callbacks[id];
}
};
}
window.TooltipManager = tooltipManager();
}
return window.TooltipManager()
},
The first thing I tried but didn't work was a service which I imported:
export default class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
Ok, I was close with the first service. It should be written this way, and I'm going to leave my console.logs in that confirmed that it is indeed a singleton even though it is running on different tooltips.
class TooltipManager {
constructor() {
if(! TooltipManager.instance){
this._data = {
tooltipIndex: 0,
callbacks: {}
};
console.log("got new instance");
} else {
console.log("got old instance");
}
}
register (callback) {
let id = "tooltip_" + this._data.tooltipIndex;
this._data.tooltipIndex++;
this._data.callbacks[id] = callback;
console.log("registered key: " + id);
return id;
}
closeOpenPopups(id) {
Object.keys(this._data.callbacks).forEach(key => {
if (id !== key) {
console.log("closed: " + key);
this._data.callbacks[key]();
}
});
}
destroy(id) {
delete this._data.callbacks[id];
}
}
export default new TooltipManager();
I got the following from console.logs:
got new instance
TooltipManager.js:19 registered key: tooltip_0
TooltipManager.js:19 registered key: tooltip_1
TooltipManager.js:19 registered key: tooltip_2
TooltipManager.js:19 registered key: tooltip_3
TooltipManager.js:26 closed: tooltip_0
TooltipManager.js:26 closed: tooltip_1
TooltipManager.js:26 closed: tooltip_2
TooltipManager.js:26 closed: tooltip_3
And indeed it solved the problem of ghost tooltips when one pops up before the other closes with a delay to prevent bounce.
In the tooltip tool I wrote, which I will later post here as an example of how easy Vue3 Teleport makes something like this to write. I want to test it a little longer before I show it off.
I just need to:
mounted() {
this.data.tooltipId = TooltipManager.register(this.forceHide);
And, which also shows some state data I use to keep track of this:
methods: {
forceHide: function() {
if (this.data.isDisplayed) {
this.data.style = '{top: -1000px, left: -1000px}';
}
this.data.hideRequested = false;
this.data.showRequested = false;
this.data.isDisplayed = false;
},
Now the next thing maybe using Vuex for this, but I may leave this in as an alternative method so it's not dependent on it.
Related
I'm new to VueJs and currently trying to load some data only once and make it globally available to all vue components. What would be the best way to achieve this?
I'm a little bit stuck because the global variables occasionally seem to become null and I can't figure out why.
In my main.js I make three global Vue instance variables:
let globalData = new Vue({
data: {
$serviceDiscoveryUrl: 'http://localhost:40000/api/v1',
$serviceCollection: null,
$clientConfiguration: null
}
});
Vue.mixin({
computed: {
$serviceDiscoveryUrl: {
get: function () { return globalData.$data.$serviceDiscoveryUrl },
set: function (newUrl) { globalData.$data.$serviceDiscoveryUrl = newUrl; }
},
$serviceCollection: {
get: function () { return globalData.$data.$serviceCollection },
set: function (newCollection) { globalData.$data.$serviceCollection = newCollection; }
},
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) { globalData.$data.$clientConfiguration = newConfiguration; }
}
}
})
and in my App.vue component I load all the data:
<script>
export default {
name: 'app',
data: function () {
return {
isLoading: true,
isError: false
};
},
methods: {
loadAllData: function () {
this.$axios.get(this.$serviceDiscoveryUrl)
.then(
response => {
this.$serviceCollection = response.data;
let configurationService = this.$serviceCollection.services.find(obj => obj.key == "ProcessConfigurationService");
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
})
}
},
created: function m() {
this.loadAllData();
}
}
</script>
But when I try to access the $clientConfiguration it seems to be null from time to time and I can't figure out why. For example when I try to build the navigation sidebar:
beforeMount: function () {
let $ = JQuery;
let clients = [];
if (this.$clientConfiguration === null)
console.error("client config is <null>");
$.each(this.$clientConfiguration, function (key, clientValue) {
let processes = [];
$.each(clientValue.processConfigurations, function (k, processValue) {
processes.push(
{
name: processValue.name,
url: '/process/' + processValue.id,
icon: 'fal fa-project-diagram'
});
});
clients.push(
{
name: clientValue.name,
url: '/client/' + clientValue.id,
icon: 'fal fa-building',
children: processes
});
});
this.nav.find(obj => obj.name == 'Processes').children = clients;
The most likely cause is that the null is just the initial value. Loading the data is asynchronous so you'll need to wait for loading to finish before trying to create any components that rely on that data.
You have an isLoading flag, which I would guess is your attempt to wait for loading to complete before showing any components (maybe via a suitable v-if). However, it currently only waits for the first request and not the second. So this:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
}
);
this.isLoading = false;
would need to be:
this.$axios.get(configurationService.address + "/api/v1/clientConfiguration").then(
response2 => {
this.$clientConfiguration = response2.data;
this.isLoading = false;
}
);
If it isn't that initial value that's the problem then you need to figure out what is setting it to null. That should be prety easy, just put a debugger statement in your setter:
$clientConfiguration: {
get: function () { return globalData.$data.$clientConfiguration },
set: function (newConfiguration) {
if (!newConfiguration) {
debugger;
}
globalData.$data.$clientConfiguration = newConfiguration;
}
}
Beyond the problem with the null, if you're using Vue 2.6+ I would suggest taking a look at Vue.observable, which is a simpler way of creating a reactive object than creating a new Vue instance.
Personally I would probably implement all of this by putting a reactive object on Vue.prototype rather than using a global mixin. That assumes that you even need the object to be reactive, if you don't then this is all somewhat more complicated than it needs to be.
I Need to refactor an IIFE in ES6. In ES6 let and const have a block scope, so I really need an IIFE in ES6? This is the ES5 version of the code:
var oojs = (function(oojs) {
var createToolbarItems = function(itemElements) {
var items = [];
[].forEach.call(itemElements,
function(el, index, array) {
var item = {
toggleActiveState: function() {
this.activated = !this.activated;
}
};
Object.defineProperties(item, {
el: {
value: el
},
enabled: {
get: function() {
return !this.el.classList.contains('disabled');
},
set: function(value) {
if (value) {
this.el.classList.remove('disabled');
} else {
this.el.classList.add('disabled');
}
}
},
activated: {
get: function() {
return this.el.classList.contains('active');
},
set: function(value) {
if (value) {
this.el.classList.add('active');
} else {
this.el.classList.remove('active');
}
}
}
});
items.push(item);
});
return items;
};
oojs.createToolbar = function(elementId) {
var element = document.getElementById(elementId);
var items = element.querySelectorAll('.toolbar-item');
return {
items: createToolbarItems(items)
}
};
return oojs;
}(oojs || {}));
What is the best way to translate this code in ES6? I tried many solution but I miss something, and I get an error: oojs is not defined.
Maybe I can use a Class instead? As you can see from the code I'm writing a Toolbar API in a OOP way (I think...)
Thanks for any help
EDIT: Thanks to georg, I try to refactor my code using classes. This is the new ES6 version:
class Toolbar {
constructor(elementId) {
this.elementId = elementId;
}
get items() {
const element = document.getElementById(this.elementId);
return element.querySelectorAll(".toolbar-item");
}
createToolbarItems() {
return [...this.items].map(el => new ToolbarItem(el));
}
}
class ToolbarItem {
constructor(el) {
this.el = el;
}
get enabled() {
return !this.el.classList.contains('disabled');
}
set enabled(value) {
if (value) {
this.el.classList.remove('disabled');
} else {
this.el.classList.add('disabled');
}
}
get activated() {
return this.el.classList.contains('active');
}
set activated(value) {
if (value) {
this.el.classList.add('active');
} else {
this.el.classList.remove('active');
}
}
toggleActiveState() {
this.activated = !this.activated;
}
}
// const toolbar = new Toolbar('myToolbar');
// const toolbarItems = toolbar.createToolbarItems();
EDIT: please check if is the right way to write this code, I'm pretty new to ES6
Thanks again
You can start by factoring out the toolbar item code (var item and below):
class ToolbarItem
{
constructor(element) {
....
}
}
Now, decide if you want to keep enabled and activated as properties or refactor them to explicit methods like isEnabled and setEnabled. In the former case it would be,
class ToolbarItem {
get enabled() {
...
}
set enabled(value) {
...
}
}
while ordinary methods can be defined like this:
class ToolbarItem {
isEnabled() {
...
}
setEnabled(value) {
...
}
}
Once you get this sorted out, replace your item initialization code with items.push(new ToolbarItem(el)) and test.
Hope this helps you getting started, good luck!
I want to send value of result from child to parent element. I used Session.set and Session.get and it works fine but I know that is not good practice because Sessions are global. So, I wanted to try something like reactive var or reactive dict but both of them are giving me only object as a result. What should I do or how should I take specific things from that object? (I am storing JSON inside that ReactiveVar or Dict and I know that they are really bad with JSON. Thank you for help!
Template.companyCreate.helpers({
CompanyName : function () {
if (Meteor.user() || Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
//this is where I want to take result and give it to parent function
}
});
return //this is where I want to take result that was given from child function and return it to CompanyName
}
else {
Router.go('/nemate-prava')
}
},
UPDATED CODE
Template.companyCreate.onCreated(function Poruke() {
this.message = new ReactiveVar(' ');
let self = this;
let user = Meteor.user();
let companyNameHandler = Template.currentData().companyNameHandler;
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
companyNameHandler(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
Template.companyCreate.helpers({
message: () => { return Template.instance().message.get() },
isNotInRole : function() {
if (!Meteor.user() || !Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
return true;
}
else {
return false;
}
},
CompanyName : function () {
return Template.instance().companyName.get();
}
});
Template.companyCreate.events({
'submit form': function(event, template) {
var Ime = event.target.Ime.value;
event.preventDefault();
Meteor.call('companyCheck', Ime, function(error, result) {
if (error) {
console.log(error.reason);
template.message.set(error.reason);
alert(error.reason);
}
else {
event.target.Ime.value = "";
console.log('Kompanija je uspesno kreirana!');
template.message.set("Uspesno!");
}
})
},
});
Method:
'findCompany'(){
ImeKompanije = firma.findOne({AdminID: this.userId}).ImeKompanije
if (typeof ImeKompanije == 'undefind') {
throw new Meteor.Error(err, "Greska!");
}
return ImeKompanije;
},
});
Router:
Router.route('/comp/:ImeKompanije', {
name: 'companyProfile',
template: 'companyProfile',
waitOn: function() {
return Meteor.subscribe('bazaFirmi', this.params.ImeKompanije)
},
action: function() {
this.render('companyProfile', {
data: function() {
return firma.findOne({ImeKompanije: this.params.ImeKompanije});
}
});
},
});
ok, there's a lot to unwind here. let's start with something small.
if (Meteor.user() || Roles.userIsInRole(Meteor.user(),['admin','adminCreator'], 'companyAdmin')) {
i think this line is meant to say, "if the user is an admin". but it's really saying, "if the user is logged in." if you meant the first one, then change the "||" to an "&&".
bigger issue is you're making a server call in a helper. helpers can get called over and over, so think of them as something that simply returns data. it should not have any side effects, such as making a server call or (yikes) re-routing the user.
so let's move all that side effect code to the onCreated() and capture the company name so it can be returned from the helper. We'll also get set up to return the company name to the parent.
Template.companyCreate.onCreated(function() {
let self = this;
let user = Meteor.user();
let companyNameHandler = Template.currentData().companyNameHandler;
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
companyNameHandler(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
now the helper is really simple, it just returns the data that was saved to the template's reactive var:
Template.companyCreate.helpers({
CompanyName : function () {
return Template.instance().companyName.get();
}
});
the last part is setting up the handler to return the data to the parent. it's bad form to have the client reaching back up to its parent, so i usually have the parent give to the child a function it can call. usually i'll do that when the child says, "i've done my work," but here we can use it to provide that data. i'll have to make some assumptions on what your parent looks like.
<template name="Parent">
{{> companyCreate companyNameHandler=getCompanyNameHandler}}
</template>
Template.Parent.helpers({
getCompanyNameHandler() {
let template = Template.instance();
return function(companyName) {
console.log(companyName);
// you can also access the parent template through the closure "template"
}
}
});
the parent's helper returns a function that is passed to the client. when the client calls it, it will execute in the parent's closure. you can see i set up a variable called "template" that would allow you to, say, access reactive vars belonging to the parent.
UPDATE: in case the handler isn't known as is inside the Meteor.call() scope, we can try using it through a reactive var.
Template.companyCreate.onCreated(function() {
let self = this;
let user = Meteor.user();
self.companyNameHandler = new ReactiveVar(Template.currentData().companyNameHandler);
self.companyName = new ReactiveVar();
if (user && Roles.userIsInRole(user,['admin','adminCreator'], 'companyAdmin')) {
Meteor.call('findCompany', function(err, result) {
if (err) {
console.log(err.reason)
}
else {
self.companyName.set(result);
let fn = self.companyNameHandler.get();
fn(result);
}
});
}
else {
Router.go('/nemate-prava')
}
});
I am building a React app using altjs as my Flux implementation. When I try to create/delete an item from the front end, no matter what I pass as a parameter to the create/delete function, it always ends up as passing the entire state.
For example : I'm trying to delete an item with id=1. I click delete on that item and pass just the id to the delete function in the component. That function calls the delete service again passing the id. Once that gets to the store layer, it has the entire state of the component and not just the id that is passed.
I'm still fairly new to React/Flux and not sure what I'm doing wrong or why this is happening.
Main component delete function :
deleteItem = (id) => {
console.log(id) //logs out 56b36c34ad9c927508c9d14f
QuestionStore.deleteQuestion(id);
}
At this point id is still just the id.
QuestionStore :
import alt from '../core/alt';
import QuestionActions from '../actions/QuestionActions';
import QuestionSource from '../sources/QuestionSource';
class QuestionStore {
constructor() {
this.bindActions(QuestionActions);
this.exportAsync(QuestionSource);
this.loaded = false;
this.modalIsOpen = false;
this.data = [];
this.question = {
"text": '',
"tag": [],
"answer": [],
"company": [],
"createdAt": ''
};
this.error = null;
this.questionAdded = null;
this.questionDeleted = null;
}
onGetQuestions(data) {
if (data === false) {
this.onFailed();
} else {
this.data = data;
this.loaded = true;
}
}
onCreateQuestion(response) {
if (response === false) {
this.onFailed();
} else {
this.questionAdded = response;
}
}
onDeleteQuestion(response) {
if (response === false) {
this.onFailed();
} else {
this.questionDeleted = response;
}
}
onFailed(err) {
this.loaded = true;
this.error = "Data unavailable";
}
}
export default alt.createStore(QuestionStore, 'QuestionStore');
QuestionSource :
import Api from '../services/QuestionApi';
import QuestionActions from '../actions/QuestionActions';
let QuestionSource = {
fetchData() {
return {
async remote(state) {
return Api.getQuestions()
},
success: QuestionActions.getQuestions
}
},
createQuestion(question) {
return {
async remote(question) {
return Api.createQuestion(question)
},
success: QuestionActions.createQuestion
}
},
deleteQuestion(id) {
//id here is an object of the entire state of QuestionStore
return {
async remote(id) {
return Api.deleteQuestion(id)
},
success: QuestionActions.deleteQuestion
}
}
};
export default QuestionSource;
Once it hits this point, id is now the entire state of the component even though only the id is passed.
The first parameter that is bound to the action is the state of the store (part of the result of the exportAsync call. So all parameters shift one to the right, and the first parameter you call the function with in turn becomes the second parameter. See below code example:
deleteQuestion(state, id) {
//state here is an object of the entire state of QuestionStore
//id will be the first argument you provide to the function.
return {
async remote(id) {
return Api.deleteQuestion(id)
},
success: QuestionActions.deleteQuestion
}
}
Documentation from alt.js about handling async operations.
I have a meteor app that allows users to update their skype name, phone number, email address, etc. To help maintain a consistent code base I have implemented an EJSON type UserModel in a common directory so it can run on the client and server.
EJSON.addType("UserModel", function fromJSONValue(value) {
return new UserModel(value);
});
UserModel.prototype = {
constructor: UserModel,
//
// EJSON Ovverrides.
//
valueOf: function() {
return JSON.parse(JSON.stringify(this), function(key, value) {
var dateFields = ["expiration", "createdAt"];
if(_.contains(dateFields, key) && typeof value === "string") {
return new Date(value);
} else {
return value;
}
});
},
typeName: function() {
return 'UserModel';
},
toJSONValue: function() {
return this.valueOf();
},
clone: function() {
return new UserModel(this.valueOf());
},
equals: function(other) {
if(!(other instanceof UserModel)) {
return false;
}
return this._id === other._id;
},
setPhoneNumbers: function(phoneNumber, queueUpdate) {
var modifier = {$set: {
'profile.phoneNumber': phoneNumber
}};
this.profile.phoneNumber = phoneNumber;
return this._saveOrQueueUpdate(modifier, queueUpdate);
},
_saveOrQueueUpdate: function(modifier, queueUpdate) {
if (!queueUpdate) {
return Meteor.users.update(this._id, modifier, function(err, res) {
});
} else {
this.pendingUpdates.push(modifier);
return true;
}
}
I call the setPhoneNumbers method on the settings page js file like so.
'blur #phonenumber':function(){
var user = Meteor.user();
var number = $("#phonenumber").val();
if(number.length){
user.setPhoneNumbers(number);
}
}
The problem with this is that whenever I call the setPhoneNumbers method, the page takes >500ms to update and locks the entire page. I looked at the docs and according to this segment, client code should never be blocking. The page only locks up when updates happen so I know it has something to do with the UserModel. Any insight to what could be causing this would be very helpful. The page is extremely slow and it is unacceptable for a production app.