access to ng-included DOM nodes? - javascript

My first experiment with angular.js.
I've a few columns, each of them includes some template:
<div class="col-md-5 js-column" ng-repeat="orm in orms" repeat-done="equalHeight">
<h2>{{ orm.name }}</h2>
<ng-include src="'/inc/_compiled/'+orm.id+'.html'"></ng-include>
</div>
Each included template contains the same elements as other templates, but they've different height. Example element:
<pre data-task="model" class="task-model">
from django.db import models
class Teacher(models.Model):
first_name = models.CharField(max_length=30)
last_name = models.CharField(max_length=30)
</pre>
The thing I want to achieve is to set equal height for specific element in all the columns. That means all pre.task-model will have the same height.
I've created some directive and the idea was to trigger it after ngRepeat loop is finished (scope.$last). But when I try to access the included nodes via jQuery/DOM selectors, I get nothing. I know each template is available in element variable, but I need to get also the other columns.
var ormApp = angular
.module('ormApp', [])
.directive('repeatDone', function () {
return function (scope, element, attrs) {
if (scope.$last) {
$('.js-column pre.task-model'); //<- got nothing
}
};
})

As mentioned in the comments, using $timeout solves the problem.
But why ?
The issue here is that several operations involved in the process, such as creating a new element (by ngRepeat), fetching the template (by ngInclude) using $http.get() (even if it comes from the $templateCache), resolving the promises returned etc, are asynchronously and are handled using $evalAsync(), which means that they are going to "happen" when everything else currently on Angular's async queue is processed.
Since there are several levels of nested $evalAsyncs (in this particular case 7), you can achieve what you want by iteratively calling $evalAsync() until the async operations required for fetching and lining the template are completed.
This is of course not a robust (or recommended) way to solve the problem, it is just meant to explain what is going on.
Using $timeout puts the operation in the browser's event queue, so the command will be processed after Angular's current $digest loop is completed (i.e. after all tasks on Angular's async queue have been processed and all taks added to the queue by those async taks etc).
(Another side-effect here is that, since the rendering engine's "render" command is already on the browser event queue, using $timeout will execute the command after the next DOM rendering has taken place. This has no relevance for this particular case, but might be important in other situations.)
See, also, this short demo illustrating both aproaches (open the DevTools console).

Related

RxJs 6 load more pagination stream

I'm trying to build one of those more modern paginations where there's not dedicated links for the individual pages but one where more results are loaded automatically when the user scrolls to the bottom. On the web page are multiple widgets that allow you to modify the search parameters. When the parameters change more results should be fetched via ajax beginning from the first page again.
I'm fairly new to RxJs and I'm having issues wrapping my head arround how to identify the observables/subjects I need and how to compose them to achive the described behavior.
Here's the specific flow I have in mind:
When the page is first loaded an initial set of parameters is taken and used to load the first page. When a "load more" event is fired the next page should be fetched and rendered to the page.
When the parameters change the page should be loaded starting from page 1 again.
When the server signals that there are no more results to load I should get notified about that via an observable. If further "load more" events are fired after no more pages are available the ajax request should not be made to save bandwidth on mobile devices.
Lastly as long as a network request is open i want to be able to display a loader so i need an observable that informs me about whether there are open requests or not.
As a bonus: Currently I've implemented signaling no more results by returning a 404 from the backend when a page one bigger than last page is requested. I'd like to use catchError on the ajax observable in such a way, that it gracefully stops the ajax request without breaking the subscription.
Here's what I was able to come up with so far, but it has multiple Problems (described below):
import { BehaviorSubject, Subject, fromEvent } from 'rxjs';
import { map, mergeMap, switchMap, takeUntil, tap } from 'rxjs/operators';
import { ajax } from 'rxjs/ajax';
import { stringify } from 'qs';
const paramsEl = document.querySelector<HTMLTextAreaElement>('#params');
const paramsChangedBtn = document.querySelector<HTMLButtonElement>('#paramsSubmit');
const loadNextPageBtn = document.querySelector<HTMLButtonElement>('#loadNextPage');
const getParams = () => JSON.parse(paramsEl.value);
const params$ = new BehaviorSubject(getParams());
const page$ = new BehaviorSubject(1);
const noMoreResults$ = new Subject<void>(); // <- public
const connections$ = new BehaviorSubject(0);
const loading$ = new BehaviorSubject(false); // <- public
// for the sake of this example we're not using an IntersectionObserver etc. but a plain button to fire a "load more" event
const loadNextPage$ = fromEvent(loadNextPageBtn, 'click');
// same for params changed event. In my real app I've got a working stream fed from the widgets
fromEvent(paramsChangedBtn, 'click').subscribe(e => params$.next(getParams()));
// when the params change, reset page to 1
params$.subscribe(() => page$.next(1));
// update loading$ observable for displaying/hiding a loader
connections$.subscribe(connections => {
if(connections > 0 && loading$.getValue() === false) loading$.next(true);
if(connections <= 0 && loading$.getValue() === true) loading$.next(false);
});
// when we need to load the next page, increment the page observable
loadNextPage$.subscribe(e => page$.next(page$.getValue() + 1));
//////////////
// whenever a new page should be requested, get the current parameters and fetch data for this page
page$
.pipe(
takeUntil(noMoreResults$),
tap(() => connections$.next(connections$.getValue() + 1)),
mergeMap(page => {
const qs = stringify({
...params$.getValue(),
page,
});
return ajax.getJSON<any>(`https://httpbin.org/get?${qs}`)
// this doesn't seem to do anything
// furthermore the ajax request would already have been made at this point
// .pipe(
// takeUntil(noMoreResults$)
// );
}),
tap(() => connections$.next(connections$.getValue() - 1)),
)
.subscribe(data => {
console.log(data.args);
// for testing purposes pretend we have no more data at page 5
if(data.args.page === "5") noMoreResults$.next();
});
// for debugging purposes
loading$.subscribe(loading => console.log('loading: ', loading));
noMoreResults$.subscribe(() => console.warn('no more results'));
You can find the running version of this here on stackblitz.
Here's the issues with the code:
Current pace of takeUntil(noMoreResults$) breaks the subscription when noMoreResults$ has been triggered and then params$ emits no further pages are loaded. (See comment in the code for the other location in the ajax pipe).
using params$.getValue() when mergeMapping to the ajax observable feels wrong, however I don't know how to pass down both the page number as well as the parameters in one stream properly.
In general I think I've overused Subjects / BehaviorSubjects quite a bit but I'm not sure. Can you either confirm or deny this?
The composition of the observables feel very messy and hard to follow. Is this based on what I'm trying to do or is there room for improvement for this problem?
Can you please provide a working example as well as elaborating on the biggest mistakes I've made.
I have been keeping this question open in my tabs ever since you created this question, wanting to help, but also, wanting to learn enough of the RxJS so I could create a solution myself.
I'm really sorry, but I haven't looked at your example, but instead, I created my own. I would have to ask you to please forgive me for the extremely large answer that I will provide here.
I was mainly driven by the excellent talk by Ben Lesh, one of the creators of modern RxJS which you can find here. I strongly suggest that you look at this video, even multiple times, to try to understand some of the stuff I used in my solution to this problem.
Like Ben, I also used Angular framework as the basis for this project. You can find my solution at GitHub. Also, just like Ben has been explaining couple of times in his talk some Angular specific stuff, I will try to do it here as well.
What I've got in my app are a simple FeedComponent and a FeedService. The service is being injected using Angular Dependency Injection to the FeedComponent.
Now, I've got an HTML bound to FeedComponent that looks like this:
<div class="loading" *ngIf="loading$ | async">
Loading...
</div>
<div class="filter">
<form #form="ngForm">
...
</form>
</div>
<div #articles class="articles">
<article *ngFor="let article of feed$ | async">
...
</article>
</div>
You can see that I've got three sections: a <div> responsible for displaying a message that the feed is being loaded; another <div> with filtering <form> that displays filtering options; and a third <div> responsible for displaying feed items as a list of <article>s.
Angular specific stuff here include *ngIf and *ngFor directives and async pipe (|). With a single sentence: *ngIf renders certain DOM element if condition stated in attribute value is truthy; *ngFor loops through an array of provided items and renders certain DOM element number of times of the array's length; pipe | transforms items so that items to the left are always input items to the pipe to the right, so does the async pipe do - it transforms an Observable (you can tell that it's an Observable by the $ sign suffix that I and many others are using) to transform items that come from the Observable to what the directive understands. async pipe is also explained in Ben's talk.
Let's get started: you can see that I'm using two Observables in my HTML template, and that's all you need. The one that will give you an array of FeedItems so you can display them on the page, and the other one that will emit boolean values when feed is being fetched from server. If you would not use Angular, but rather some other framework or library, or nothing at all, you could still have only these two streams. You would have to manually subscribe to them (and unsubscribe later, when not needed anymore) and when you'd get results, you should update the DOM accordingly. Angular and async pipe do all of this here for me.
These two are feed$ and loading$ Observable streams, respectively. Both of them are defined in the FeedComponent that is bound to this HTML, very simply, like this:
feed$ = this.feedService.feed$;
loading$ = this.feedService.loading$.pipe(delay(10));
As I said, feedService is injected to FeedComponent through FeedComponent's constructor using Angular DI:
constructor(private feedService: FeedService) {}
You would just have to create new FeedService object if you'd use your own JS framework/lib or no lib at all. I'm adding delay of 10 ms to feedService.loading$ stream because I'm getting some Angular error that I should not explain here. You may not need it at all if not using Angular.
Now, to be able to provide feed items (FeedItems[]) through feed$ stream, you need to listen to the two possible events: a scroll event that would fire when the user has scrolled enough to the bottom of the page and an event that happens when filter form input values change. These two events need to be combined to a single Observable that we will call filterSeed$ - it will emit values contained in the input elements of the used form.
The first event stream can be formed out of these two Observables:
scrollPercent$: Observable<number> = fromEvent(document, 'scroll')
.pipe(
map(() => {
const scrollTop = this.articles.nativeElement.getBoundingClientRect().top;
const docHeight = this.articles.nativeElement.getBoundingClientRect().height;
const winHeight = window.innerHeight;
const scroll = scrollTop / (winHeight - docHeight);
return Math.round(scroll * 100);
})
);
loadMore$: Observable<number> = this.scrollPercent$
.pipe(
filter(percent => percent >= 80),
take(1),
repeatWhen(() => this.feedLoadingStops$)
);
scrollPercent$ is an Observable that emits some numbers. They represent scroll percentage when document's scroll event fires (created using fromEvent). Whatever event it emits, I don't really care about it. I only care about when it emits, so I can map it to percentages using some simple math. this.articles.nativeElement is Angular specific, so if you need another example, please take a look at this Pen about how to achieve it with jQuery. The returning value of the map function is rounded scroll percentage.
loadMore$ is an Observable that fires events only when a user has scrolled enough so that new feed items should be loaded - it fires scroll percentage number, but we don't really care about that, you'll see that we're ignoring these numbers later. The threshold when this should happen is at or after 80% of the scroll. So, I'm using filter here to let only those items that are above the threshold (remember, I need loadMore$ to emit when this threshold is reached and passed). And I'm using take(1) here because I really only need one such item.
Just like Ben has had a problem when the whole stream only worked once, I was having it as well. Because take completes (effectively unsubscribes) from the source when it takes that one item, I need to resubscribe again to the same source, which is, all the way to the top, the stream created by fromEvent.
So, I need to start listening to the scroll events once again, but there's catch here: I don't want to start doing it immediately, but rather when the loading completes. So, I need to use repeatWhen instead of just repeat. repeatWhen takes a factory function that it calls when needed to get an Observable to subscribe to. It listens to the provided Observable (this.feedLoadingStops$) and resubscribes to the source when the this.feedLoadingStops$ emits.
The this.feedLoadingStops$ looks like this:
feedLoadingStops$ = this.loading$.pipe(map(v => !v), filter(v => v));
It inverts false values so they become true, and vice versa, so that emitted true value indicates that loading has stopped (remember, when loading$ emits false, it indicates that loading has stopped). It also filters just true values so that we only get emits when it stops loading.
But, you may wander why. Why did repeat work for Ben and not for me? Why did I have to resubscribe only when the loading stops. It's because we both used higher order mapping operators after events fired by fromEvent to flatten the HTTP requests later on. I will certainly come to that later, but what he used was the exhaustMap operator, and I used switchMap which would always switch to the latest emitted item by the source and subscribe to it. When loadMore$ is resubscribed again (using just repeat), it would start listening to the scroll events again, and since user would certainly continue scrolling more, loadMore$ would start emitting once again and the switchMap would continue to resubscribe to the provided HTTP request all the way until user wouldn't stop scrolling. Which is really not what we want - we don't want to create multiple, exactly the same HTTP requests to fetch a single resource just because user is doing something we're responsible to solve. exhaustMap is different so that it does exactly the opposite to swithcMap - it will wait for the first emitted item to finish (basically, it will exhaust) until it subscribes to the next.
That was the explanation of the first event stream that will help create filterSeed$ Observable. The other one is rather simple one. Angular provides such Observables on its own when it comes to forms. I used this.form.valueChanges and I was automatically subscribed to any form input element value changes. Since I only have a single <select> which I use to fake should I load the feed with all items or only items with text or only items with images, I would like to listen to when a user selects a different option to fire an event.
Since Angular provides this for me, you may want to create your own Observable that would emit changed form input values for you.
And finally, this is what filterSeed$ would look like:
filterSeed$: Observable<FeedFilter> = defer(() => merge(
this.loadMore$.pipe(map(() => this.form.value)),
this.form.valueChanges
));
Here, I want to merge two streams: the one that is created by listening to the loadMore$ events, and the other that is listening to the filter form value changes. And that is exactly what I want: I want to load new feed items only when a user has scrolled enough to the bottom or when it changed a filter. this.form.valueChanges already provides FeedFilter items, but this.loadMore$ does not. Remember, this.loadMore$ emits numbers which I sad I don't really care about, so I'm mapping them to this.form.value. This is yet again Angular specific, so you'd have to implement your own reading of the whole form input elements. Current this.form.value is always the same as the last emitted item from this.form.valueChanges.
The reason to use defer here is yet another Angular specific because by the time filterSeed$ is created, this.form is still undefined, so I need to wait until subscription happens to make sure this.form is available. And I will subscribe to this Observable in ngAfterViewInit() lifecycle hook which is called upon component's view creation. You may not need to use defer here if not using Angular. Now, when everything is ready, it's time to subscribe to this.filterSeed$ and do some data loading. Don't forget to unsubscribe when leaving component/page not to leak memory by not removing all of the event listeners created by fromEvent.
ngAfterViewInit(): void {
this.subscription = this.filterSeed$.subscribe(this.feedService.filter$);
}
ngOnDestroy(): void {
this.subscription.unsubscribe();
}
We now come to the second part of this answer which is FeedService. The FeedService is only a simple JS object that has some Observables and some state. I need this state in order to be able to work with multiple Observables - some might say this is not the true Rx way, but I found this to be easier for me to solve it this way. I'm also injecting Angular's HttpClient (as http variable) to the FeedService which is only an Angular wrapper to XHR. You could use RxJS ajax static creation method instead - both should behave the same.
The FeedService has got three public Observables and you may have seen all of them being used in FeedComponent: filter$, loading$ and feed$ Observables. The later two are used by FeedComponent to render some stuff to the DOM, while filter$ was used to feed it with filterSeed$. Basically, filter$ is just a Subject:
filter$ = new Subject<FeedFilter>();
And since Subjects are both Observables and Observers, I could use it as an Observer, so I passed it to subscribe method when I subscribed to filterSeed$ in FeedComponent. What this means is that filter$ Observer will subscribe to filterSeed$ and any call to next method (basically, any emission) from filterSeed$ will pass through to the filter$ Subject. This means that anyone else using filter$ will get the value emitted by filterSeed$.
And filter$ is used to create feed$ Observable. Here's what it looks like:
feed$: Observable<FeedItem[]> = this.filter$.pipe(
switchMap(filter => {
if (filter !== this.filter) {
this.filter = filter;
this.nextPage = 1;
this.shouldReset = true;
}
return this.getFeed$;
}),
scan((acc, value) => {
if (this.shouldReset) {
this.shouldReset = false;
return value;
}
return acc.concat(value);
}, [])
)
I'm using two operators here: switchMap and scan. I am also having some state that I keep in the class itself in variables this.filter, this.nextPage and this.shouldReset.
I already mentioned higher order mapping operators. I'm using switchMap here. And it is being used after events fired by filter$ Subject which is connected through subscribe method with the FeedComponent's filterSeed$. So, whenever a refresh event is fired (either by user scrolling enough or by user changing a filter in the filter form), I want to map it to getFeed$ Observable which is responsible for creating HTTP requests. The reason to choose switchMap over others (over exhaustMap which Ben used) is that I want to make sure that I always get the result from the latest filter used by the user. I.e. if the user sent a request with one filter and changed a filter in meantime while the first request is still loading, I want to cancel that request and switch to another HTTP request.
Since filter$ Subject is emitting FeedFilter objects, they are passed to switchMap's callback function as filter parameter. This is where I'm checking if filter is actually the same as the filter in FeedService (this.filter). If they are not the same, it means that the filter is changed, so I need to save the new filter to the FeedService's filter (this.filter = filter;). This also means that I have to reset page to page 1 (this.nextPage = 1;) and set this.shouldReset to true. Then I return getFeed$ to which switchMap internally subscribes.
All of these state holding variables are later used by either getFeed$ Observable or the next operator that comes after switchMap: scan. But, how does getFeed$ looks like. Here's how:
getFeed$: Observable<FeedItem[]> = defer(() => {
if (this.nextPage) {
this.loadingSubject$.next(true);
const url = appendQuery('/feed', { page: this.nextPage, feedFilter: this.filter.feedFilter });
return this.http.get<FakeFeedResponse>(url);
} else {
return NEVER;
}
}).pipe(
catchError(() => /* Potentially handle this.nextPage here */EMPTY),
tap(response => {
this.nextPage = response.nextPage;
this.loadingSubject$.next(false);
}),
map(response => response.items),
share()
);
I'm again using defer here. This is because I'm saving some state outside of these streams, so I want values from these state variables to be read when a subscription to getFeed$ is made, not when getFeed$ object is created.
In the defer's callback function body I'm checking if this.nextPage exists. The server returns null if there are no more items to load, so in that case, I'm returning NEVER which is an Observable that never emits. However, if there are items to load (when nextPage is a valid, truthy number), I'm returning an HTTP get request. nextPage is set to 1 by default (or is being reset to 1 in switchMap if filter is changed). I'm constructing url by appending nextPage and feedFilter as query string to '/feed' route.
Also, I'm using loadingSubject$ to emit true indicating that the loading has started. loadingSubject$ looks like this:
private loadingSubject$ = new BehaviorSubject(false);
loading$: Observable<boolean> = this.loadingSubject$.asObservable();
It is a BehaviorSubject with the default value of false. Values emitted by this Subject are offered to FeedComponent through loading$ Observable. When FeedComponent first subscribes to loading$ Observable, it will get false immediately.
The this.http.get<FakeFeedResponse>(url) request returned to defer is using Angular XHR wrapper. I'm injecting http to FeedService, but you should be able to use RxJS's ajax as I already mentioned. It emits objects of FakeFeedResponse type.
After this Observable is constructed using defer, I want to do some more stuff when it emits. First thing is to handle errors using catchError. If an error happens, I want to return an EMPTY Observable which just completes without emitting any item. I added comment here so that you may add some more error handling or handle (re)setting of nextPage or something.
After that, I'm saving nextPage from the response in tap and also emitting false to loadingSubject$ indicating that the loading has stopped. After tap, I'm using map to extract items from the response.
And then I'm shareing it. This is actually not really needed in my case. Why? Because there is only one subscriber to getFeed$ (which is switchMap) so there's really no need to share it across multiple subscribers, but it can stay here if it would ever need - actually, if there would ever exist another subscriber. I added it because Ben added it as well, but he has more than one subscriber to his getFeed$ Observable.
And that's all about getFeed$ which is being returned to switchMap in its callback method. In feed$ Observable, after switchMap, I'm using scan. Actually, scan is here just so that Angular's async pipe could receive already loaded items by concatenating new values to an already loaded ones (acc.concat(value)). I'm using this.shouldReset flag here so that I don't use concat when the filter is changed. If you would not use Angular, you would probably subscribe yourself to feed$ Observable and you would probably handle this case yourself instead of scan, so you wouldn't probably need to have scan here.
After all, I'm using Angular Interceptor feature to fake all of the server responses. Please take a look how.
And that's it. I'm really sorry for the very long answer, if you have questions, please open an Issue on GitHub. I really hope that this answer might help you shape your solution, which I didn't really look at, I'm sorry.
If you'd like to try this example, you can clone the project, run npm install and then ng serve which will compile the whole project and run a dev server so you can try this project on your own.

Reading OData contexts in onInit of controller

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.

Backbone views which don't know about their container, models to be fetched via AJAX, no UI/UX trade-offs and maintainable code

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.

backbone js - reduce calls to the server

Just wondering how people deal stopping multiple external server calls? I'm doing everything in the .complete of the fetch because otherwise when I try to call anything the fetch hasn't completed and nothing is populated in the collection.
I'm new to backbone so I'm probably missing a trick.. but is there a way to do a fetch and store that information somewhere so that you never have to fetch again, you just work off the collection as a variable? All of my information comes from an external site, so I don't want to be making lots of unnecessary external calls if I can. I'm not updating the server or anything, its all just read-only.
What do other people do for a similar set up? Am I missing something silly? Or am I set up badly for this? Here's what I have so far (work in progress)
Oh also: I'm doing the fetch in the router.. is that a bad idea?
http://jsfiddle.net/leapin_leprechaun/b8y6L0rf/
.complete(
//after the fetch has been completed
function(){
//create the initial buttons
//pull the unique leagues out
var uniqueLeagues = _.uniq(matches.pluck("league"));
//pull the unique leagues out
var uniqueDates = _.uniq(matches.pluck("matchDate"));
//pass to info to the relative functions to create buttons
getLeagues(uniqueLeagues);
getMatchDates(uniqueDates);
homeBtn();
fetched = true;
}
); //end complete
Thanks for your time!
This is an often recurring question but the answer is rather simple.
Perhaps I'll make some drawings today, if it helps.
I never took the time to learn UML properly, so forgive me for that.
1. The problem
What you currently have is this:
The problem however is that this isn't very dynamic.
If these 3 functions at the right would require to be executed from different ajax callback functions, they need to be added to any of these callbacks.
Imagine that you want to change the name of any of these 3 functions, it means that your code would break instantly, and you would need to update each of these callbacks.
Your question indicates that you feel that you want to avoid every function to perform the async call separately, which is indeed the case because this creates unnecessary overhead.
2. Event aggregation
The solutions is to implement an event driven approach, which works like this:
This pattern is also called pub/sub (or observer pattern) because there are objects that publish events (in this case on the left) and objects that subscribe (on the right).
With this pattern, you don't need to call every function explicitly after the ajax callback is finished; rather, the objects subscribe to certain events, and execute methods when the event gets triggered. This way you are always certain that the methods will be executed.
Note that when triggering an event, parameters can be passed as well, which allows you to access the collection from the subscribing objects.
3. Backbone implementation
Backbone promotes an event driven approach.
Setting up an event aggregator is simple and can be done as follows:
window.APP = {};
APP.vent = _.extend({}, Backbone.Events);
From the ajax callback, you just trigger an event (you give it any name you want, but by convention, a semi colon is used as a separator):
APP.vent.trigger("some:event", collection);
The three receiving objects subscribe to the event as follows:
APP.vent.on("some:event", function(collection){
console.log(collection.toJSON());
});
And that's basically all.
One thing to take into account is to make sure that when you subscribe to events using "on", you also need to un-subscribe by calling "off", if you no longer need the object.
How to handle that is all up to you in Backbone.js but here is one of options you can take
Creating a View which has body as its el and handle everything.(I usually use Coffee so This might has some syntax errors)
$( document ).ready(function() {
mainView = new MainView({el: "body"});
});
MainView = Backbone.View.extend({
initialize : function(){
this.prepareCollection();
},
prepareCollection : function(collection){
_checker = function(){
if (collection.length === _done) {
this.render();
}
};
_.bind(_checker,this);
collection.each(function(item){
item.fetch(
success : function(){
//you can also initialize router here.
_checker();
}
);
});
},
rener : function(){
//make instance of View whichever you want and you can use colleciton just like variable
}
})

KnockoutJS afterRender callback when all nested Components have been rendered?

I have a hierarchy of nested KnockoutJS Components using 3.2.0. It's working very well but I'm looking to execute some code once my entire hierarchy of components has been loaded and rendered. It's a rough equivalent of afterRender(), needed for the same common uses cases as afterRender.
I've tried a few approaches but no luck so far:
Added the following to the root template but it gets called before the nested components are loaded, so too early.
<!--ko template: {afterRender: onLoad.bind($data)} -->
Using the latest 3.3.0-alpha and specifying synchronous:true on all components. But I believe since I'm using AMD, the components are still 'loaded' asynchronously which mean that just because my root applyBindings() returns, doesn't mean that all components have been loaded and rendered.
Even tried building a collection of deferred objects that get resolved only when their corresponding components are loaded. This got overly complicated and still didn't work for reasons I won't go into.
Is there a way to get a callback called once a complete hierarchy of knockoutjs components have been loaded and rendered? Thanks!
I just came across these two threads so it seems others are looking for this as well. The key differentiator from the existing workarounds are they don't work with nested components.
https://github.com/knockout/knockout/issues/1533
https://github.com/knockout/knockout/issues/1475
I've written a knockout library that triggers an event when all components have been loaded and bound. It uses reference counting, similar to referencing counting used for garbage collection. I extensively use components in my project(s), including nesting many levels deep, and I can't live without knowing when everything is "ready to go". I haven't spend much time on documentation of usage, but the basics are there.
Git Hub wiki:
https://github.com/ericraider33/ko.component.loader/wiki
Fiddle:
https://jsfiddle.net/ericeschenbach/487hp5zf/embedded/result/
Usage HTML:
<div id="ko-div">
Status: <span data-bind="text: loading() ? 'Loading' : 'Done'"></span>
<br><br>
<test-panel></test-panel>
</div>
Usage JS:
var pageModel = {
loading: ko.observable(true),
completedCallback: function (childRef) {
pageModel.loading(false);
childRef.testValue(childRef.testValue()+1);
}
};
var tpRef = ko.componentLoader.ref.child({ completedCallback: pageModel.completedCallback});
var tpModel = {
attached: function(element) { return tpRef; },
testValue: ko.observable(5)
};
ko.components.register('test-panel', {
viewModel: function() { return tpModel; },
template: '<div data-bind="attached: true">Test Panel<br>From Code <span data-bind="text: testValue"></span></div>'
});
ko.componentLoader.setOptions({ verbose: true });
ko.applyBindings(pageModel, $('#ko-div')[0]);
Here is what worked for me. I did not try it in all possible variations such as mixing sync and async components, or using custom component loaders.
There is a method in KO 3.3.0 that all components loading goes through:
ko.components = { get: function(componentName, callback) { ...
the get method is invoked with a desired componentName and when component has been loaded - a callback is invoked.
So all you need to do is wrap ko.components.get and callback and increment pendingComponentsCount on each call, and decrement it after callback is executed. When count reaches zero it means that all components were loaded.
25 lines of JS code (using underscorejs).
You also need to handle a special case where ko.applyBindings did not encounter any components, in which it also means that all components (all zero of them) were loaded.
Again, not sure if this works in every situation, but it seems to be working in my case. I can think of few scenarios where this can easily break (for example if somebody would cache a reference to ko.components.get before you get to wrap it).
If you'r working with ko.components this might be of use:
1) Create a deferred object to keep track of each component loading
var statusX = $.Deferred()
var statusY = $.Deferred()
2) Inform knockout to tell you when the component is loaded and ready
ko.components.get('x-component', statusX.resolve) //Note: not calling resolve, but passing the function
ko.components.get('y-component', statusY.resolve)
3) Synch up both status deferreds
$.when(statusX.promise(), statusY.promise())
.done( function allComponentsLoaded(componentX, componentY){
//Both components are ready here
//Note the arguments from the function comes via
//ko->jquery deferred resolve
});

Categories