I am building an Electron based application that contains a grid containing unique rows. I would like a context-menu that is specific to each row. Here is an example:
Although this screen shot is cropped, you can see there are multiple rows and each row contains separate data. Since I'd like to right-click on a row and get a unique context menu, I have implemented electron-context-menu, which does work on the first right click, but then subsequent right-clicks causes a stacking effect of context menus.
Specifically, here is what happens:
I right click on Row-1 and the proper context menu shows up
I right click on Row-2 and a repeat of the context menu for Row-1 shows up then Row-2's context menu shows up. (Notice in the screen shot the context menu showing does not correspond to the row my mouse is over)
This repeats itself.
In React.JS, here is my listener, which collects the contextmenu object as needed by the electron-context-menu module:
handleContextMenu() {
this.props.contextMenu({
window: electron.remote.BrowserWindow.getFocusedWindow(),
prepend: (params, browserWindow) => [{
label: `Library Compare ${this.state.msn}`,
click: () => this.runLibCompare()
}],
append: (params, browserWindow) => [{
label: '---',
}]
})
};
Where this.props.contextMenu(...) perculates up the React.JS components to be fed into:
const contextMenu = eRequire('electron-context-menu');
I have done some massive debugging and I don't think the issue is the module. The module I am using essentially organizes the information about the context menu and then uses electron.remote functions and a menu.popup function which comes from electron internals. Here is a link to the specific line in github.
const menu = (electron.Menu || electron.remote.Menu).buildFromTemplate(menuTpl);
menu.popup(electron.remote ? electron.remote.getCurrentWindow() : win);
This call to menu.popup leads to this line in electron.
const remoteMemberFunction = function (...args) {
if (this && this.constructor === remoteMemberFunction) {
// Constructor call.
let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CONSTRUCTOR', metaId, member.name, wrapArgs(args))
return metaToValue(ret)
} else {
// Call member function.
let ret = ipcRenderer.sendSync('ELECTRON_BROWSER_MEMBER_CALL', metaId, member.name, wrapArgs(args))
return metaToValue(ret)
}
}
So I see a call to ipcRender.sendSync -- however when I add debugging statements in ipcMain's receiver of those calls, I don't see any output!
ipcMain.on('ELECTRON_BROWSER_MEMBER_CALL', function (event, id, method, args) {
try {
args = unwrapArgs(event.sender, args)
let obj = objectsRegistry.get(id)
if (obj == null) {
throwRPCError(`Cannot call function '${method}' on missing remote object ${id}`)
}
callFunction(event, obj[method], obj, args)
} catch (error) {
event.returnValue = exceptionToMeta(error)
}
})
When I added debug statements to the above function, I didn't see any output. And that is where my search his a wall.
I am using electron 1.4.15. I know this issue should be resolvable, after-all the Atom IDE (which is electron based) does not have this issue even though it has multiple context menus.
I think there is some memory I need to clear somewhere, I just can't figure out how to clear the stack of previous context menus!
I solve this by first getting the target of the click using e.target. Then, depending on that, I call the corresponding contextmenu. If target hit is not in the list of targets for my app, I use a default contextmenu.
window.addEventListener(
"contextmenu",
e => {
e.preventDefault();
if (e.target.id === 'fullscr'){
console.log(e && e.target);
// e.preventDefault();
mymenu.popup(remote.getCurrentWindow());
}else{
editmenu.popup(remote.getCurrentWindow());
}
console.log(e.which);
},
false
);
Related
Hi i want to remove 3 options from electron title menu they are
reload
Force reload
Toggle developer tools
as shown below
Here is what i have tried
background.js
import { app, session, protocol, BrowserWindow , dialog, ipcMain, Menu} from 'electron'
const menu = Menu.getApplicationMenu(); // get default menu
console.log('menu %s',menu.items);
let menuIndex = menu.items.findIndex(o => {
return o.role == 'viewmenu'
})
//splice items based on names , i'm not able to do that because i cannot see the full json object and its structure in console.log
//Menu.setApplicationMenu(menu); //set menu
Further i don't know what to do
Please help me thanks in advance !!
You can create a new menu based on the default using Menu.buildFromTemplate and pass the default's menu items as template.
Then set that as application menu. During the construction you can cut out elements you don't want
const menu = Menu.getApplicationMenu(); // get default menu
// build a new menu based on default one
const newmenu = Menu.buildFromTemplate(
menu.items.map(i => {
// overwrite viewmenu item
if (i.role === 'viewmenu') {
// create new submenu
const newviewsub = Menu.buildFromTemplate(
i.submenu.items.slice(4) // cut first 4 item (4th is separator)
)
// replace this item's submenu with the news
return Object.assign({}, i, { submenu: newviewsub })
}
// otherwise keep
return i
})
)
Menu.setApplicationMenu(newmenu)
You should see that the 'view' menu's first 3 element is gone
my problem is, that I have a table which should update everytime when the user chooses something from a dropdown component. The problem now is that my table updates "too late" in the frontend. So when the user chooses an option for the first time nothing will happen. Then when the user chooses an option for the second time from the dropdown component, the table will show the data from the option he has picked before. If the user chooses an option for the 3rd time, the table will show the data from the second one and so on.
So how can I fix this? I work with ReactJS and Semantic UI
My Code:
This renders the Row for the existing data
renderTableData() {
return this.state.songs.map((song, index) => {
const { id, nr, songname, link } = song
return (
<Table.Row key={id}>
<Table.Cell>{nr}</Table.Cell>
<Table.Cell>{songname}</Table.Cell>
<Table.Cell>{link}</Table.Cell>
</Table.Row>
)
})
}
The Code in the main render() function of React (Its shown correctly, expect that the data is "outdated":
`<Table>
<Table.Header>
<Table.Row>
<Table.HeaderCell width={1}>Nr</Table.HeaderCell>
<Table.HeaderCell width={2}>Songname</Table.HeaderCell>
<Table.HeaderCell width={1}>Link</Table.HeaderCell>
</Table.Row>
</Table.Header>
{this.renderTableData()}
</Table>`
The code when the option from the dropdown gets changed:
onChangeDropdown(e) {
this.setState({game: e.target.textContent}, ()=>{
this.state.songs.length = 0;
for(var i = 0; i< this.state.musicData.length;i++){
if(this.state.musicData[i].game == this.state.game){
for(var j = 0; j<this.state.musicData[i].songs.length;j++){
this.state.songs.push({id: j+1, nr: j+1, songname: this.state.musicData[i].songs[j].name, link: this.state.musicData[i].songs[j].link})
}
break;
}
}
this.renderTableData()
})
}
The game variable in this.setState is correct and also the for-loop works as expected when the user changes the dropdown option, I already checked it with the debugger
I hope you can help me out there, ty
is not that is updating too late, is that you are mutating the state without using setState so React doesn't know what changed, you should refactor your code to always use setState to update the state, not push, something like this:
onChangeDropdown(e) {
this.setState((currentState) => {
const newSongs = [];
const game = e.target.textContent;
musicData.forEach((data) => {
if (data.game === game) {
musicData.songs.forEach((song, index) => {
newSongs.push({
id: index + 1,
nr: index + 1,
songname: song.name,
link: song.link,
});
});
}
});
return {
...currentState,
game,
songs: newSongs,
};
});
}
I changed your for loops to use forEach, less complexity, easier to read
Here is what I did:
create a empty array to store the selected songs (newSongs)
loop all the music data and then loop all the songs inside each item in music data
add the songs from the selected game into newSongs
return newSongs + game to update the selected game, ...currentState is to preserve the other parts of the state between changes
So every time the dropodown changes, I create a new array and run the logic
The setState callback can return an object to replace whole state, so before that you can do any calculation you need to.
Updating the state in React is asyncronous, that's one of the reasons you can't mutate the state directly and need to use setState any time you need to update it
I am trying to create entries on the Chrome context menu based on what is selected.
I found several questions about this on Stackoverflow, and for all of them the answer is: use a content script with a "mousedown" listener that looks at the current selection and creates the Context Menu.
I implemented this, but it does not always work. Sometimes all the log messages say that the context menu was modified as I wanted, but the context menu that appears is not updated.
Based on this I suspected it was a race condition: sometimes chrome starts rendering the context menu before the code ran completely.
I tried adding a eventListener to "contextmenu" and "mouseup". The later triggers when the user selects the text with the mouse, so it changes the contextmenu much before it appears (even seconds). Even with this technique, I still see the same error happening!
This happens very often in Chrome 22.0.1229.94 (Mac), occasionally in Chromium 20.0.1132.47 (linux) and it did not happen in 2 minutes trying on Windows (Chrome 22.0.1229.94).
What is happening exactly? How can I fix that? Is there any other workaround?
Here is a simplified version of my code (not so simple because I am keeping the log messages):
manifest.json:
{
"name": "Test",
"version": "0.1",
"permissions": ["contextMenus"],
"content_scripts": [{
"matches": ["http://*/*", "https://*/*"],
"js": ["content_script.js"]
}],
"background": {
"scripts": ["background.js"]
},
"manifest_version": 2
}
content_script.js
function loadContextMenu() {
var selection = window.getSelection().toString().trim();
chrome.extension.sendMessage({request: 'loadContextMenu', selection: selection}, function (response) {
console.log('sendMessage callback');
});
}
document.addEventListener('mousedown', function(event){
if (event.button == 2) {
loadContextMenu();
}
}, true);
background.js
function SelectionType(str) {
if (str.match("^[0-9]+$"))
return "number";
else if (str.match("^[a-z]+$"))
return "lowercase string";
else
return "other";
}
chrome.extension.onMessage.addListener(function(msg, sender, sendResponse) {
console.log("msg.request = " + msg.request);
if (msg.request == "loadContextMenu") {
var type = SelectionType(msg.selection);
console.log("selection = " + msg.selection + ", type = " + type);
if (type == "number" || type == "lowercase string") {
console.log("Creating context menu with title = " + type);
chrome.contextMenus.removeAll(function() {
console.log("contextMenus.removeAll callback");
chrome.contextMenus.create(
{"title": type,
"contexts": ["selection"],
"onclick": function(info, tab) {alert(1);}},
function() {
console.log("ContextMenu.create callback! Error? " + chrome.extension.lastError);});
});
} else {
console.log("Removing context menu")
chrome.contextMenus.removeAll(function() {
console.log("contextMenus.removeAll callback");
});
}
console.log("handling message 'loadContextMenu' done.");
}
sendResponse({});
});
The contextMenus API is used to define context menu entries. It does not need to be called right before a context menu is opened. So, instead of creating the entries on the contextmenu event, use the selectionchange event to continuously update the contextmenu entry.
I will show a simple example which just displays the selected text in the context menu entry, to show that the entries are synchronized well.
Use this content script:
document.addEventListener('selectionchange', function() {
var selection = window.getSelection().toString().trim();
chrome.runtime.sendMessage({
request: 'updateContextMenu',
selection: selection
});
});
At the background, we're going to create the contextmenu entry only once. After that, we update the contextmenu item (using the ID which we get from chrome.contextMenus.create).
When the selection is empty, we remove the context menu entry if needed.
// ID to manage the context menu entry
var cmid;
var cm_clickHandler = function(clickData, tab) {
alert('Selected ' + clickData.selectionText + ' in ' + tab.url);
};
chrome.runtime.onMessage.addListener(function(msg, sender, sendResponse) {
if (msg.request === 'updateContextMenu') {
var type = msg.selection;
if (type == '') {
// Remove the context menu entry
if (cmid != null) {
chrome.contextMenus.remove(cmid);
cmid = null; // Invalidate entry now to avoid race conditions
} // else: No contextmenu ID, so nothing to remove
} else { // Add/update context menu entry
var options = {
title: type,
contexts: ['selection'],
onclick: cm_clickHandler
};
if (cmid != null) {
chrome.contextMenus.update(cmid, options);
} else {
// Create new menu, and remember the ID
cmid = chrome.contextMenus.create(options);
}
}
}
});
To keep this example simple, I assumed that there's only one context menu entry. If you want to support more entries, create an array or hash to store the IDs.
Tips
Optimization - To reduce the number of chrome.contextMenus API calls, cache the relevant values of the parameters. Then, use a simple === comparison to check whether the contextMenu item need to be created/updated.
Debugging - All chrome.contextMenus methods are asynchronous. To debug your code, pass a callback function to the .create, .remove or .update methods.
MDN doc for menus.create(), 'title' param
You can use "%s" in the string. If you do this in a menu item, and some text is selected in the page when the menu is shown, then the selected text will be interpolated into the title.
https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/menus/create
Thus
browser.contextMenus.create({
id: 'menu-search',
title: "Search '%s'", // selected text as %s
contexts: ['selection'], // show only if selection exist
})
Contextual help in prompts
I need to implement contextual help for a chatbot. My strategy is to use the active prompt as an index for a table with help-textlines. I am struggling with finding the active prompt after a stepContext.replaceDialog() in a waterfall dialog.
I will use the Compex Dialog sample as example.
In reviewSelectionDialog below is a prompt called CHOICE_PROMPT. This is the prompt in which I would like to add contextual help. If the user enters help, the helptext should be shown that is about that prompt.
In the same dialog is a loopstep. Based on a user decision, the dialog is repeated (looped) by the replaceDialog() method.
ReviewSelectionDialog is extended with CancelAndHelpDialog. As a result I am able to check for and act on any user interrupts like 'help'.
In CancelAndHelpDialog I need the active prompt when help was entered by the user so I am able to show relevant help. (CHOICE_PROMPT in this example).
My question
In the first pass of ReviewSelectionDialog, after sending 'help', I am able to get the active prompt in the CancelAndHelpDialog via innerDc.activeDialog.id. But after the stepContext.replaceDialog() in loopStep and sending 'help' again in the CHOICE_PROMPT, innerDc.activeDialog.id shows REVIEW_SELECTION_DIALOG. Where do I find the active prompt after a replace_dialog()?
ReviewSelectionDialog
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { ChoicePrompt, WaterfallDialog } = require('botbuilder-dialogs');
const REVIEW_SELECTION_DIALOG = 'REVIEW_SELECTION_DIALOG';
const { CancelAndHelpDialog } = require('./cancelAndHelpDialog');
const CHOICE_PROMPT = 'CHOICE_PROMPT';
const WATERFALL_DIALOG = 'WATERFALL_DIALOG';
class ReviewSelectionDialog extends CancelAndHelpDialog {
constructor() {
super(REVIEW_SELECTION_DIALOG);
// Define a "done" response for the company selection prompt.
this.doneOption = 'done';
// Define value names for values tracked inside the dialogs.
this.companiesSelected = 'value-companiesSelected';
// Define the company choices for the company selection prompt.
this.companyOptions = ['Adatum Corporation', 'Contoso Suites', 'Graphic Design Institute', 'Wide World Importers'];
this.addDialog(new ChoicePrompt(CHOICE_PROMPT));
this.addDialog(new WaterfallDialog(WATERFALL_DIALOG, [
this.selectionStep.bind(this),
this.loopStep.bind(this)
]));
this.initialDialogId = WATERFALL_DIALOG;
}
async selectionStep(stepContext) {
// Continue using the same selection list, if any, from the previous iteration of this dialog.
const list = Array.isArray(stepContext.options) ? stepContext.options : [];
stepContext.values[this.companiesSelected] = list;
// Create a prompt message.
let message = '';
if (list.length === 0) {
message = `Please choose a company to review, or \`${ this.doneOption }\` to finish.`;
} else {
message = `You have selected **${ list[0] }**. You can review an additional company, or choose \`${ this.doneOption }\` to finish.`;
}
// Create the list of options to choose from.
const options = list.length > 0
? this.companyOptions.filter(function(item) { return item !== list[0]; })
: this.companyOptions.slice();
options.push(this.doneOption);
// Prompt the user for a choice.
return await stepContext.prompt(CHOICE_PROMPT, {
prompt: message,
retryPrompt: 'Please choose an option from the list.',
choices: options
});
}
async loopStep(stepContext) {
// Retrieve their selection list, the choice they made, and whether they chose to finish.
const list = stepContext.values[this.companiesSelected];
const choice = stepContext.result;
const done = choice.value === this.doneOption;
if (!done) {
// If they chose a company, add it to the list.
list.push(choice.value);
}
if (done || list.length > 1) {
// If they're done, exit and return their list.
return await stepContext.endDialog(list);
} else {
// Otherwise, repeat this dialog, passing in the list from this iteration.
return await stepContext.replaceDialog(REVIEW_SELECTION_DIALOG, list);
}
}
}
module.exports.ReviewSelectionDialog = ReviewSelectionDialog;
module.exports.REVIEW_SELECTION_DIALOG = REVIEW_SELECTION_DIALOG;
CancelAndHelpDialog
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.
const { InputHints } = require('botbuilder');
const { ComponentDialog, DialogTurnStatus } = require('botbuilder-dialogs');
/**
* This base class watches for common phrases like "help" and "cancel" and takes action on them
* BEFORE they reach the normal bot logic.
*/
class CancelAndHelpDialog extends ComponentDialog {
async onContinueDialog(innerDc) {
const result = await this.interrupt(innerDc);
if (result) {
return result;
}
return await super.onContinueDialog(innerDc);
}
async interrupt(innerDc) {
if (innerDc.context.activity.text) {
const text = innerDc.context.activity.text.toLowerCase();
switch (text) {
case 'help':
case '?': {
const helpMessageText = 'Show help about prompt: ' + innerDc.activeDialog.id;
await innerDc.context.sendActivity(helpMessageText, helpMessageText, InputHints.ExpectingInput);
return { status: DialogTurnStatus.waiting };
}
case 'cancel':
case 'quit': {
const cancelMessageText = 'Cancelling...';
await innerDc.context.sendActivity(cancelMessageText, cancelMessageText, InputHints.IgnoringInput);
return await innerDc.cancelAllDialogs();
}
}
}
}
}
module.exports.CancelAndHelpDialog = CancelAndHelpDialog;
I want to thank you for using the sample code because you've actually revealed a bug that I've reported here: https://github.com/microsoft/BotBuilder-Samples/issues/2457
The underlying problem here is that the dialogs library has two ways of stacking dialogs. Ordinarily, one dialog gets stacked on top of another dialog like this:
[ CHOICE_PROMPT ]
[ WATERFALL_DIALOG ]
However, component dialogs form a nested dialog stack that stacks inward rather than further upward:
[ REVIEW_SELECTION_DIALOG ]
[ TOP_LEVEL_DIALOG ]
[ MAIN_DIALOG ]
Since not all dialogs are component dialogs, the two ways combine to look like this:
[ CHOICE_PROMPT ]
[ WATERFALL_DIALOG ]
[ REVIEW_SELECTION_DIALOG ]
[ TOP_LEVEL_DIALOG ]
[ MAIN_DIALOG ]
I want to note that the order of this stack is not necessarily what you'd expect if you're used to writing hierarchical lists that look like this (with the most recently added item on the bottom):
MAIN_DIALOG
TOP_LEVEL_DIALOG
REVIEW_SELECTION_DIALOG
WATERFALL_DIALOG
CHOICE_PROMPT
Some people might not consider the second way of stacking actual stacking, since it's a parent-child relationship and not a stack. The reason I'm calling it a second way of stacking here is because of the conceptual similarity to a dialog stack. When you design your bot's dialogs, you have a choice about whether you want each new dialog to be added on top of the existing dialog stack or be nested as a child in an inner dialog stack. The two ways behave similarly because a component dialog ends when its last child dialog ends, so when you pop a dialog off of a stack the stack unravels outwards in much the same way as it unravels downwards. (Remember that new dialogs get added to the top of the stack so "downwards" here means from newer dialogs back to older dialogs, like the stack diagrams I started with.)
The "active dialog" is the dialog at the top of the stack. Since each component dialog has its own dialog set and dialog state and dialog stack and dialog context, each component dialog has a different idea of what the active dialog is. Because the active dialog is defined in terms of a specific dialog stack, when there are multiple dialog stacks the active dialog depends on who you ask.
This didn't cause a problem for you when you were looking for the active dialog in the innermost component dialog. But then you replaced that component dialog's child with the component dialog itself. After that, your (full) stack looked like this:
[ CHOICE_PROMPT ]
[ WATERFALL_DIALOG ]
[ REVIEW_SELECTION_DIALOG ]
[ REVIEW_SELECTION_DIALOG ]
[ TOP_LEVEL_DIALOG ]
[ MAIN_DIALOG ]
When your CancelAndHelpDialog tried to access the active dialog of its inner dialog context, it correctly returned a ReviewSelectionDialog because that was the only dialog on its stack. You wanted to return the choice prompt but that choice prompt was in the dialog stack of the child ReviewSelectionDialog and not the parent ReviewSelectionDialog.
The bug is that you should be replacing the waterfall dialog with itself rather than with the parent component dialog. So it could look like this:
return await stepContext.replaceDialog(WATERFALL_DIALOG, list);
Or like this:
return await stepContext.replaceDialog(this.initialDialogId, list);
Ultimately, this still hasn't answered a question that you may have meant to ask. Since you've seen that problems can arise when you get the active dialog in an intermediate dialog context, you may want a way to get the "real" innermost active dialog. This can be accomplished with some simple recursion:
function getInnermostActiveDialog(dc) {
const child = dc.child;
return child ? getInnermostActiveDialog(child) : dc.activeDialog;
}
Relative newbie; forgive me if my etiquette and form here aren't great. I'm open to feedback.
I have used create-react-native-app to create an application using PouchDB (which I believe ultimately uses AsyncStorage) to store a list of "items" (basically).
Within a TabNavigator (main app) I have a StackNavigator ("List screen") for the relevant portion of the app. It looks to the DB and queries for the items and then I .map() over each returned record to generate custom ListView-like components dynamically. If there are no records, it alternately displays a prompt telling the user so. In either case, there is an "Add Item" TouchableOpacity that takes them to a screen where they an add a new item (for which they are taken to an "Add" screen).
When navigating back from the "Add" screen I'm using a pattern discussed quite a bit here on SO in which I've passed a "refresh" function as a navigation param. Once the user uses a button on the "Add" screen to "save" the changes, it then does a db.post() and adds them item, runs the "refresh" function on the "List screen" and then navigates back like so:
<TouchableOpacity
style={styles.myButton}
onPress={() => {
if (this.state.itemBrand == '') {
Alert.alert(
'Missing Information',
'Please be sure to select a Brand',
[
{text: 'OK', onPress: () =>
console.log('OK pressed on AddItemScreen')},
],
{ cancelable: false }
)
} else {
this.createItem();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsScreen');
}
}
}
>
And all of this works fine. The "refresh" function (passed as onGoBack param) works fine... for this screen. The database is called with the query, the new entry is found and the components for the item renders up like a charm.
Each of the rendered ListItem-like components on the "List screen" contains a react-native-slideout with an "Edit" option. An onPress for these will send the user to an "Item Details" screen, and the selected item's _id from PouchDB is passed as a prop to the "Item Details" screen where loadItem() runs in componentDidMount and does a db.get(id) in the database module. Additional details are shown from a list of "events" property for that _id (which are objects, in an array) which render out into another bunch of ListItem-like components.
The problem arises when either choose to "Add" an event to the list for the item... or Delete it (using another function via [another] slideout for these items. There is a similar backward navigation, called in the same form as above after either of the two functions is called from the "Add Event" screen, this being the "Add" example:
async createEvent() {
var eventData = {
eventName: this.state.eventName.trim(),
eventSponsor: this.state.eventSponsor.trim(),
eventDate: this.state.eventDate,
eventJudge: this.state.eventJudge.trim(),
eventStandings: this.state.eventStandings.trim(),
eventPointsEarned: parseInt(this.state.eventPointsEarned.trim()),
};
var key = this.key;
var rev = this.rev;
await db.createEvent(key, rev, eventData);
}
which calls my "db_ops" module function:
exports.createEvent = function (id, rev, eventData) {
console.log('You called db.createEvent()');
db.get(id)
.then(function(doc) {
var arrWork = doc.events; //assign array of events to working variable
console.log('arrWork is first assigned: ' + arrWork);
arrWork.push(eventData);
console.log('then, arrWork was pushed and became: ' + arrWork);
var arrEvents = arrWork.sort((a,b)=>{
var dateA = new Date(a.eventDate), dateB = new Date(b.eventDate);
return b.eventDate - a.eventDate;
})
doc.events = arrEvents;
return db.put(doc);
})
.then((response) => {
console.log("db.createEvent() response was:\n" +
JSON.stringify(response));
})
.catch(function(err){
console.log("Error in db.createEvent():\n" + err);
});
}
After which the "Add Event" screen's button fires the above in similar sequence to the first, just before navigating back:
this.createEvent();
this.props.navigation.state.params.onGoBack();
this.props.navigation.navigate('ItemsDetails');
The "refresh" function looks like so (also called in componentDidMount):
loadItem() {
console.log('Someone called loadItem() with this.itemID of ' + this.itemID);
var id = this.itemID;
let totalWon = 0;
db.loadItem(id)
.then((item) => {
console.log('[LOAD ITEM] got back data of:\n' + JSON.stringify(item));
this.setState({objItem: item, events: item.events});
if (this.state.events.length != 0) { this.setState({itemLoaded: true});
this.state.events.map(function(event) {
totalWon += parseInt(event.eventPointsEarned);
console.log('totalWon is ' + totalWon + ' with ' +
event.eventPointsEarned + ' having been added.');
});
};
this.setState({totalWon: totalWon});
})
.catch((err) => {
console.log('db.loadItem() error: ' + err);
this.setState({itemLoaded: false});
});
}
I'm at a loss for why the List Screen refreshes when I add an item... but not when I'm doing other async db operations with PouchDB in what I think is similar fashion to modify the object containing the "event" information and then heading back to the Item Details screen.
Am I screwing up with Promise chain someplace? Neglecting behavior of the StackNavigator when navigating deeper?
The only other difference being that I'm manipulating the array in the db function in the non-working case, whereas the others I'm merely creating/posting or deleting/removing the record, etc. before going back to update state on the prior screen.
Edit to add, as per comments, going back to "List screen" and the opening "Item Details" does pull the database data and correctly shows that the update was made.
Further checking I've done also revealed that the console.log in createEvent() to print the response to the db call isn't logging until after some of the other dynamic rendering methods are getting called on the "Item Details" screen. So it seems as though the prior screen is doing the get() that loadItem() calls before the Promise chain in createEvent() is resolving. Whether the larger issue is due to state management is still unclear -- though it would make sense in some respects -- to me as this could be happening regardless of whether I've called my onGoBack() function.
Edit/bump: I’ve tried to put async/await to use in various places in both the db_ops module on the db.get() and the component-side loadItem() which calls it. There’s something in the timing of these that just doesn’t jive and I am just totally stuck here. Aside from trying out redux (which I think is overkill in this particular case), any ideas?
There is nothing to do with PDB or navigation, it's about how you manage outer changes in your depending (already mounted in Navigator since they are in history - it's important to understand - so componentDidMount isn't enough) components. If you don't use global state redux-alike management (as I do) the only way to let know depending component that it should update is passing corresponding props and checking if they were changed.
Like so:
//root.js
refreshEvents = ()=> { //pass it to DeleteView via screenProps
this.setState({time2refreshEvents: +new Date()}) //pass time2refreshEvents to EventList via screenProps
}
//DeleteView.js
//delete button...
onPress={db.deleteThing(thingID).then(()=> this.props.screenProps.refreshEvents())}
//EventList.js
...
constructor(props) {
super(props);
this.state = {
events: [],
noEvents: false,
ready: false,
time2refreshEvents: this.props.screenProps.time2refreshEvents,
}
}
static getDerivedStateFromProps(nextProps, currentState) {
if (nextProps.screenProps.time2refreshEvents !== currentState.time2refreshEvents ) {
return {time2refreshEvents : nextProps.screenProps.time2refreshEvents }
} else {
return null
}
}
componentDidMount() {
this._getEvents()
}
componentDidUpdate(prevProps, prevState) {
if (this.state.time2refreshEvents !== prevState.time2refreshEvents) {
this._getEvents()
}
}
_getEvents = ()=> {
//do stuff querying db and updating your list with actual data
}