How to best access data from QueryRenderer in a parent component in Relay Modern? - relayjs

As you can see from the picture below, I'm rendering the popover using a react-relay QueryRenderer since the request is slow, and I do not want the rest of the page to wait for events to be fetched.
My problem is that in the navigation I have a button to show/hide the popover. That button should only be rendered when events has loaded, and the button also needs to show a count of how many events there is.
So my question is how to pass events data up from QueryRenderer (popover) to a parent component (toggle button)?
My first idea was to reuse my QueryRenderer for events and pass in dataFrom={'STORE_ONLY'}, to avoid a new HTTP request and use the cache instead, but unfortunately 'STORE_ONLY' is not an option... YET...
From looking at https://github.com/relay-tools/relay-hooks/issues/5 it seems like store-only will be supported by useQuery in the future, so is that the recommended solution to go about it, or how is the recommended way? Surely facebook, and many other applications, must have had this need frequently?

You can achieve redux-like relay store with custom handlers and local schema.
I'll be guessing what your queries, components and fields might be named like so don't forget to change it to correct values
Somewhere in project's src folder create a file ClientState.client.graphql to extend your root query type with new field for client state:
// ClientState.client.graphql
type ClientState {
showToggleButton: Boolean!
eventsCount: Int
}
extend type Query {
clientState: ClientState!
}
this will allow you to wrap Toggle button with fragment like this:
fragment ToggleButton_query on Query {
clientState {
showToggleButton
eventsCount
}
}
and spread this fragment in parent query (probably AppQuery)
Then in your second query, where you'll be fetching events, add #__clientField directive, to define custom handle for that field:
query EventModal {
events #__clientField(handle: "eventsData") {
totalCount
}
}
Create EventsDataHandler for handle eventsData:
// EventsDataHandler.js
// update method will be called every time when field with `#__clientField(handle: "eventsData")` is fetched
const EventsDataHandler = {
update (store, payload) {
const record = store.get(payload.dataID)
if (!record) {
return
}
// get "events" from record
const events = record.getLinkedRecord(payload.fieldKey)
// get events count and set client state values
const eventsCount = events.getValue('totalCount')
const clientState = store.getRoot().getLinkedRecord('clientState')
clientState.setValue(eventsCount, 'eventsCount')
clientState.setValue(true, 'showToggleButton')
// link "events" to record, so the "events" field in EventModal is not undefined
record.setLinkedRecord(events, payload.handleKey)
}
}
export default EventsDataHandler
Last thing to do is to assign custom (and default) handlers to environment and create init store values:
// environment.js
import { commitLocalUpdate, ConnectionHandler, Environment, RecordSource, Store, ViewerHandler } from 'relay-runtime'
import EventsDataHandler from './EventsDataHandler'
// ...
const handlerProvider = handle => {
switch (handle) {
case 'connection':
return ConnectionHandler
case 'viewer':
return ViewerHandler
case 'eventsData':
return EventsDataHandler
default:
throw new Error(`Handler for ${handle} not found.`)
}
}
const environment = new Environment({
network,
store,
handlerProvider
})
// set init client state values
commitLocalUpdate(environment, store => {
const FIELD_KEY = 'clientState'
const TYPENAME = 'ClientState'
const dataID = `client:${FIELD_KEY}`
const record = store.create(dataID, TYPENAME)
record.setValue(false, 'showToggleButton')
// prevent relay from removing client state
environment.retain({
dataID,
variables: {},
node: { selections: [] }
})
store.getRoot().setLinkedRecord(record, FIELD_KEY)
})

Related

Electron: Can same channel name use for ipcMain.on and ipcMain.handle?

Can we register the same channel for ipcMain.on method and ipcMain.handle()?
For eg:
ipcMain.handle('test', async(event,args) => {
let result = await somePromise();
return result;
});
ipcMain.on('test', async(event,args) => {
event.returnValue = await somePromise();
});
Will the above code, give the error No handler register for 'test'? if ipcRenderer called this via invoke and sendSync in an order?
for eg:
ipcRenderer.invoke('test', data).then(result => {
console.log(result);
return result;
});
someFunction(data) {
return ipcRenderer.sendSync('test', data);
}
This is one of those things that you can easily test out.
Looking at their code for ipcMain.handle, they store the channel name in an _invokeHandlers map that seems isolated from the rest of the module (meaning from ipcMain.on).
In fact, ipcMain extends an EventEmitter, which is a Node class that maintains its own internal structure for managing events (This is the module where on and once are defined).
So you should be able to safely use both ipcMain.on("test", ...) and ipcMain.handle("test", ...) as long as you trigger them using the appropriate mechanism: send/sendSync corresponds with on/once and invoke corresponds with handle/handleOnce.

Priority web SDK method getSubform returns parent form

Parent form is the same I only change the company in the request and on some company i get parent form instead of sub form. So for example I have default company make a request get some form. According to user actions I change company in form request( but it is the same form name).
After that I try to get some sub form and on one particular company/form get the parent sub form instead of a child.
I already read topic about the problem that looks the same, but get rows and set active rows doesn't help me.
Do somebody had this problem?
async getDocuments(formName: string, getMappedRows: Function) {
const _currentOrderType = this.priorityService.getCurrentOrderType();
const _groupForm = await this.priorityService.getFormBridge(eSharedForm.GROUP.toString());
await _groupForm.setActiveRow(_currentOrderType.company.key);
const ListForm = await this.priorityService.getSubFormBridge(formName.toString(), _groupForm);
//some more logic
}
private async _getSubForm(name: string, form: any) {
try {
const subform = await form.startSubForm(name, this.onMessage, this.onUpdate);
debugger;
return subform;
} catch (e) {
this.subForms[name] = undefined;
const _e = JSON.stringify(e);
console.log(`[PriorityService::_getSubForm] Error starting subform '${name}': ${_e}`);
throw e;
}
}
and after that i see the same form as before
It is not clear from your code snippet what getSubFormBridge does or what is the value of _currentOrderType.company.key Is it numeric? is this the number of the row you want to retrieve?
When you start the Parent form, try to set autoRetrieveFirstRows to 1, which means, select the first row of the current form. If you know for sure what row you want to retrieve, pass the correct row number.
For example:
const form = await priority.formStart('ORDERS', this.onError, this.onSuccess, '', 1);
If you need to filter first and than select a row, use setActiveRow method and pass it the number of the row you want to retrieve. (Starts from 1)
Once the row is selected, retrieve the subform. For example:
const subform = await form.startSubForm("ORDERITEMS");

how to retain expand/collapse state in jqgrid grouping?

I have implemented a jqgrid in grouping method. By default I have kept the groups collapsed using groupCollapse:true parameter of jqgrid. My grid works well but When I expand the group and sort a column, the whole grid is reloaded and the expanded state of the column is not retained. How can I retain the expanded state while sorting?
Please write always which version of jqGrid, which you use (can use), and from which fork (free jqGrid, commercial Guriddo jqGrid JS or an old jqGrid in version <=4.7).
Your requirements could be easy realized in "free jqGrid", which I develop. It allows to use groupCollapse as callback function, which returns Boolean (see the issue). In combination with onClickGroup callback or jqGridGroupingClickGroup event one can easy persist the grouping state.
UPDATED: I created the demo https://jsfiddle.net/92da8xhq/, which demonstrates how one can persist the collapsing state in the grouping grid. Below I describe shortly the code. The demo uses one level of grouping to make the code more simple for understanding.
I added custom collapsedGroups: {} parameter to jqGrid. We will use the parameter to hold the list of collapsed groups. I used collapsedGroups: { "test2": true } in the demo to demonstrated that we can create the grid with some collapsed groups at the beginning. We don't use the value of the property of collapsedGroups object. Just the existence of the property test2 for example means that the group with the value test2 has collapsed state.
The demo uses groupCollapse property of groupingView defined as the callback function. The function tests whether the group is in the list of collapsed groups (has collapsedGroups property with some value)
groupingView: {
groupField: ["name"],
groupCollapse: function (options) {
var collapsedGroups = $(this).jqGrid("getGridParam", "collapsedGroups") || {};
// options looks like { group: number, rowid: string }
if (collapsedGroups.hasOwnProperty(options.group.value)) {
return true;
}
return false;
}
}
We adjust additionally the properties of the custom collapsedGroups parameter after expanding/collapsing of the group. We use the following onClickGroup callback:
onClickGroup: function (hid, isCollapsed) {
var p = $(this).jqGrid("getGridParam"),
iGroup = $(this).jqGrid("getGroupHeaderIndex", hid),
group = p.groupingView.groups[iGroup];
if (p.collapsedGroups == null) {
// be sure that the custom parameter is initialized as an empty object
p.collapsedGroups = {};
}
if (isCollapsed) {
// we place group.value in the p.collapsedGroups object as a property
if (!p.collapsedGroups.hasOwnProperty(group.value)) {
// create the property group.value in with some value
p.collapsedGroups[group.value] = true;
}
} else if (p.collapsedGroups.hasOwnProperty(group.value)) {
// remove group.value property from the p.collapsedGroups object
delete p.collapsedGroups[group.value];
}
}
groupingView: {
groupCollapse: true,
groupField: ["name"],
plusicon: 'ace-icon fa fa-plus-square purple',
minusicon: 'ace-icon fa fa-edit red'
}

Strategy for modifying a route parameter dynamically within the app

I'm developing a simple app that fetches the weekly lunch menu from a server. The query looks something like:
query {
lunchMenu(date: $date) {
id
name
etc.
}
}
I've defined a Route like so:
class LunchRoute extends Relay.Route {
static queries = {
lunchMenu: () => Relay.QL`
query { lunchMenu(date: $date) }
`,
};
static paramDefinitions = {
date: {required: false},
};
static routeName = 'LunchRoute';
}
I want the user to be able to select the date within the app via a datepicker and I've developed a LunchBrowser component which does that and renders the lunch menu. In index.js I have:
const lunchRoute = new LunchRoute({date: new Date()});
ReactDOM.render(
<Relay.RootContainer
Component={LunchBrowser}
route={lunchRoute}
/>,
document.getElementById('root')
);
The question is: how do I communicate a change in the date selection (from the LunchBrowser component) up one level so that I can modify the 'date' parameter in the Route and re-render?
I suppose I could have the LunchBrowser fire off some window event that is caught in index.js that does this, but this seems very wrong in that communication is flowing "up" from a lower-level component outside of React.
Or should I redefine my schema such that lunchMenu(date: $date) is pushed down one level? As in:
query {
lunch {
lunchMenu(date: $date) {
id
name
etc.
}
}
}
in which case lunch(date: $date) can be pushed down into LunchBrowserContainer's fragment and I no longer have to worry about dynamically modifying the Route's parameter.

Why isn't my updated observable List reflected in the template?

I've got:
my-app
community-list
On attached, my-app gets the user and loads the app.user. In the meantime, community-list is attached (even before app.user is loaded) and so I haven't been able to get the user's starred communities yet. Therefore, the solution I'm working on is as follows.
In community-list.attached():
app.changes.listen((List<ChangeRecord> records) {
if (app.user != null) {
getUserStarredCommunities();
}
});
Elsewhere in community-list is said metho:
// This is triggered by an app.changes.listen.
void getUserStarredCommunities() {
// Determine if this user has starred the community.
communities.forEach((community) {
var starredCommunityRef = new db.Firebase(firebaseLocation + '/users/' + app.user.username + '/communities/' + community['id']);
starredCommunityRef.onValue.listen((e) {
if (e.snapshot.val() == null) {
community['userStarred'] = false;
} else {
community['userStarred'] = true;
}
});
});
}
Note that communities is an observable list in community-list:
#observable List communities = toObservable([]);
Which is initially populated in community-list.attached():
getCommunities() {
var f = new db.Firebase(firebaseLocation + '/communities');
var communityRef = f.limit(20);
communityRef.onChildAdded.listen((e) {
var community = e.snapshot.val();
// If no updated date, use the created date.
if (community['updatedDate'] == null) {
community['updatedDate'] = DateTime.parse(community['createdDate']);
}
// snapshot.name is Firebase's ID, i.e. "the name of the Firebase location"
// So we'll add that to our local item list.
community['id'] = e.snapshot.name();
// Insert each new community into the list.
communities.add(community);
// Sort the list by the item's updatedDate, then reverse it.
communities.sort((m1, m2) => m1["updatedDate"].compareTo(m2["updatedDate"]));
communities = communities.reversed.toList();
});
}
In summary, I load the list of communities even before I have a user, but once I have a user I want to update each community (Map) in the list of communities with the userStarred = true/false, which I then use in my community-list template.
Alas, it doesn't seem like the List updates. How do I achieve this?
This whole app.changes.listen business is expensive. What's the proper practice in a case like this, where an element is loaded before I load objects (like app.user) that will modify it in some way.
1)
toList() creates a copy of the list. You need to apply toObservable again to get an observable list.
communities = toObservable(communities.reversed.toList());
This also assigns a new list to communities which is covered by #observable.
I think it should trigger anyway
2) You update your communities explicitly. It shouldn't be necessary to listen for changes. You can call a method containing
if (app.user != null) {
getUserStarredCommunities();
}
explicitly each time you change the list.
You also call Firebase for each community when a change in communities occurs. I don't know Firebase but it seems you send a request to a server each time which is of course expensive.
You should remember for what user+community combination you already made the call and use the remembered result instead.
With app.changes.listen you listen to any updated of any #observable field in your component. If you have other observable fields beside communities this method might be called too often.
If you are only interested in changes to communities you should put this code into a method like
communitiesChanged(oldVal, newVal) {
if (app.user != null) {
getUserStarredCommunities();
}
}
but the better option is to not listen to changes and another method name and call it explicitly as state above anyways if possible.

Resources