The Problem
I'm wondering if there's a neat, performant way to bind an angular view to the existence of a particular item in an array. Basically, I have two controllers on one page. Controller A can delete or add items in an array that's injected by a service. I want Controller B, that's injected with the same service, to update its view when I do deletes or adds from Controller A.
The Problem With Lobsters
Say I'm creating a lobster dating site. I'd have two views, side by side:
The hottest lobster view shows a list of the hottest lobsters in the ocean. If any of these lobsters happens to be your friend, its list item will be highlighted and have a message saying that you're friends.
The lobster friends view is on the same page as the hottest lobster view. If I unfriend a lobster from the lobster friend view, (i.e. remove the lobster from the lobsterFriend array), the hottest lobster view should update accordingly, and stop highlighting the unfriended lobster.
I'd like a solution that will work with large numbers of lobsters.
The Setup
Disclaimer: Code in this question is just for illustration purposes. I'm not actually creating a dating site for lobsters.
I have an angular service that I'm injecting into two controllers. The service returns an array of objects.
lobsterFriendService, a service for managing lobster friends:
angular.module('lobsterDating')
.factory('lobsterFriendService', function($http) {
return {
// An array of lobsters
lobsterFriends: $http.get('lobsterApi/lobsterFriends/'),
addLobsterFriend: function (lobster) {
this.lobsterFriends.push(lobster);
$http.post('lobsterApi/lobsterFriends/', lobster);
},
deleteLobsterFriend: function (crustaceanId) {
this.lobsterFriends = this.lobsterFriends.filter(function (lobster) { return lobster.id !== crustaceanId; });
$http.delete('lobsterApi/lobsterFriends/', crustaceanId);
}
}
});
LobsterFriendsCtrl, a controller for the friends list, injected with lobsterFriendService:
angular.module('lobsterDating')
.controller('LobsterFriendsCtrl', ["lobsterFriendService", function(lobsterFriendService) {
$scope.removeFriend = function (lobsterId) {
lobsterFriendService.deleteLobsterFriend(lobsterId);
}
}]);
HottestLobsterCtrl, a controller for the hottest lobster page:
angular.module('lobsterDating')
.controller('HottestLobstersCtrl', ["lobsterFriendService", "hottestLobsters" function(lobsterFriendService, hottestLobsters) {
$scope.model = {
hottestLobsters: hottestLobsters
};
$scope.lobsterIsFriend = function (lobsterId) {
return lobsterFriendService.lobsterFriends.contains(lobsterId);
}
$scope.addLobsterFriend = function (lobster) {
lobsterFriendService.addLobsterFriend(lobster);
}
}]);
hottestLobsters.html, the view bound to HottestLobsterCtrl:
<div ng-repeat="lobster in model.hottestLobsters">
<div class="lobsterFriend" ng-if="lobsterIsFriend(lobster.id)">{{lobster.name}} is your friend ;)</div>
<div class="twoClawsUp" ng-if="lobsterIsFriend(lobster.id)" ng-click="addLobsterFriend(lobster)">Two claws up for {{lobster.name}}!</div>
</div>
Possible Solutions
Set up a watch on the lobsterFriends array. When it updates, we can update some property on the HottestLobsterCtrl controller, trigger a digest, etc. I think this would be rather expensive.
Emit an event whenever a lobster is friended or unfriended. This is a path I'd want to avoid going down if possible, as I'm already injecting a service with the actual object right there.
Use some sort of helper library that handles computed properties nicely.
???? Please help?
What you want to do is save the data in a single obj/array and when you update, simply change the value of that object and array instead of keeping two arrays. You can think of this as a master array. The reason for this is because obj/array gets passed by reference thus it doesn't matter which controller they are in, it will be the same data. Plunker Example
service('dataHolder', function(){
this.data = [{
name : 'Joe',
isFriend: false
},
{
name : 'Michelle',
isFriend: false
},
{
name : 'Adam',
isFriend: true
},
{
name : 'West',
isFriend: false
}]
})
In your example, if you find a way to combine hottestLobsters and LobsterFriends in a single object through a merge and control them through properties then there is no need to have any watch or broadcast . It will simply be like sharing the same scope.
Related
What is the correct approach when working with an "new object" that is to be saved in a collection. Say I have a collection Cars. I have a /cars/new-car
url and then a form with:
name: __
parts: list of parts here
If I want to make this form "reactive" in the sense that if I add a new part in the parts array it shows a rerender is the best approach to make the whole "Car" a reactive object. Or should one just add a new row in the dom?
I dont want to automatically insert the whole thing into the "Cars" collection until It has a name and a list of parts.
Most examples shows very simple of adding to collection -> rerender of DOM which is very straightforward.
Edit: The same concept may apply to when editing a car. Fetching the car from a collection, setting up so the returned object is reactive(so I can add/remove parts) when done get all values and store the edited car information.
Start out by initializing an "empty" car as a reactive variable.
Template.cars.onCreated(function () {
this.car = new ReactiveVar({}); // empty car
});
Say your dom has some sort of attribute on each field describing which key it is:
<input data-key="name" placeholder="Car name"/>
Then you can bind an event that will use the data from this to update the reactive variable.
Template.cars.events({
'change input': function (e, template) {
template.car.set(_.extend(template.car.get(), {
[$(e.target).data('key')]: $(e.target).val()
}));
}
});
This will construct the object as you fill in your inputs.
Consider using Session for your /cars/new-car page
When the page first loads
Session.set('parts', []});
Session.set('name', '');
When the user saves a part
var addedPart = getPart();
var update = Session.get('parts').push(addedPart);
Session.set('parts', update);
Then your template helper functions can get everything it needs to render the view by calling Session.get().
Template.view.helpers({
currentParts: function() {
return Session.get('parts');
}
});
What do you think? I'm fairly new to Meteor myself, so there maybe even more clever ways to do batch updates on the session. But this is general gist.
I need some ideas on how to create a search filter in EmberJS where the search results can persist across views. Lets say I have a list of contacts and I filtered the list. Assume this route is called Contacts/Index
Now I have a second route called Contacts/Details. The user will be directed to this route once they select a result from the contact list. Now when they click 'Back To Contacts', I want the previous filter to be still applied instead of showing all the Contacts.
I didn't write any code yet, so I can't provide a JSFiddle. All I can think of now is probably to create a global variable to keep track of the text that is used to filter and apply that when transitioning back to the Contacts/Index view but I'm not sure if it is the right way to do it.
This is just pseudo-code that doesn't really care what your filter type is, but you could apply a filter property to the ContactsIndexController:
App.ContactsIndexController = Ember.ArrayController.extend({
//...
filter: 'name=bro',
filteredContent: function () {
if(this.get('filter')){
return this.get('content').filter//...;
} else {
return this.get('content');
}
}.property('filter')
//...//
});
Whenever you change the filter, make sure to update the filter property:
this.set('filter', 'foo=bar');
Then in your handlebars template you always loop over filteredContent:
{{#each filteredContent}}
{{/each}}
When you transition back and forth between the Contacts inner routes, it should retain the filter when you return to the index.
You can also see how this pattern could be used to take this one step further and manipulate the filter from literally anywhere in the application. If you aren't in that controller's context, you can still update that property and bindings will appropriately render the computed property next time you visit.
From another controller:
this.set('controllers.contacts-index.filter', 'year=20x6')
From a route:
this.controllerFor('contacts-index').set('filter', 'year=20x6')
From a view within the index controller:
this.set('controller.filter', 'year=20x6')
I'm sure you get the idea.
This is, of course, one of several approaches you could take. I prefer this particular pattern.
Hope that helps and good luck!
I am trying to work with Ember.js
Can I expose my data model as JSON through a route or controller?
I have an object like this saved in the store:
this.store.createRecord('Person', {
id: 1,
name: this.get('name'),
email: this.get('email')
});
I want to expose this data from a route or controller as JSON object. I don't want to use any view.
Is it possible to do this?
Thanks for help!
EDIT
My route is:
App.ResultRoute = Ember.Route.extend({
model: function() {
return this.store.find('person', 1);
}
});
There is '1' because I want only this record.
In this way It works and I see in the view the {{name}} and the {{email} of the Person object.
I want to see only the JSON, I tried to do how you suggest me :
App.ResultRoute = Ember.Route.extend({
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
});
But I receive this error:
Uncaught Error: Assertion Failed: Error: More context objects were passed than there are dynamic segments for the route: error
What is my error?
The way I would do this would be, I would have an api in my model which would return a plain json object to whoever asked it. So the Person model would have a getPersonDetails method which will hide all the internal details, including the attributes and associations and whatever else, and return the state of the person object it is invoked upon.
So, for example, if you wanted to display a table of persons or something, you would do a createRecord, and just ask the newly created person object for it's details.
Start from the beginning of this guide. http://emberjs.com/guides/routing/specifying-a-routes-model/ It will show you how to specify a model for a route.
Then, read this entire guide on controllers: http://emberjs.com/guides/controllers/
In general, you would access that data from the route's model hook with:
this.store.find('person') // All records
If you wanted to access that first object as JSON, you could do:
var person_JSON = this.store.find('person').then(function (persons) {
//The persons records are now available so you can do whatever you want with them
console.log(persons.objectAt(0).get('content'));
});
You could also iterate over all records and strip out the content to produce raw json without the Ember wrapping... Just depends on what you need to really do.
Really the best place to put this would be the route's afterModel hook, though. You wouldn't be working with a promise, as Ember would have dealt with that for you:
afterModel: function (model) {
model.get('content').forEach(function (item) {
console.log(item.get('content'));
});
}
Hope that helps.
Edit: Since you have one record try this:
afterModel: function (model) {
console.log(model.get('content'));
}
I have a route that has two models associated with it as shown below:
App.IndexRoute = Ember.Route.extend({
model: function() {
return Ember.RSVP.hash({
sites: this.store.find('site'),
songs: this.store.find('song')
})
},
Now later on, I need to be able to retrieve the first object in the sites model in order to do a transition I'll show below. I figured I can set the models using setupController, but when dealing with multiple models as depicated above, I'm not sure how to fill this part in:
setupController: function(controller, ???) {
controller.set('model1', ???);
controller.set('model2', ???);
}
And finally, I'd like to be able to retrieve the first object in model1 (it's multiple instances of site as described above)
afterModel: function() {
firstRecord = this.('sites').objectAt(0);
this.transitionTo('site', firstRecord.id);
}
It's also possible that I'm not designing my application properly. sites in this case is a component I built that displays different sites within a few different controllers. The controllers are dependent on this component in that they need to know which site is selected in order to do their own thing. So in controllers that need access to the component, I do something like:
{{site-nav sites=sites}}
Where site-nav is my component. It needs its own model, as does the controller itself.
First, you're going to need to modify your model hook slightly, to make sure you stay in the right scope:
model:function(){
var self = this;
return Ember.RSVP.hash({
sites: self.store.find('site'),
songs: self.store.find('song')
})
}
To get the different models in setupController, you just access it from the second parameters, like this:
setupController:function(controller,models) {
controller.set('sites',models.sites);
controller.set('songs',models.songs);
}
afterModel provides two parameters, this first being the resolved model for your route, so you'd do it something like this:
afterModel:function(models){
var site = models.sites.get('firstObject');
this.transitionTo('site',site);
}
Correct me if I'm wrong, but I thought Ember should most of the model - view binding for you?
What would be the case when you have to manually track model changes and update/refresh the view accordingly?
The app I'm working have nested routes and models associated with them.
App.Router.map(function() {
this.resource('exams', {path: "/exams"}, function() {
this.resource('exam', {path: ":exam_id"}, function(){
this.resource('questions', {path: "/questions"}, function() {
this.route("question", {path: ":question_id" });
this.route("new");
})
})
});
});
Everything works fine and I'm able to get exams and questions separately from the rest server.
For each model I have appropriate Ember.ArrayController and Ember.ObjectController to deal with list and single model items in the view. Basically for both models the way I handle things is IDENTICAL except for the fact that one is nested within the other. One more difference is that to display the nested route data I'm using another {{outlet}} - the one that is inside the first template.
Now the problem is that the top level model binding to the views is handled automatically by Ember without any special observers, bindings etc.. - e.g. When I add new item it is saved and the list view is refreshed to reflect the change or when the item is deleted it is auto removed from the view. "It just works (c)"
For second model (question), on the other hand, I'm able to reproduce all the crud behaviour and it works fine, but the UI is not updated automatically to reflect the changes.
For instance I had to something like this when adding new entry (the line in question has a comment):
App.QuestionsController = Ember.ArrayController.extend({
needs: ['exam'],
actions: {
create: function () {
var exam_id = this.get('controllers.exam.id')
var title = this.get('newQuestion');
if (!title.trim()) { return; }
var item = this.store.createRecord('question', {
title: title,
exam_id: exam_id
});
item.save();
this.set('newQuestion', '');
this.get('content').pushObject(item); // <-- this somehow important to update the UI
}
}
});
Whereas it was handled for me for (exam model)
What am I doing wrong? How do I get Ember.js to track and bind model and change the UI for me?
With
this.get('content').pushObject(item);
you push your new question to questions controller array. I think it would be better if you push the new question directly to the exam has_many relation.
exam = this.modelFor('exam');
exam.get('questions').pushObject(item);