Refreshing lookups in SPA with Breeze.js when value has changed - breeze

I have an application that records a transaction and the user can pick the category from a drop down.
Categories are loaded up at application startup as they are "mostly" static / rarely going to change.
So, in my datacontext.js I do the usual and prime my data;
var primeData = function () {
var promise = Q.all([
getLookups(),
getBankAccountPartials(null, true)])
.then(applyValidators);
return promise.then(success);
function success() {
datacontext.lookups = {
categories: getLocal('Categories', 'name', true),
transactiontypes: getLocal('TransactionTypes', 'name', true),
payees: getLocal('Payees', 'name', true)
};
log('Primed data', datacontext.lookups);
}
function applyValidators() {
model.applyBankAccountValidators(manager.metadataStore);
}
};
function getLookups() {
return EntityQuery.from('Lookups')
.using(manager).execute()
.then(processLookups)
.fail(queryFailed);
}
Now, occasionally in an Admin screen the user can edit and add a category.
In the categoryadd.js viewmodel my save code looks something like this (extract shown);
save = function () {
isSaving(true);
datacontext.saveChanges()
.then(goToEditView).fin(complete);
function goToEditView(result) {
router.replaceLocation('#/categorydetail/' + category().id());
}
function complete() {
isSaving(false);
}
},
How do I refresh just the Categories lookup data? Or, am I just doing this wrong and should perhaps NOT have categories as a lookup?
Thanks.

Breeze.js synchronises automatically and knows to search out the Category and update it in its lookup list.
I checked this by calling datacontext.lookups from the browser console after the save had been performed and inspecting the objects it showed me the category name had been refreshed.

Related

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

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)
})

BreezeJS executeQueryLocally and return string

I can execute a query in breeze from the server (using EF) which returns a load of boostrap data thus:
em.executeQuery(_lookupsQuery).then(function (data) {
_lookups = data.results;
console.log(_lookups[0].currentUserId);
This returns currentUserId which is a guid. I then store em using local storage for querying locally later:
_lookups = [{
currentUserId: em.executeQueryLocally(_lookupsQuery.toType(breeze.DataType.String))
}];
However this does not work as it requires an entity type e.g:
em.executeQueryLocally(_lookupsQuery.toType(em.metadataStore.getEntityType("Measure")))
Since currentUserId is a guid I am not sure which type to cast the query to. I have tried to make an entity type on the client just for this but it does not seem to work. Any help on solving this would be appreciated.
Edit:
After a suggestion, I modified lookups:
[HttpGet]
public async Task<object> Lookups()
{
var currentUser = await UserManager.FindById(Guid.Parse(User.Identity.GetUserId()));
var companyId = currentUser.CompanyId.Value;
return new
{
currentUser = new
{
Id = currentUser.Id
}
};
}
When querying remotely using:
em.executeQuery(_lookupsQuery).then(function (data) {
_lookups = data.results;
console.log(_lookups[0].currentUser);
I get:
Object { id="f2dceb4b-29e7-4533-99e2-2052dc39143a"}
I set up the new entity type:
metadataStore.addEntityType({
shortName: "CurrentUser",
dataProperties: {
id: { dataType: "String", isPartOfKey: true }
}
});
but when I query locally:
_lookups = [{
currentUser: em.executeQueryLocally(_lookupsQuery.toType(em.metadataStore.getEntityType("CurrentUser"))) }];
console.log(_lookups[0].currentUser);
this returns []
What am I doing wrong?
There are a few ways you can handle it. I am only going to touch on the two most basic methods that I personally use and hopefully one sparks your interest.
-1. Create an entityType in your metadataStore for user-type information.
metadataStore.addEntityType({
shortName: "User",
dataProperties: {
userId: { dataType: "String", isPartOfKey: true },
userName: { dataType: "String" }
}
});
This will add an entity type that you can serialize your return results to. It is important to note here that if you change anything about that user without accepting changes locally it will try to save it next time you call saveChanges() so make sure you handle those situations if applicable.
Of course that isn't the only option. You can most certainly just grab that user id from the query results without Breeze ever knowing about it or what it is for.
-2. POJO
function user(data) {
var self = this;
self.UserId = data.userId;
self.UserName = data.userName;
}
// After your query returns data
query.execute().then(userReturned);
function userReturned(data) {
// data is the returned object, which contains
// an httpResponse which is what breeze returns
// as the raw response
new user(data.httpResponse.data);
}
This method basically just grabs the httpResponse when the promise returns and uses it to create a Plain Old JavaScript Object without Breeze knowing about it. Of course for this to work you need to examine your data object that is returned and find what you are looking for and serialize that.
Edit
Your query locally should look like this -
query = breeze.entityQuery().from("Whatever").toType("CurrentUser").executeLocally();
em.executeQueryLocally(query);

BreezeJS - Using expand

I am querying the server to get an entity with expand
function _loadIncidents() {
var deffered = Q.defer(),
queryObj = new breeze.EntityQuery().from('Incidents').expand(['Deployments', 'IncidentComments', 'DTasks', 'ExtendedProperties', 'IncidentEvents']);
dataRepository.fetchEntitiesByQuery(queryObj, true).then(function (incidents) {
var query = breeze.EntityQuery.from("DTasks"),
incidentIds = dataRepository.getEntitiesByQuerySync(query);
deffered.resolve();
}, function(err) {
deffered.reject(err);
});
return deffered.promise;
};
I am getting the results and all is fine, how ever when I query breeze cache to get the entities - I am getting empty collection. So when using expand does the expanded entities are added to the cache?
Yes the related entities identified in the expand should be in cache ... if the query is "correct" and the server interpreted your request as you intended.
Look at the payload of the response from the first request. Are the related entities present? If not, perhaps the query was not well received on the server. As a general rule, you want to make sure the data are coming over the wire before wondering whether Breeze is doing the right thing with those data.
I do find myself wondering about the spelling of the items in your expand list. They are all in PascalCase. Are they these the names of navigation properties of the Incident type? Or are they the names of the related EntityTypes? They need to be former (nav property names), not the latter.
I Had problem with the navigation property - as I am not using OData webapi not using EF , there is problem with the navigation properties so for the current time i just wrote
Object.defineProperty(this, 'Deployments', {
get: function () {
return (this.entityAspect && this.entityAspect.entityManager) ?
this.entityAspect.entityManager.executeQueryLocally(new breeze.EntityQuery("Deployments").
where('IncidentID', 'eq', this.IncidentID)) :
[];
},
set: function (value) { //used only when loading incidents from the server
if (!value.results) {
return;
}
var i = 0,
dataRepository = require('sharedServices/dataRepository');
for (i; i < value.results.length; i++) {
dataRepository.addUnchangedEntity('Deployment', value.results[i]);
}
},
enumerable: true
});

Breeze querying local cache with EF and Web API

Problem
I have a view with 6 drop downs. Each of which is being populated by a Web API call. I want
to use breeze to run the query locally once it has populated from the remote server
The code runs fine when the data call is against the server. The issue is when trying to query the local cache. I never get any results returned. Is my approach flawed or am I doing something wrong ?
SERVER SIDE
View model
class genericDropDown()
{
public int value{get;set;}
public string option{get;set;}
}
The WebAPI [A single sample method]
[HttpGet]
// GET api/<controller>
public object GetSomeVals()
{
return _context.getClinician();
}
The Repository [A single sample method]
public IEnumerable<genericDropDown> getDropDownVal()
{
return context.somemodel(a=>new{a.id,a.firstname,a.lastname}).ToList().
Select(x => new GenericDropDown
{ value = x.id, option = x.firstname+ " " + x.lastname});}
}
CLIENT SIDE
Datacontext.js
var _manager = new breeze.EntityManager("EndPoint");
//Being called from my view model
var getDropDownBindings = function(KO1, KO2) {
//First add the entity to the local metadatastore then populate the entity
$.when(
addDD('clinicianDropDown', webAPIMethod),
getData(KO1, webAPIMethod, null, 'clinicianDropDown'),
addDD('docTypeDropDown', webAPIMethod);
getData(KO2, webAPIMethod, null, 'docTypeDropDown'),
).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
logger.log('Got drop down vals', "", 'dataContext', true);
}
};
//Add the entity to local store. First param is typename and second is
resource name (Web API method)
var addDD = function(shortName,resName) {
_manager.metadataStore.addEntityType({
shortName: shortName,
namespace: "Namespace",
autoGeneratedKeyType: breeze.AutoGeneratedKeyType.Identity,
defaultResourceName:resName,
dataProperties: {
value: { dataType: DataType.Int32,
isNullable: false, isPartOfKey: true },
option: { dataType: DataType.String, isNullable: false }
}
});
return _manager.metadataStore.registerEntityTypeCtor(shortName, null, null);
};
//Get the data
var getData = function(observableArray, dataEndPoint, parameters, mapto) {
if (observableArray != null)
observableArray([]);
//TO DO: Incorporate logic for server or local call depending on
// whether this method is accessed for the first time
var query = breeze.EntityQuery.from(dataEndPoint);
if (mapto != null && mapto != "")
query = query.toType(mapto);
if (parameters != null)
query = query.withParameters(parameters);
//This approach doesnt work on local querying as Jquery complains
//there is no 'then' method. Not sure how to implement promises
//when querying locally
/* return _manager.executeQuery(query).then(querySucceeded).fail(queryFailed);
function querySucceeded(data) {
if (observableArray != null)
observableArray(data.results);
}
*/
//The array length from this query is always 0
var data = _manager.executeQueryLocally(query);
observableArray(data.results);
return;
};
//Generic error handler
function queryFailed(error) {
logger.log(error.message, null, 'dataContext', true);
}
viewmodel.js
//In Durandal's activate method populate the observable arrays
dataContext.getDropDownBindings (KO1,KO2);
Viewmodel.html
<select class="dropdown" data-bind="options: KO1, optionsText: 'option', value: 'value', optionsCaption: 'Clinicians'"></select>
<select class="dropdown" data-bind="options: KO2 optionsText: 'option', value: 'value', optionsCaption: 'Document Types'"></select>
You can only execute local queries against types that are described by metadata.
Without more information I can't be sure, but my guess is that your GetSomeVals method is not returning 'entities' but just loose data. In other words, the types of objects returned from the GetSomeVals method must be entities (or contain entities within a projection) in order for breeze to be able to perform a local query. This is because Breeze knows how to cache and query entities but has no ideas how to cache 'arbitrary' query results.
Note that you can return an anonymous type containing entities of different types from the server, (in order to populate mostly static small datasets), but the individual items must be 'entities'. In this case, Breeze will take apart the anon result and pick out any entities to include in the EntityManager cache.
Per you question of how to perform an local query with promises, use the FetchStrategy.FromLocalCache with the using method.
i.e. this query
var results = em.executeQueryLocally(query)
can also be expressed as:
query = query.using(FetchStrategy.FromLocalCache);
return em.executeQuery(query).then(data) {
var results = data.results;
}
The local query is still executed synchonously but is made to look async.

what is the right emberjs way to switch between various filtering options?

I've an individualStore (extends from Em.ArrayController), whose task is to keep an array of individual objects. There are several APIs that my application calls, and they return individual objects which are sent to the store. Think about it as the database of cached individual records in my application.
App.individualStore = App.ArrayController.create({
allIndividuals: function () {
return this.get('content').sort(function (a, b) {
return (b.votes_count - a.votes_count);
});
}.property('#each.votes_count').cacheable(),
aliveIndividuals: function () {
return this.get('content').filter(function (individual) {
return (!!individual.living);
}).sort(function (a, b) {
return (b.votes_count - a.votes_count);
});
}.property('#each.living', '#each.votes_count').cacheable(),
deceasedIndividuals: function () {
return this.get('content').filter(function (individual) {
return (!individual.living);
}).sort(function (a, b) {
return (b.votes_count - a.votes_count);
});
}.property('#each.living', '#each.votes_count').cacheable()
});
My view has a `individualsBinding: 'App.individualStore.allIndividuals', which renders up as intended perfectly.
I want to add filtering buttons, e.g. Show: All | Alive | Deceased. What would be the right way to change the filtering here? Keep in mind that whatever the criteria is, I'd like it to keep in sync with individualStore always.
Someone suggested to change bindings on runtime,
this.bind('users', Ember.Binding.from('App.individualStore.aliveIndividuals'));
This works in my first two-three clicks on these buttons, but then it freezes the browser (sort of infinite loop?).
This also doesn't feel like the best option to me. I'm new to ember, so anything you say would be helpful. Thanks in advance.
I would make the filter function itself a property and by changing a filterName on the controller, you are notified and accordingly update the filtered content, see http://jsfiddle.net/pangratz666/ypcLq/
App.controller = Ember.ArrayProxy.create({
content: [],
filterName: 'all',
allFilter: function() {
return true;
},
aliveFilter: function(individual) {
return ( !! individual.living);
},
deceasedFilter: function(individual) {
return (!individual.living);
},
filtered: function() {
var filterName = this.get('filterName');
var filterFunc = this.get(filterName + 'Filter');
return this.filter(filterFunc).sort(function(a, b) {
return (b.votes_count - a.votes_count);
});
}.property('content.#each', 'filterName').cacheable()
});
So you can later in your view set the filter which shall be used via App.controller.set('filterName', 'alive').
Just as a note: you can chain filters via this.filter(filterFunc1).filter(filterFunc2) so you could for example filter all alive individuals of a specific age, ...

Resources