How to prevent views from being destroyed in Ember.js - ember.js

Quick note:
I don't believe this is a duplicate of Ember.js: Prevent destroying of views. Other related questions that I've found are out-of-date.
In case this becomes out-of-date later, I am using Ember 1.7.0 with Handlebars 1.3.0.
Context for the question:
As the title states, I am wondering how to transition between views without destroying them. Using queryParams does not solve my issue.
I am creating a calculator with the following nested views:
>>Calculator View
>>Report View (hasMany relationship to Calculator)
--School Partial (I am using queryParams here)
I am able to navigate between the Report views just fine without destroying my School partial, since I am using queryParams and using a displaySchoolPartial boolean to show/hide the partial. Example below:
Report template (stripped to only show the essential part):
<script type="text/x-handlebars" data-template-name="calculator/report">
...
{{#link-to "calculator.report" (query-parameters displaySchoolPartial="true")}}
{{render "_school"}}
</script>
School template (also stripped down):
<script type="text/x-handlebars" data-template-name="_school">
{{#with controllers.calculatorReport}}
<div {{bind-attr class=":schoolPartialWrapper displaySchoolPartial::hide-element"}}>
...
</div>
{{/with}}
</script>
This works as expected. Navigating between different Report views and School partials, as stated before, does not destroy the view.
The problem:
My problem comes when navigating to the Calculator view, the Report view is destroyed, which then destroys my School view. I do not want to also use queryParams to replace my Report views.
The reason I need to make sure the views aren't destroyed is because I have a select box with 3,000 schools in my School partial. It takes too long to re-render this. It would be a much better UX to simply show/hide the Report views.

Don't fight with Ember. You will lose.
Views are instantiated and rendered when needed and torn down when done.
Why do you have a 3000-element dropdown, anyway?
If you really, really want to do this, what I would suggest is putting a {{render}} on your application page, and hide it. The view will be created and rendered when the app comes up and persist as long as the app is alive. Then, in the didInsertElement of your view, do a cloneNode of that hidden element and insert it into the view's DOM somewhere. You may have to muck around getting event handlers wired up correctly.

My suggestion is not using "render" but using "partial", so you only need to drop in the template that you want. Have a control variable that set show/hide via css class. And control that variable using you controllers.
Using "partial" will allow you to have school template independent from report, thereby removing report will not affect school.
Just make sure you define the outlet and partial correctly.
Hope it helps!

Related

Best practice for dynamically rendering element outside of template

What is the best way of toggling the presence of some element (represented by a template) that is a direct child of <body>?
I'm talking about a modal box, notification, light-box etc. that is triggered either by some user event or by a route.
Examples: Dialog for newsletter sign-up that is shown after a user clicks a button. Overlay for content editing that is triggered by appending /edit to the item's route
Edit:
The first solution I though of was using Session to control state and then lining up all the application's modals and messages inside #if statements at the end of my main layout template like this:
<template name="layout">
<!-- yields and stuff -->
{{#if isActiveModal 'editArticle'}}{{> editArticle}}{{/if}}
{{#if ...
</template>
The problem is modularity; if a teammate is working inside some page template and needs to display a specific message or dialog, he or she has to edit the main layout to add it. I would have liked some easy way of conditionally appending a template to <body>.
Should be possible with the tools at hand, shouldn't it?
I actually use bootbox throughout several of my Meteor apps and it works great and does not interfere with any of Meteor's rendering. You can use the normal alert, confirm, prompt, as well as a custom dialog function, in a non-blocking way. There is a smart package available:
https://github.com/TimHeckel/meteor-bootboxjs
See some of my apps and smart packages for examples (grep for bootbox.):
https://github.com/mizzao/CrowdMapper/
https://github.com/HarvardEconCS/turkserver-meteor/
If you use the latest version of the new template engine, you can work with the body tag as its own special template through UI.body:
meteor update --release template-engine-preview-10.1
HTML:
<body>
{{#with alert}}
{{> modal}}
{{/with}}
</body>
<template name="modal">
<div class="modal">
X
<p>{{message}}</p>
</div>
</template>
JS:
if (Meteor.isClient) {
UI.body.alert = function() {
return Session.get("modal-alert");
};
UI.body.events({
"click .close": function() {
Session.set("modal-alert", null);
}
});
}
Since I posted the question, Blaze has become more sophisticated. Elements can now be rendered anywhere in the DOM while keeping a logical view hierarchy. See this discussion:
https://forums.meteor.com/t/most-useful-meteor-apis/1560/8
I've inserted the main outline below – the full thread also has code examples.
...
[Use] Blaze.render to output a template anywhere in the DOM – such as in a dimmable wrapper directly inside <body> – while maintaining a parent-child relationship between the view of the opening template and the view of the modal. This relationship is required for them to communicate directly.
(Bear in mind that a Session variable, among other limitations, can only hold serializable data...)
With the hierarchy intact, it's possible to set up shared reactivity between the modal and the background and call methods on the other, too. If we volunteered to use Blaze.renderWithData, then the two templates can even share the same data context!
Manually traversing the view hierarchy is pretty tedious, but with the parent() method from aldeed:template-extension, it isn't an issue.
Depending on whether one likes to use template instances as a 'view-model' or prefers to pass stuff around, reciprocal reactivity between the two templates can either be mediated by a new ReactiveVar assigned to a property on the parent template instance or view, or by a more retrieve-on-demand TemplateVar.
The entire exercise with Blaze.render can be duly incapsulated by using tricks like block helper templates and keyword arguments, so that everything is kept declarative.

Ember JS - Reference view inside linkTo

I have an Ember JS confusion. First of all I have to admit I'm a bit of an Ember newbie so apologies if this is obvious.
I have the following template:
<script type="text/x-handlebars" data-template-name="threadListItem">
{{#linkTo show thread.receiverLogin}}
<h4>{{thread.receiver.login}}</h4>
<span>{{view.prettyTime}}</span>
{{/linkTo}}
</script>
There's basically a linkTo helper with some elements inside of it.
The weird thing is the {{view.prettyTime}} doesn't display anything when it's inside the linkTo.
However, if I move the span outside the linkTo it finds the view variable ok and it works.
Is there anyway to get access to the view object inside the linkTo?
Christopher is correct that you can access the prettyTime property with:
{{view.parentView.prettyTime}}
Since you're new to ember, it's maybe worth mentioning that in most cases it is best to avoid accessing properties of the view. Instead, consider adding a prettyTime property to your controller. The controller will be the default context for sub-views like the one created by the linkTo helper, so you'll be able to access it with just
{{prettyTime}}
linkTo creates its own view, so the view you want to get access to is the parentView of view.
{{view.parentView.prettyTime}}

Updating backed array in Ember.js rc1 does not propogate to view

I am trying to upgrade a partially built UI to the latest Ember.js rc1 and it has turned into a very big rewrite job thanks to the dramatically changed API. Most info out there (and here) has been rendered useless. I've had to go through the documentation again several times to get things partially working but there are a lot of loose ends. Here is a biggie. The views do not update like they did under the previous version. I'm missing something that must have to do with rerender, {{outlet}} or something else that I'm not aware of. The ember guides seem to need updates.
The template is very simple:
<script type="text/x-handlebars" data-template-name="index">
<button {{action "addOne"}}>add one</button>
<ul>
{{#each item in controller}}
<li>{{item.title}}</li>
{{/each}}
</ul>
</script>
When clicked, the button adds a new element to the backed array. The console logs show that the array is growing, but the template does not change. Here is a jsfiddle to illustrate how far I've gotten. Can anyone figure out what needs to be added?
I modified your example to highlight the fact when we use arrays in Ember, that we are using Ember arrays (Ember.A() or Em.A() if you want to make explicit this fact). From my understanding, you can use the methods Em.A().addObject and Em.A().removeObject to achieve the basic functionality using the Ember.Object getter and setter methods, (i.e. .get() & .set()) .
In order be properly observed by the Ember application, it is important to use the Ember getters and setters.
A modified version of your fiddle.

How can we load multiple instances into a template?

We're trying to put together a portal, where a layout can have any number of core widgets in any sequence in the main layout.
To simulate this, we've got a number of outlets:
<h1>{{title}}</h1>
{{outlet pos1}}
{{outlet pos2}}
{{outlet pos3}}
{{outlet pos4}}
{{outlet pos5}}
{{outlet pos6}}
{{outlet pos7}}
{{outlet pos8}}
{{outlet pos9}}
{{outlet pos10}}
And in the router, we're attempting to load them in one by one:
connectOutlets: function(router, group) {
router.get('applicationController').connectOutlet('group', group);
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
However, when there are more than one, the final context is used. So in this example, we get two instances of the toDo object, both of which are for id #15.
Am I approaching this in the right way and is it possible to do this programatically, rather than having a fixed layout of outlets?
Thanks,
Dan
Edit: My answer is based on the assumption that this complex solution is really needed in your case. Based on your simple example one could also say, that you could use an ArrayController for all your ToDo items. But here is my try on the answer to the complex problem:
the problem are the following 2 lines:
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
What you basically do there is:
Connect the outlet with name pos9 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 41.
Connect the outlet with name pos3 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 15 (so you are overriding the content of the same controller).
The result is that you end up with both outlets connected to the same instance of a controller. And you the same ToDos since you have set the content property of this single instance twice. The core problem is from my point of view: EmberJS uses single instances of controllers by default.
So my solution approach would be to instantiate a new Controller for each outlet you have. Unfortunately this also requires modification to the lookup of the View. As you likely know, Controller and View are matched by name. So roughly the algorithm would be in pseudocode:
Create new instance of Controller, e.g.: var newController = App.ToDoController.create();
Inject this controller into the router with the appropriate name, e.g. router.set('todoControllerForPos9', newController);
Based on this name, you must enable Ember to find the matching view, e.g. App.set('TodoControllerForPos9View', App.ToDoView);
Finally call connectOutlet on the router, e.g.: router.get('groupController').connectOutlet('pos9', 'todoControllerForPos9', App.ToDo.find(41));
As you might guess, i ran into this problem myself. I did ask this question before and this is the solution, i came up with. I think, this is a missing feature i ember. I call it dynamic outlet names. See my original question here: How to implement a list of multiple elements, where an element can be expanded by click? (Dynamic Outlet Names?)
Have a look at the Update section and the fiddle provided there and you will recognize my pseudo code provided here.
It would be still great, if someone could have a look at my solution, because it is still hacky at the moment, but seems valueable to me. Hope this will gain some attention now with this big answer :-)

Ember.js mark active menu binding misunderstood

I was trying to fully understand the previous answer of the question emberjs - how to mark active menu item using router infrastructure
In the answer I tried to simplify (without using a outlet navigation and without adding a second level routing under the root) but in my case the active link doesn't work ://
Here's a fiddle :
http://jsfiddle.net/archange/Scqxw/
What I don't understand in the fiddle is why the "isActive: function()" is not updated after the set on the router... :/
So if someone passing by can explain me the reason. Big Thanks.
And if someone has another solution for handling navigation menu (perhaps using bindAttr ?)
Thanks
What is happening here is that when a template/view creates another view, it gives the newly created view access to it's controller. For example:
<script type="text/x-handlebars" data-template-name="application">
{{view App.NavigationView}}
</script>
The controller of the navigation view is going to be the instance applicationController. This is expected behavior.
If you want to tell your newly created NavigationView to use another controller, just pass that in, like so:
{{view App.NavigationView controllerBinding="navigationController"}}
Please note that I wired these two controllers together in App.ready.
Here is your edited fiddle: http://jsfiddle.net/Scqxw/5/