A week ago I ran into a problem with emberjs and DataTables.
I was using ember-data to get data from the asp codebehind using webmethods based on the route parameters. Then I would use that data to create a table with datatables. However, when I changed the route, which changed the data and therefore changed the html, datatables would add the rows, but it wouldn't remove the old rows. In addition none of the functionality would work on the new rows and whenever I would sort, it would remove the new data.
Please let me know if anyone has a better answer than the one I posted.
I looked and found a lot of questions on this topic, or similar topics; However all of the solutions were hacky or costly performance-wise. So I found my own.
It isn't perfect; I would love for ember to implement an event based on this.
I added a controller initially for navigating in my application view. The change event looks mostly like this:
paramsChanged: function () {
if (this.type && this.filingType.value && this.year && this.period) {
this.transitionToRoute('application');
Ember.run.next(this, function () {
this.transitionToRoute(this.type.value, this.year, this.period);
});
//console.log('persist');
}
}.observes('type', 'year', 'period')
This is changing the route to application(basically removing the sub view) then moving to whichever route I need next.
The performance cost, although untested, should be negligible. I need to run the code for creating the view anyway, and I'm already in the application. I'm destroying a little bit extra by transitioning to the index, then I'm recreating the sub-view on the next run loop causing the initialization code contained in didInsertElement to be run.
Related
I am loading server response in a datatable using js in a asp.net core razor page. Because the data/UI is complex, I need to render different layouts based on current value in each table cell.
Datatable supports a renderer function for each cell, so I could have something like:
...
"data":"somefield",
"renderer":function(data,type,row,meta){
if (data.someId)=="someValue"{
return "<div... some label with somValue</div>"
}else{
return "<div... some label without value</div>"
}
}
}
This works perfectly fine, however when divs get complex with style and many labels it becomes harder to maintain or change.
I did look a bit into Razor's PartialViews as it may seem like a good alternative. Having the UI in a cshtml file, being able to pass parameters from parent #Model and using c# in it to render it based on the parameter received.
While I am able to load the partial view in the parent page, using <partial name=''/> or #Html.Partial(...) I didn't manage to get it's content in js using $.get and return it in the datatable's render function. Probably async wouldn't work in this case? Or it would be too slow?
My question is: what would be a better way to handle this situation? Maybe partial views are not the way. I am looking for a way of easily maintaining/changing the cell content. Thank you for your time.
I'm not really familiar with asp.net but I will try to answer your question.
I don't think PartialViews are going to work in the way you suggest because they appear to be server-side code, and trying to do a GET request for every line in your table could potentially generate a large number of server requests.
I think you have a couple of potential solutions. First is to loop through your data on the server, and for each property that matches the condition generate the partial view and assign it to the property. Then return your data array with one of the properties in each row being a chunk of HTML. As I said, I don't have experience with this language so it's hard for me to provide a code example, but I hope you understand what I'm trying to say. Then in DataTables you just need to output the value
columns: [
{ data: 'someField' }
]
THe second option is to generate the HTML on the client using JavaScript. Since you say that it could be complex it's best if you have a function that returns a HTML string. If it's a large amount of HTML then you could even put this function in a separate file and export it, to make it more manageable. There are a couple of typos in your example, so I'm going to fix them here. Renderer is a table property, the one you want is columns.render. Also in the render function the data argument references the data property that is defined in the line above. If you want to reference a different property, use the row argument.
columns: [
{
data: 'someField',
render: (data, type, row) => renderMyData(data, row)
}
]
function renderMyData(data, row) {
if (row.someId == "someValue") {
return "<div...> some label with somValue</div>"
} else {
return "<div...> some label without value</div>"
}
}
I've tried to prepare data from an OData source to show it in a bar graph in my fiori app. For this, I setup the OData model in the manifest.json. A test with a list, simply using
items="{path : 'modelname>/dataset'}
works fine and shows the content.
To prepare data for a diagram (VizFrame), I used the onInit() function in the controller of the view (mvc:XMLView). The data preparation is similar to the one discussed in question.
At first I obtain the ODataModel:
var oODataModel = this.getOwnerComponent().getModel("modelname");
Next I do the binding:
var oBindings = oODataModel.bindList("/dataset");
Unfortunately, the oBindings().getContexts() array is always empty, and also oBindings.getLength() is zero. As a consequence, the VizFrame shows only "No Data".
May it be that the data model is not fully loaded during the onInit() function, or do I misunderstand the way to access data?
Thanks in advance
Update
I temporary solved the problem by using the automatically created bind from the view displaying the data as list. I grep the "dataReceived" event from the binding getView().byId("myList").getBindings("items") and do my calculation there. The model for the diagram (since it is used in a different view) is created in the Component.js, and registered in the Core sap.ui.getCore().setModel("graphModel").
I think this solution is dirty, because the graph data depends on the list data from a different view, which causes problems, e.g. when you use a growing list (because the data in the binding gets updated and a different range is selected from the odata model).
Any suggestions, how I can get the odata model entries without depending on a different list?
The following image outlines the lifecycle of your UI5 application.
Important are the steps which are highlighted with a red circle. Basically, in your onInit you don't have full access to your model via this.getView().getModel().
That's probably why you tried using this.getOwnerComponent().getModel(). This gives you access to the model, but it's not bound to the view yet so you don't get any contexts.
Similarly metadataLoaded() returns a Promise that is fullfilled a little too early: Right after the metadata has been loaded, which might be before any view binding has been done.
What I usually do is
use onBeforeRendering
This is the lifecycle hook that gets called right after onInit. The view and its models exist, but they are not yet shown to the user. Good possibility to do stuff with your model.
use onRouteMatched
This is not really a lifecycle hook but an event handler which can be bound to the router object of your app. Since you define the event handler in your onInit it will be called later (but not too late) and you can then do your desired stuff. This obviously works only if you've set up routing.
You'll have to wait until the models metadata has been loaded. Try this:
onInit: function() {
var oBindings;
var oODataModel = this.getComponent().getModel("modelname");
oODataModel.metadataLoaded().then(function() {
oBindings = oODataModel.bindList("/dataset");
}.bind(this));
},
May it be that the data model is not fully loaded during the onInit()
function, or do I misunderstand the way to access data?
You could test if your model is fully loaded by console log it before you do the list binding
console.log(oODataModel);
var oBindings = oODataModel.bindList("/dataset");
If your model contains no data, then that's the problem.
My basic misunderstanding was to force the use of the bindings. This seems to work only with UI elements, which organize the data handling. I switched to
oODataModel.read("/dataset", {success: function(oEvent) {
// do all my calculations on the oEvent.results array
// write result into graphModel
}
});
This whole calculation is in a function attached to the requestSent event of the graphModel, which is set as model for the VizFrame in the onBeforeRendering part of the view/controller.
Since I'm not totally sure on which level my issue actually is to be solved best, I'd like to summarise the path I went and the things I tried first:
It's more or less about $el (I think).
As most basic backbone examples state, I started with having the $el defined within its view, like
Invoice.InvoiceView = Backbone.View.extend({
el: $('#container'),
template: ..,
..
});
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
Following this article's advice, I switched to passing $el over to the view while calling the render()-method. Example:
$('#container').html( new WineListView({model: app.wineList}).render().el );
So far so good - but now render() gets called, while it maybe shouldn't (yet).
For example the View asynchronously fetches a model in its initialize()-routine. Adding a binding to reset or sync (e.g. like this.model.bind('sync', this.render, this)) makes sure, render() gets definitely called once the model is fetched, however above stated way, render() still might get called while the model isn't fetched yet.
Not nice, but working(TM), I solved that by checking for the model's existence of its primary key:
render: function() {
if(this.model.get('id')) {
...
}
However, what I didn't expect - and if it really isn't documented (at least I didn't find anything about it) I think it really should be - the fetch operation doesn't seem to be atomic. While the primary key ('id') might be already part of the model, the rest might not, yet. So there's no guarantee the model is fetched completely that way. But that whole checking seemed wrong anyway, so I did some research and got pointed to the deferred.done-callback which sounded exactly what I was looking for, so my code morphed into this:
render: render() {
var self = this;
this.model.deferred.done(function() {
self.model.get('..')
};
return this;
}
..
$('#container').html( new WineListView({model: app.wineList}).render().el);
It works! Nice, hu? Ehrm.. not really. It might be nice from the runtime-flow's point of view, but that code is quite cumbersome (to put it mildly..). But I'd even bite that bullet, if there wouldn't be that little, tiny detail, that this code sets (=replaces) the view instantly, but populates it later (due to the deferred).
Imagine you have two (full-page) views, a show and an edit one - and you'd like to instantly switch between the two (e.g. after hitting save in the edit-view it morphs into the show-view. But using above code it sets (=resets) the view immediately and then renders its content, once the deferred fires (as in, once fetching the model is completed).
This could be a short flickering or a long blank transition page. Either way, not cool.
So, I guess my question is: How to implement views, which don't know about their container, involve models which need to be fetched, views which should be rendered on demand (but only once the model is fetched completely), having no need to accept UI/UX trade-offs and - the cherry on the cake - having maintainable code in the end.
First of all, the first method you found is terrible (hard coding selector in view's constructor)
The second: new WineListView({model: app.wineList}).render().el is very common and ok. This requires you to return the reference to view from render method, and everyone seems to follow this, which is unnecessary.
The best method (imo) is to simply attach the views element to the container, like this
$('#container').html(new WineListView({model: app.wineList}).el);
The WineListView doesn't need to know about where it's going to be used, and whatever is initializing WineListView doesn't need to worry about when to render the WineListView view instance:
because the el is a live reference to an HTML Element, the view instance can modify it anytime it wants to, and the changes will reflect wherever it is attached in DOM/ when it gets attached in DOM.
For example,
WineListView = Backbone.View.extend({
initialize: function(){
this.render(); // maybe call it here
this.model.fetch({
success: _.bind(this,function(){
this.render(); // or maybe here
})
});
}
});
Regarding flickering: this hardly has to do anything with rendering or backbone, it's just that you're replacing one element with another and there will be an emptiness for a tiny bit of time even if your new view renders instantly. You should handle this using general techniques like transitions, loaders etc, or avoid having to switch elements (For example convert labels into inputs in the same view, without switching view)
First off, the linked example is outdated. It's using version 0.9.2,
whereas the current version (2016-04-19) is 1.3.3. I recommend
you have look at the change log and note the differences, there are many.
Using the el property is fine. Like everything though, there's a time and place.
It didn't feel right, that the view is supposed to know about its parent (=container). The paragraph 'Decouple Views from other DOM elements' written on http://coenraets.org/blog/2012/01/backbone-js-lessons-learned-and-improved-sample-app/) perfectly puts it into words.
I wouldn't define an el property on every view, but sometimes it makes sense, such as your example. Which is why, I'm assuming, Backbone allows the use of the el property. If you know container is already in the DOM, why not use it?
You have a few options:
The approach outlined in my original answer, a work-around.
fetch the model, and in the success callback, insert the view element into the DOM:
model.fetch({
success:function() {
$('#container').html(new View({model:model}).render().el);
}
});
Another work-around.
Define an el property on the view and fetch the model in the view initialize function. The new content will be rendered in the container element (also the view), when the content/model data is ready, by ready, I mean when the model has finished fetching from the server.
In short,
If you don't want to define an el property, go with number 1.
If you don't want to let the view fetch the model, go with number 2.
If you want to use the el property, go with number 3.
So, I guess my question is: How to implement views, which don't know about their container
In your example, I would use the el property, it's simple a solution with the least amount of code. Not using the el property here, turns into hacky work-arounds that involve more code (complexity) without adding any value (power).
Here's what the code looks like using el:
var Model = Backbone.Model.extend({url:'/model_url'});
var model = new Model();
// set-up a view
var View = Backbone.View.extend({
el:'#container',
template:'model_template',
initialize:function() {
this.model.fetch();
this.listenTo(this.model,'sync',this.render);
},
render:function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
}
});
var view = new View({model:model});
Check out the documentation for el.
Here is an updated working example.
If there is an obvious flicker because, your model takes a noticeable amount of time
to be fetched from the server...maybe you should think about displaying a loading bar/variation thereof
while fetching the model. Otherwise instead of seeing the flicker, it will appear the
application is slow, delayed, or hanging..but in reality - it's waiting to render the next view,
waiting for the model to finish fetching from the server. Sitting on old content, just waiting for
the model to load new data to show new content.
See below.
https://jsfiddle.net/z30y983n/1/
First, the script gets 4 items from Github API and renders a list. If you submit 'NEXT' button, the script re-draws the list after re-sends Get request.
GET request.
repo.Repo.getList = function (api) {
return m.request({
method: "GET",
url: api,
type: repo.Repo,
extract: repo.linkHeader.setLinkHeader,
initialValue: []
})
.then(function (data) {
// bad solution.
return repo.vm.list(repo.vm.list().concat(data));
});
};
Concat Array.
repo.vm.api(links['next'])
return m('button', {onclick: repo.vm.add}, 'NEXT');
It works just as expected. But, It is bad solution, aren't you? I think
this concat process should be completed in View-Model (repo.vm).
Is there any good method? Or is this all right?
It's not ideal to modify things in the view method. Consider the view a template, that should only display state, since redraws can happen quite rapidly. Instead you should let the controller do the request, and modify state when the request is finished. Then the view will be displayed.
Code-wise, I think you're a bit deep into "too much structure". There are viewmodels, a LinkHeader prototype, and the program flow jumps all over the place. The mental model is quite simple, so keep it that way instead of getting into patterns that will only make things abstract and complicated.
Here's my take on it: https://jsfiddle.net/ciscoheat/akwdqhpx/
The parser is the same, but after that I've tried to keep the code compressed and local, so you can look at a part of the code and understand it. Here's a very good article about locality and cohesion. I've also changed a few names to keep closer to the mental model.
I have an application in ASP.MVC. The requirement is that I select a person from a list of people and click 'Info' and it should load the details of the person in that page. I have the Info controller and everything works fine if I go to the Info page from a different controller. In the page I am trying to make it work with JavaScript and it doesn't seem to take me to the desired page but to a different controller.
I have a ToDoList controller and in the .cshtml I have this code on click of the Info link.
function DoInfo#(i.ToString())() {
$("#sessionid").val("#Model.cSessionId[i]");
alert("hey");
$("#PageController").val(66);
$("#formID").submit();
}
I go to the ToDoList controller to do the redirection like this
if (viewModel.PageController == 66)
{
pass = new PassingData();
pass.personid = TSSessionService.ReadPersonId(viewModel.SessionId);
TempData["pass"] = pass;
return RedirectToAction("Index", "Info");
}
It never goes there and instead goes to a different controller. I cannot seem to find how they are linked and why is it not going back to controller where the Info link button is i.e. back to the ToDoList controller.
Let me know if it is not clear and I will try to explain again and I will give any other details.
I guess I'm confused as to why you are doing this as a combination of form and JavaScript. Are there other properties that you need to pass along that you are not posting above? Why do you need to use JavaScript to do this if you are just returning a new view?
You indicate in your post that when a person is selected from a list you need to go to a controller and display a view. This seems fairly straightforward, and I would like to suggest simplifying the problem.
Start with this: change your link to not use a form or JavaScript. Just make it a link. If it is text, you can use #Html.ActionLink() and even pass in the parameters you need.
If you're not displaying text, just use #Url.ActionLink() in your href property of the anchor you're wrapping your element with. Both of these allow you to leverage routing to ensure the correct path is being constructed.
If the controller that you are trying to get to has access to whatever TSSessionService is, then you don't need to pass through the TempData["pass"] you are trying to push through, so it makes it cleaner in that way as well.
If you do need to submit a more complicated value set, I would recommend coming up with a generic .click() event handler in jQuery that can respond to any of the clicks, bound by a common class name. You can use a data-val attribute in your link and read from $(this).attr('data-val') in your handler to store/fetch other important info. This allows you to more easily build up an object to POST to a controller.
Hope this helps some, but if I'm missing a critical point then please update the question above.