Sails.js multiple controllers/models to one view - sails.js

I've been trying out sails and so far I've been able to display an index of all items in a database table model. However, I have two different models, modelA and modelB and I would like to display them to a single view called dashboard in the root of views - /dashboard.
However, I'm having a hard time trying to understand how to get the different controllers display the list unto the dashboard. How do I get this done, can it be done with the two different controllers and if so how. Or can I create a separate controller called dashboard calling the different instances of the models?
Having a hard time wrapping my head around this ...

If i understand correctly, you want a controller that fetches data from 2 different models and sends that data into a view.
Since node now supports it, i would use async/await for the example, but its the same with callbacks but you have to nest your calls or use caolan async.
const DashBoardController = {
async show(req, res){
const instanceA = await ModelA.findOne(1) // I used a hardcoded number for simplicity
const instanceB = await ModelB.findOne(1)
res.view('mycrontroller/dashboard', {instanceA, instanceB})
}
}
export DashBoardController;
Then in your view you just access it with the name of the keys you defined...instanceA and instanceB
If that's not what you meant, then let me know.

Related

Passing a selected database to a DBcontext via DI

Here is my scenario. Imagine a screen with a dropdown of US states. This list is populated from one Admin database. Depending on the choice of other items on the screen get filled with other databases. we have a database per state that share a single schema. I don't have any issue using DI for the States dropdown. However, I am having issues with getting the selected state. I tested hardcoding a state and DI works fine. I would like to use Session for this, but I have read you can't and frankly, I have not been able to make it work. Any suggestions would be appreciated.
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddScoped(p => p.GetService<IHttpContextAccessor>()?.HttpContext);
services.AddDbContext<AdminManagement.Data.AdminDataContext>(options =>
options.UseSqlServer(Configuration.GetSection("Connections:myAdmin").Value).UseQueryTrackingBehavior(QueryTrackingBehavior.NoTracking));
//this is the issue here I want to be able to pass the selected state
services.AddDbContext<CollectionDataContext>((serviceProvider, builder) =>
{
//I wish I could use this...any alternatives?
//HttpContext.Session.GetString("SelectedState");
//hardcoded for testing purposes. it works ok
var selectedDb = "SC";
//this gets the connection string from app settings, later I will get it from an API
var connectionString = GetConnectionStringFromService(selectedDb);
builder.UseSqlServer(connectionString);
});
//my one admin database Data context
services.AddScoped<AdminManagement.Data.AdminManagementQueries>();
// my multiple databases clases that use DI
services.AddScoped<CollectionManagementQueries>();
services.AddScoped<CollectionManagementCommands>();
You need to retrieve the context from the service provider. That's done via:
var httpContextAccessor = serviceProvider.GetRequiredService<IHttpContextAccessor>();
Then, you can do something like:
var selectedDb = httpContextAccessor.HttpContext.Session.GetString("SelectedState");
Note, however, that IHttpContextAccessor is not registered by default. You can fix that by adding the following in ConfigureServices:
services.AddHttpContextAccessor();

Angular Dynamic form observable property binding

I have a problem with some dynamically generated forms and passing values to them. I feel like someone must have solved this, or I’m missing something obvious, but I can't find any mention of it.
So for example, I have three components, a parent, a child, and then a child of that child. For names, I’ll go with, formComponent, questionComponent, textBoxComponent. Both of the children are using changeDetection.OnPush.
So form component passes some values down to questionComponent through the inputs, and some are using the async pipe to subscribe to their respective values in the store.
QuestionComponent dynamically creates different components, then places them on the page if they match (so many types of components, but each questionComponent only handles on one component.
some code:
#Input() normalValue
#Input() asyncPipedValue
#ViewChild('questionRef', {read: ViewContainerRef}) public questionRef: any;
private textBoxComponent: ComponentFactory<TextBoxComponent>;
ngOnInit() {
let component =
this.questionRef.createComponent(this.checkboxComponent);
component.instance.normalValue = this.normalValue;
component.instance. asyncPipedValue = this. asyncPipedValue;
}
This works fine for all instances of normalValues, but not for asyncValues. I can confirm in questionComponent’s ngOnChanges that the value is being updated, but that value is not passed to textBoxComponent.
What I basically need is the async pipe, but not for templates. I’ve tried multiple solutions to different ways to pass asyncValues, I’ve tried detecting when asyncPipeValue changes, and triggering changeDetectionRef.markForChanges() on the textBoxComponent, but that only works when I change the changeDetectionStrategy to normal, which kinda defeats the performance gains I get from using ngrx.
This seems like too big of an oversight to not already have a solution, so I’m assuming it’s just me not thinking of something. Any thoughts?
I do something similar, whereby I have forms populated from data coming from my Ngrx Store. My forms aren't dynamic so I'm not 100% sure if this will also work for you.
Define your input with just a setter, then call patchValue(), or setValue() on your form/ form control. Your root component stays the same, passing the data into your next component with the async pipe.
#Input() set asyncPipedValue(data) {
if (data) {
this.textBoxComponent.patchValue(data);
}
}
patchValue() is on the AbstractControl class. If you don't have access to that from your question component, your TextBoxComponent could expose a similar method, that can be called from your QuestionComponent, with the implementation performing the update of the control.
One thing to watch out for though, if you're also subscribing to valueChanges on your form/control, you may want to set the second parameter so the valueChanges event doesn't fire immediately.
this.textBoxComponent.patchValue(data, { emitEvent: false });
or
this.textBoxComponent.setValue(...same as above);
Then in your TextBoxComponent
this.myTextBox.valueChanges
.debounceTime(a couple of seconds maybe)
.distinctUntilChanged()
.map(changes => {
this.store.dispatch(changes);
})
.subscribe();
This approach is working pretty well, and removes the need to have save/update buttons everywhere.
I believe I have figured out a solution (with some help from the gitter.com/angular channel).
Since the values are coming in to the questionComponent can change, and trigger it's ngOnChanges to fire, whenever there is an event in ngOnChanges, it needs to parse through the event, and bind and changes to the dynamic child component.
ngOnChanges(event) {
if (this.component) {
_.forEach(event, (value, key) => {
if (value && value.currentValue) {
this.component.instance[key] = value.currentValue;
}
});
}
}
This is all in questionComponent, it resets the components instance variables if they have changed. The biggest problem with this so far, is that the child's ngOnChanges doesn't fire, so this isn't a full solution. I'll continue to dig into it.
Here are my thoughts on the question, taking into account limited code snippet.
First, provided example doesn't seem to have anything to do with ngrx. In this case, it is expected that ngOnInit runs only once and at that time this.asyncPipedValue value is undefined. Consequently, if changeDetection of this.checkboxComponent is ChangeDetection.OnPush the value won't get updated. I recommend reading one excellent article about change detection and passing async inputs. That article also contains other not less great resources on change detection. In addition, it seems that the same inputs are passed twice through the component tree which is not a good solution from my point of view.
Second, another approach would be to use ngrx and then you don't need to pass any async inputs at all. Especially, this way is good if two components do not have the parent-child relationship in the component tree. In this case, one component dispatches action to put data to Store and another component subscribes to that data from Store.
export class DataDispatcherCmp {
constructor(private store: Store<ApplicationState>) {
}
onNewData(data: SomeData) {
this.store.dispatch(new SetNewDataAction(data));
}
}
export class DataConsumerCmp implements OnInit {
newData$: Observable<SomeData>;
constructor(private store: Store<ApplicationState>) {
}
ngOnInit() {
this.newData$ = this.store.select('someData');
}
}
Hope this helps or gives some clues at least.

Fiori -- Cross application navigation; Handling startup parameters?

This question is related to:
Fiori - Cross Application Navigation
http://help.sap.com/saphelp_uiaddon10/helpdata/en/07/9561b716bb4f2f8ae4e47bacbdb86d/content.htm
Remove URL params on routing
My use case is like this:
I have multiple applications that should link to others (deep).
Since documentation of cross navigation mention to avoid deep links I decided to use a startup parameter.
For example:
Application A has a list of some items in the detail view of one item there is a reference to another application B that contains some other details.
Assume A shows article details and B shows some details of a producer of an article.
Application A would now use some navigation like this:
sap.ushell.Container.getService("CrossApplicationNavigation").hrefForExternal({
target : { semanticObject : "ApplicationB", action : "display" },
params : { "someID" : "102343333"}
})
Now in application B I use code like this inside the Component.js at the end of the init method.
var oRouter = that.getRouter().initialize();
var oComponentData = this.getComponentData();
if (oComponentData.startupParameters) {
oRouter.navTo("SomeView", {
someId : oComponentData.startupParameters.someID[0],
}, false);
}
First question: Is this the right place for handling the startup parameters?
Second question: If I using the navigation the startup parameter will still be in the code, I would prefer to remove it, but how?
Update
In the target application (B) it would lead to the following URL:
https://server/sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html?sap-client=100&sap-language=EN#SemObject-display?someID=102343333&/SomeView(102343333)/
Anyhow I would prefere to have something like this:
https://server/sap/bc/ui5_ui5/ui2/ushell/shells/abap/FioriLaunchpad.html?sap-client=100&sap-language=EN#SemObject-display?/SomeView(102343333)/
The parameter must be retrieved as
var oComponentData = this.getComponentData();
if (oComponentData.startupParameters) {
oRouter.navTo("SomeView", {
someId : oComponentData.startupParameters.someID[0],
}, false);
as you write. In Fiori applications, the startup parameters injected into the Component data of your constructor may have been renamed, enriched by further default values etc.. Thus they may be distinct from the parameter one observes in the url. Applications are advised to refrain from trying to inspect the URL directly.
If one supplies a very long set of url parameters, one will observer that the FLP replaces some of them with sap-intent-param=AS123424 ("compacted URL") to work around url length restrictions on some platforms and in bookmarks, in the
getComponentData().startupParameters one will receive the full set of parameters).
As to the second question.
No, there is currently no way to "cleanse" the URL and avoid the redundancy between and inner app route.
SemObject-display?someID=102343333&/SomeView(102343333)/
which after navigation may look like
SemObject-display?someID=102343333&/SomeView(102343999)/
App was started with 102343333, but then user navigated within the app to another item (102343999).
Any change in the "Shell-part" of the has (SemObject-display?someID102343333) will lead to a cross-app-navigation (reinstantiation of your component) with a different startupParameter.
(There are cases where this is desired in the flow, e.g. a cross navigation from a OrgUnit factsheet to the parent OrgUnit factsheet via a link).
There were ideas within SAP to fuse the inner-app routes and the intent parameters, but they were not carried out, as it's mostly url aesthetics.
Note: To support boomarking, one has to respect both startup parameters and
inner app route during component instantiation,
assuming the user created a bookmark on
SemObject-display?someID=102343333&/SomeView(102343999)/
(While he was looking at 9999(!)).
When reinstantiating the app, the inner app route should take higher precedence than startup-parameters.
So amend the code to:
var oComponentData = this.getComponentData();
if (oComponentData.startupParameters) {
if (sap.ui.core.getHashChanger().getHash()=== "") {
// if no inner app route present, navigate
oRouter.navTo("SomeView", {
someId : oComponentData.startupParameters.someID[0],
}, false);
}
}
https://sapui5.netweaver.ondemand.com/#docs/api/symbols/sap.ushell.services.CrossApplicationNavigation.html
SAP Fiori Launchpad for Developers, Navigation Concept
http://www.sdn.sap.com/irj/scn/go/portal/prtroot/docs/library/uuid/907ae317-cb47-3210-9bba-e1b5e70e5c79?QuickLink=index&overridelayout=true&59575491523067
I was having issues navigating from a Fiori elements app in to a deep page in a freestyle UI5 app and then answer from #user6649841 provided most the solution for my requirement.
In my instance, navigating from the elements list report (app "A") in to the target freestyle app (app "B") I didn't want the worklist/initial page in app B to display at all and instead go straight to the detail page without a flickering of the initial app screen.
The below worked for me, note though it doesn't solve the ugly URL issues. In my case I'm not fussed about it as my nav back will nav back to the elements list report (App A) and never show the worklist page in App B so the user will never make another search on top of this URL which would lead with inconsistent inner and outer keys
Component.js (at end of init function after all the standard sap code, but before router initialization):
var oComponentData = this.getComponentData();
var startupParams = oComponentData.startupParameters;
if (startupParams && startupParams.myQueryStringParamName && startupParams.myQueryStringParamName[0]) {
//In my case using hash changer as I dont want the original landing page (default route) to be
//in the history, so the detail page loads straight away and nav back will cause to nav back to App A
var hashChanger = sap.ui.core.routing.HashChanger.getInstance();
hashChanger.replaceHash("detailPage/" + startupParams.myQueryStringParamName[0]);
}
//initialise after the above so the new hash is set and it doesnt initially load the
//page assigned to the default route causing a flickering and nav slide effect
this.getRouter().initialize();
Looking at the UI5 SDK in UI5 1.48 and above in the initialize method of router you can pass in a boolean to tell it to ignore the initial hash so possibly can do a simpler implementation in newer releases of UI5
Is Component.js right place for handling the startup parameters?
Depends,if you have multiple views and you want to dynamically route based on the incoming parameters. Else you can handle in specific view also.
Your second question was not quite clear to me.
Nevertheless, if you want to only specific cases of startup parameters, then from Source App, set some flag to understand where is the request coming from and handle accordingly. So this way, your normal navigation won't be tampered.

SAPUI5 bindAggregation complete for a Table

I am binding an aggregation to a table . I couldn't find an event which is triggered after the binding is complete . There is "updateFinished" event for sap.m.List , which is exactly what I am looking for in a Table (and a dropodown). I thought of using attachRequestCompleted() on the model , but the model is used at other places where I do not want this event to trigger.
Is there anyway to trigger a event once the databinding is complete on a Table (and a dropdown)?
Any help is appreciated.
Thanks in advance.
update: There is "updateFinished" event for table extended from ListBase. I am still not sure how I missed it before I posted this question. But, the question is still valid for a dropdown and TableSelectDialog controls.
I also stumbled upon that problem, but in a different Context.
I have a Grid layout in which I dynamically load Panels via an oData Model.
Therefore I have entered the path in my XML Grid-View element.
<l:Grid id="grid" content="{some path...}">...</l:Grid>
Now I wanted to set the grid view busy and when the data is loaded revert this.
Therefore I use the Binding of the grid view.
In the Controllers onInit method I have added:
this._oGrid = this.getView().byId("grid");
this.getRouter().attachRouteMatched(this._onRouteMatch.bind(this));
Please note that the bind method is not available in every browser. You need to apply a polyfill. (See https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind)
Also Bind has nothing to do with the binding :D
I needed to do this because the Binding is not available in the onInit function.
The _onRouteMatched function:
var oContent = this._oGrid.getBinding("content");
oContent.attachDataReceived(function(oData) {
this._oGrid.setBusy(false);
}.bind(this));
Now when the data is received the busy option is set to false.
If you want to show a 'loading' indicator for your table while the data is still loading (and thus not bound), I think the best approach is the following:
Use a dedicated JSONModel which holds only UI-specific stuff, like toggling enabled/readonly/busy/etc properties of controls.
In your case, something like:
var oUIModelData = {
tableIsBusy : false,
//etc, things like :
btnSubmitEnabled : false,
someControlVisible : true
};
var oUIModel = new sap.ui.model.json.JSONModel();
oUIModel.setData(oUIModelData);
sap.ui.getCore().setModel(oUIModel, "UIModel");
In your table definition, bind the busy property to {UIModel>/tableIsBusy} and set the table's property busyIndicatorDelay to 0 to avoid any delays
Just before you do your OData service call, set the property tableBusy to true. This will immediately show the busy overlay to your table:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, true);
//here your OData read call
In your OData service's requestCompleted (and if needed, also in requestFailed) event handlers, set the busy property of the UIModel back to false:
sap.ui.getCore().getModel("UIModel").setProperty(tableIsBusy, false);
The big benefit of this approach is (IMHO) instead of relying on each control to check whether the data has been loaded, simply do it during the actual load.
And by binding these UI-related things to a (separate) model saves you from writing lots of extra code ;-)
In general you could solve the problem by using batch processing on the OData service. According to https://sapui5.netweaver.ondemand.com/docs/guide/6c47b2b39db9404582994070ec3d57a2.html:
Use OData model v2.
var oModel = new sap.ui.model.odata.v2.ODataModel(myServiceUrl);
Define a set of deferred batch groups.
oModel.setDeferredBatchGroups(["myGroupId", "myGroupId2"]);
Add the batch group information to the corresponding bindings, e.g:
{
path:"/myEntities",
parameters: {
batchGroupId: "myGroupId"
}
}
All read/query actions on bindings in a certain batch group will be held back until a .submitChanges(.) call on the batch group is made.
oModel.submitChanges({
batchGroupId: "myGroupId",
success: mySuccessHandler,
error: myErrorHandler
});
Use the success/error handlers to execute actions.
This rather generic approach gives you additional control such as trigger actions, grouping and event handling on the OData model.

GXT (Ext-GWT) + Pagination + HTTP GET

I'm trying to populate a GXT Grid using data retrieved from an online API (for instance, going to www.example.com/documents returns a JSON array of documents). In addition, I need to paginate the result.
I've read all the various blogs and tutorials, but most of them populate the pagination proxy using something like TestData.GetDocuments(). However, I want to get that info using HTTP GET.
I've managed to populate a grid, but without pagination, using a RequestBuilder + proxy + reader + loader. But it seems as though the actual loading of the data is "put off" until some hidden stage deep inside the GXT code. Pagination requires that data from the start, so I'm not sure what to do.
Can someone provide a simple code example which does what I need?
Thank you.
I managed to get this going, here is what I did:
First I defined the proxy and loader for my data along with the paging toolbat:
private PagingModelMemoryProxy proxy;
private PagingLoader<PagingLoadResult<ModelData>> loader;
private PagingToolBar toolBar;
Next is the creation of each one, initializing with an empty ArrayList.
proxy = new PagingModelMemoryProxy(new ArrayList<EquipmentModel>());
loader = new BasePagingLoader<PagingLoadResult<ModelData>>(proxy);
loader.setRemoteSort(true);
toolBar = new PagingToolBar(100);
toolBar.bind(loader);
loader.load(0, 100);
Last, I have a set method in my view that gets called when the AJAX call is complete, but you could trigger it anywhere. Here is my entire set method, Equipment and EquipmentModel are my database and view models respectively.
public void setEquipmentData(List<Equipment> data)
{
Collections.sort(data);
// build a list of models to be loaded
List<EquipmentModel> models = new ArrayList<EquipmentModel>();
for (Equipment equipment : data)
{
EquipmentModel model = new EquipmentModel(equipment);
models.add(model);
}
// load the list of models into the proxy and reconfigure the grid to
// refresh display.
proxy.setData(models);
ListStore<EquipmentModel> equipmentStore = new ListStore<EquipmentModel>(loader);
equipmentGrid.reconfigure(equipmentStore, equipmentColumnModel);
loader.load(0, 100);
}
The key here for me was re-creating the store with the same loader, the column model was pre-created and gets reused.