I find I'm trying to pick up learning Ember at a time of particular fluctuation. The recent "Road to 2.0" blog post has helped me clarify which direction to head, but I'm struggling to validate my approach to Ember at a high level.
I want to be sensitive to people's time. My full code is here for anyone interested in providing more specific feedback (would love), but I'm mostly interested in this high level feedback on my app's structuring and my utilization of Ember's capabilities.
App Background:
I'm working on a user-to-many chat to text SMS app. Visually, each user has multiple chat windows (a Conversation) open w/ messages (Message) specific to a Profile message history. The rails backed sends messages to the target Profile. This project is very much in development.
Key Questions:
What is the best way to associate a model with a component? I'm passing each conversation model to a conversation component. As my component logic becomes so tightly integrated to the view, tt seems like a component class is taking on too much heft outside of UI. I'm starting to add of logic around how UI bubbles up to the model, but wonder if there are better approaches.
Since I'm breaking away from the proxying behavior of Array controller, I find myself referencing my model collection via this.get('content') - is there a better way to deal with the collection of conversations?
Finally, to invoke actions in a component, I've read of using Ember.Evented mixin to trigger and observe events. I.e. when a user tries to open a chat window for a profile when that chat is already open, I'd want to flash the target chat window. Is this a good way to manage these interaction in context of "Road to 2.0"?
What about passing events from the controller to the Message subcomponents? Message subcomponents would be bound to each messages' statuses (success, fail, etc). I imagine i'd just bind some message display to a record's state and status attribute. Any way I could do it better?
I'm super open to feedback. Be harsh! :)
High level code:
(full code)
ChatApp.Router.map(function () {
this.resource('conversations', { path: '/' });
});
ChatApp.ConversationsRoute = Ember.Route.extend({
model: function () { //this is a collection of active conversations
},
activate: function() { //listens to event stream
}
});
ChatApp.ConversationsController = Ember.Controller.extend({
actions: {
openChat: function(user_id, profile_id){ //open chat if one isn't open.
}
},
removeExcessChats: function(){ // removes chats that don't fit in window
},
});
ChatApp.ConversationHolderComponent = Ember.Component.extend({
actions: {
markInactive: function(){
// referencing a passed in conversation is the only way I know to reference the model.
this.get('conversation').markInactive();
},
createMessage: function(){
}
},
isFlashed: false
});
Component templates:
<script type="text/x-handlebars" data-template-name="components/conversation-holder">
<button {{action "markInactive"}}>close</button>
<h3>Profile: {{conversation.profile}} Conversation: {{conversation.id}}</h3>
<ul class="list-unstyled">
{{#each message in conversation.messages}}
<li><strong>{{message.type}}</strong> {{message.body}}</li>
{{/each}}
<li>
<form class="form-inline" {{action "createMessage" on="submit"}}>
{{input class="message_body" placeholder="Start typing a message..." value=conversation.new_message_body type="text"}}
{{input class="btn" type="submit" value="Send"}}
</form>
</li>
</ul>
</script>
<script type="text/x-handlebars" data-template-name="conversations">
<section id="todoapp">
<header id="header">
<h1>Chat Messaging</h1>
</header>
</section>
<section id="main">
<p>Open a new chat with profile id #1 <a href="#" {{action "openChat" 1 1}} >Open w/ profile 1</a> | <a href="#" {{action "openChat" 1 6}} >open profile already in convo</a></p>
<ul id="chat-app" class="list-unstyled clearfix">
{{#each conversation in model}}
<li>{{chat-holder conversation=conversation}}</li>
{{/each}}
</ul>
</section>
</script>
I didn't go through your app design, but I'm answering based on the more general Ember concepts that you mentioned.
1.
There isn't really a model object in Ember. You have a route with a model hook that returns whatever you want as your model. It can be a string, array or just a number.
When you use Ember Data, what will happen is that the model hook returns Ember Data objects.
A component can receive any object as its model/content. So, there isn't a best or worst way of associating a model and component, you just pass it what it needs.
2.
If your component is starting to get too big, probably you should split it in two or more components. Nothing wrong with having a component's template render other components.
Also, if you have logic that is shared among many components, you can refactor that into a mixin and include it in each component.
3.
Your idea for message passing between the controller and the components is *probably* right. The usual flow in Ember apps is events up & data down. Since the controller is at a higher level than a component, you can't send event in that direction, but by updating bound values you can pass new info to the components.
Related
I have to construct a tree structure like the below image.
For this I use a Ember View and recursively call to construct the whole tree like structure based on the supplied model.
My Templates are:
<script type="text/x-handlebars" data-template-name="index">
<div class="zd-fldr fleft" style="width:230px;">
<ul class="fldr-sub">
{{#each item in model}}
{{view App.FoldertreeView model=item contentBinding="item"}}
{{/each}}
</ul>
</div>
</script>
<script type="text/x-handlebars" data-template-name="foldertree">
{{#if item.subfolder }}
<span {{action 'getSubFolder' item}} {{bind-attr class="item.IS_OPENED:fdtree-icon:ftree-icon"}}> </span>
{{else}}
<span class=""> </span>
{{/if}}
<span style="padding-top:20px;" class="fdetail fleft" >{{item.FOLDER_NAME}}</span>
<ul style="margin-top:30px;" {{bind-attr class="item.IS_OPENED:showdiv:hidediv"}}>
{{#each item in item.children}}
{{view "foldertree" model=item contentBinding="item"}}
{{/each}}
</ul>
</script>
JavaScript:
App.IndexRoute = Ember.Route.extend({
model: function() {
var treeArray = [];
for(var i=0; i<4000; i++){
var temp_obj = { 'FETCHED_DATA': false, 'FOLDER_ID': i, 'FOLDER_NAME': 'Folder_'+i, 'IS_OPENED': false, 'opened': true, 'subfolder': true, 'children': [] };
treeArray.push(temp_obj);
}
return treeArray;
}
});
App.FoldertreeView = Ember.View.extend({
tagName: 'li',
templateName: 'foldertree',
classNames: ['treediv', 's-fldr']
});
Initially I load only the first level folders from the server by calling an API.
Then when the open node is clicked, the children array is filled by calling an request to the server.
Now when the model length is greater than 3000 "Stop Script" error is thrown in Firefox browser.
In my tree there is no limit for the number of nodes. How can I solve this problem.
Demo JS Bin (Try it in Firefox)
Ember is a web framework. Given that information, you need to realize that you can't efficiently render 6000 items in a browser without reusing some view elements. Even native applications don't do this: in iOS, for instance, the cells in a TableView are reusable, so a table displaying a collection of 6000 items only has enough cells to cover the height of he view and some scrolling overlap. The view is aware of its scroll location, and renders the 10-20 items that need to be rendered from the collection, and when you scroll down it removes the top element, places an element at the bottom, and renders the next item in the data array. This way, everyone wins. I would suggest you do the same, as JS/HTML just can't handle that many elements efficiently.
I know it's not a fun implementation, but once you come up with a component that does this the first time, you'll be glad you did.
Honorable mentions: https://github.com/emberjs/list-view. You're doing a file tree and not a list, which is more difficult than just a long list, but you may still be able to use it if you change up your UI a little bit. If you have the folder structure navigable with a tree and show files in a list-view, this may mitigate your issue depending on whether the problem is with a number of files or a number of folders.
This is not really an Ember issue but a general javascript issue. When a script is taking to long time to execute this kind of errors message are displayed / fired by the browser and it's different on each browser.
You can read this good blog post about long time runing scripts
If you have browser environment undercontroll (i mean your computer our your companies computers) you can still setup firefox to run longer scripts
However a good practice would be to "split" your script in sub task taking less time to execute.
EDIT
Ass discussed in the comments this is due to the Huge number of view you generate. You can have 6000 models returned from your backend however generating 6000 view at once is heavy.
Here is a proposition on how to handle this : http://jsbin.com/zakisoyesi/6/edit?html,js,output free to you to adapt it to your use case and event to make it transparent to the user by using onScroll or any other event.
I am working on an application that needs to modify the content of the navbar after login. Here's a basic sketch I put together (with some help from other samples online):
http://jsbin.com/umutag/1/ with this underlying code: http://jsbin.com/umutag/1/edit
How do I get the header view to display model data?
Should I be using a different helper for the template? (e.g. a {{view}}, {{render}}, or {{control}})
BTW, I've scoured this site and others, but most entries are a few months old and I see ember has been changing a lot since then (or I'm missing something obvious). The above example uses Ember 1.0.0 RC6.
Bryan
You ultimately want to bind the value in the controller (probably ApplicationController) that keeps track of whether the user is logged in or not. Since this is pertaining to login, you most likely have something like a SessionController that keeps track of the token. Here's one way to go about it:
App.SessionController = Em.Controller.extend({
token: null,
username: null,
isLoggedIn: function() {
return !!this.get("token");
}.property("token");
// ...
});
App.ApplicationController = Em.Controller.extend({
needs: "session",
isLoggedInBinding: "controllers.session.isLoggedIn",
usernameBinding: "controllers.session.username"
//...
});
And in your navbar in the template:
{{#if isLoggedIn}}
<li>Logged in as {{username}}</li>
<li>{{#linkTo "index"}}Home{{/linkTo}}</li>
<li>{{#linkTo "secret"}}Secret{{/linkTo}}</li>
{{else}}
<li>{{#linkTo "login"}}Log in{{/linkTo}}</li>
{{/if}}
We're building app that allows users to post messages to various social media outlets. Our designer has created a series of interactions which allow users to change various settings in their application by use of sliding panels. I've done a quick screen cap to illustrate:
http://screencast.com/t/tDlyMud7Yb7e
The question I have is one of architecture. I'm not sure whether I should be using a View or a Controller (or both) to store some of the methods these panels will contain. Here's the HTML for the panels. They're not currently in a script tag or view:
<div id="panel-account-settings" class="panel closed">
<div class="panel-inner">
<i class="icon-cancel"></i>close
<h3>Account Settings</h3>
Google Analytics
Link Shortening
Disconnect Account
</div>
<div id="panel-google-analytics" class="panel-inner">
<i class="icon-arrow-right"></i>back
<h3>Google Analytics</h3>
<div class="toggle">
<label>Off</label>
</div>
<p>We <strong>won't</strong> append stuff to your links, so they <strong>won't</strong> be trackable in your Google Analytics account.</p>
<img src="{{ STATIC_URL }}images/ga-addressbar.png" />
</div>
<div id="panel-disconnect" class="panel-inner">
<i class="icon-arrow-right"></i>back
<h3>Disconnect This Account</h3>
<p>If you disconnect this account you will lose all the metrics we tracked for each message. Are you absolute sure you want to get rid of them?</p>
<div class="button-group">
Disconnect
</div>
</div>
</div>
The gear icon shown in the video is contained with the accounts template
<script type="text/x-handlebars" data-template-name="accounts">
{{#each account in controller}}
<div class="avatar-name">
<p>{{account.name}}</p>
<p>#{{account.username}}</p>
<i class="icon-cog" {{action "openPanel" Social.SettingsView account }}></i>
</div>
{{/each}}
</script>
which has a bare bones controller
Social.AccountsController = Ember.ArrayController.extend({
openPanel: function(view,account){
console.log(view,account);
$(this).parents(".item-account").addClass("active");
$("#panel-account-settings").prepareTransition().removeClass("closed");
}
});
as well as a Route and a Model. Given the interaction I'm looking to accomplish, my question is where should I be putting the pieces and parts? At a minimum I need to pass in the current Account model so that I know which account I'll be applying changes to. I thought about creating a mainPanel view which would contain the other view...something like this:
<script type="text/x-handlebars" data-template-name="panelView">
<div id="panel-account-settings" class="panel closed">
{{ partial "panelSettingsView" }}
{{ partial "panelAnalyticsView" }}
{{ partial "panelDisconnectView" }}
</div>
</script>
and then the action helper on the gear icon could pass in the account AND the required view. But I'm not sure if that's the right approach. I'd appreciate some input or suggestions. Thanks.
UPDATE 1
Ideally I'd like to eventually load in the content of each panel via AJAX but that's a want to, not a need to.
UPDATE 2
I tried creating a PanelView which would contain the logic on which panels to load:
Social.PanelView = Ember.View.extend({
tagName: 'div',
classNames: ['panel-inner'],
openPanel: function(view,account){
console.log(view,account);
}
});
But when I tried to call it from the gear icon I got an error. This:
<i class="icon-cog" {{action openPanel target="Social.PanelView" }}></i>
Threw this error:
Uncaught Error: assertion failed: The action 'openPanel' did not exist on Social.PanelView
Isn't that the correct syntax?
UPDATE 3
Adding version information:
DEBUG: Ember.VERSION : 1.0.0-rc.1
DEBUG: Handlebars.VERSION : 1.0.0-rc.3
DEBUG: jQuery.VERSION : 1.9.1
The best practice is to always put any DOM- or UI-related logic into your view, and leave data representation to the controller (i.e., a reference to a 'selected' item in the controller is a common example).
Your Social.AccountsController.openPanel method has logic that touches the DOM, which is entirely a view concern. A good start would be to move that logic into the view (Social. SettingsView ?).
It'd be a bit easier to understand your goals and offer more suggestions if you had a jsfiddle of what you have so far.
EDIT: Another good practice is to decompose things into very small objects. So you could explore having a selectedAccount ObjectController whose content is the currently chosen Account (and a corresponding View for it).
i try to create my first ember.js app. A calendar-
my day model
App.Day = Ember.Object.extend({
today : null,
dayNumber : null,
addEvent : function() {
console.log(this);
$("#myModal").modal('show');
}
});
the html view
<div class="cal">
{{#each App.DayList}}
{{#if this.today}}
<div class="day today" {{action "addEvent" target="model" }}>
{{#with this as model}}
<span class="text">{{this.dayNumber}}</span>
{{/with}}
</div>
{{else}}
<div class="day" {{action "addEvent" target="model" }}>
{{#with this as model}}
<span class="text">{{this.dayNumber}}</span>
{{/with}}
</div>
{{/if}}
{{/each}}
</div>
so on click on day i show the bootstrap dialog and I wont to load extern data, but I need a information about clicked day.
My understanding is I create a view
App.DayDetails = Ember.View.extend({
});
and inside this view I send an ajax request, but how to get information about clicked day inside this view?
You should almost never be doing any AJAX in a view.
Views do two things:
(1) draw themselves
(2) respond to UI events (clicks, typing, etc)
Your view should get its contents from a controller, in this case I suppose App.DayController or DayDetailsController. (that's another thing, it's best practice to end your subclasses with View or Controller, etc, so its obvious at a glance what they do).
Where the controller gets that data from is where things might get complicated. Ideally, in a mature app, you'd have a data store (a combination—in concept—of your server-side database and ActiveRecord, if you use rails) that would be queried. Simplistically, however, you could have the controller be responsible for using jQuery to manually handle an ajax request. So long as we're taking short-cuts, you could put such a call in a number of place, (a singleton controller, a day-specific item controller, the day model itself), just NOT the view. And it's important when taking these sorts of short-cuts to limit the contagion... all you should be doing with the manual ajax is fetching the JSON and then immediately and expeditiously getting it back into the ember ecosystem by setting it as the content of an array controller. I.e., no going one or two steps further by trying to insert the data into a view manually or whatnot. Don't fight Ember, if you can avoid it.
A few things:
(1) Your use of this is superfluous, as are the {{with}} statements. Inside an {{each}} block the context will be the current object (or its wrapping controller, if you're using itemController) in the iteration. (UNLESS you use "x in y" syntax, in which case the context remains the controller)
(2) The model should NOT be attempting to modify the DOM. Instead, rely on bindings and your controllers to coordinate UI changes. What you might want to do is have a App.DayController that you can put addEvent on, and then in your {{each}} use itemController="App.DayController".
App.DayController = Ember.ObjectController.extend({
addEvent: function () {
// ...
}
});
Then, the context for each loop in your {{each}} template will be each individual day controller. The controller will automatically be the target and context for the views so your template would look like this:
{{#each App.DayList itemController="App.DayController"}}
<div {{bindAttr class=":day today"}} {{action addEvent}}>{{dayNumber}}</div>
{{/each}}
(the : in :day means that day will always be a class, but today will only be a class if the today property on the context is truthy)
Because each day sends addEvent to its own controller, there's no need for figuring out what day to load.
Trying to figure out the "ember best practices" for my app, regarding MVC. also for reference, I'm using ember-data, ember-layout, and ember-route-manager.
I'll use User as an example:
what I feel like I want to do is to get a User model from the database... then wrap it in a UserController, and set the model on a "content" property... then in a View, I want to bind to the controller for some functionality, and to the controller.content for model-level data. so a controller might look something like:
App.UserViewController = Em.Object.create({
content: userRecord,
isFollowingBinding : 'content.you_follow',
toggleFollow: function() {
make server call to change following flag
}
});
then the view could bind to the {{controller.content.name}}, or {{#if controller.isFollowing}}, or {{action "toggleFollowing" target="controller"}}
but say I get a list of User models back from the database... I feel like what should happen is that each of those models should be wrapped with a controller, and that should be returned as a list... so the view would have a list of UserControllers
Incidentally, I've done this... and it is working nicely.... except that everytime I reload the list, I wrap all of the new model objects with new controllers... and over time, the # of controllers in memory get larger and larger. on my base Controller class, I'm logging calls to "destroy", and I dont see it ever happening
when it comes to Em.View... I know that everytime it is removed from the screen, .destroy() gets calls (I am logging those as well). so if I were to move my code into a view, i know it will get destroyed and recreated everytime... but I dont feel like the functionality like toggleFollow() is supposed to be in view...
SO QUESTIONS:
is this how MVC is supposed to work? every instance of a model wrapped in a controller for that model? where there could be lots of controller instances created for one screen?
if I go down this approach, then I'm responsible for destroy()ing all of the controllers I create?
or is the functionality I've described above really meant for a View, and them Ember would create/destroy them as they are added/removed from the screen? also allowing template designers to decide what functionality they need (if they just need the {{user.name}}, theres no need to instantiate other controller/view classes... but if they need a "toggle" button, then they could wrap that part of the template in {{#view App.UserViewController contentBinding="this"}} )
I re-wrote this a few times... hopefully it makes sense....
I wouldn't wrap every user into an own controller.
Instead I would bind the user to a view, say App.UserView and handle the action toggleFollow on that view. This action will then delegate it's action to a controller which will handle the server call, see http://jsfiddle.net/pangratz666/hSwEZ/
Handlebars:
<script type="text/x-handlebars" >
{{#each App.usersController}}
{{#view App.UserView userBinding="this" controllerBinding="App.usersController"}}
{{user.name}}
{{#if isFollowing}}
<a {{action "toggleFollowing"}} class="clickable" >stop following</a>
{{else}}
<a {{action "toggleFollowing"}} class="clickable" >start following</a>
{{/if}}
{{#if user.isSaving}}saving ...{{/if}}
{{/view}}
{{/each}}
</script>
JavaScript:
App.usersController = Ember.ArrayProxy.create({
content: [],
toggleFollowing: function(user) {
user.set('isSaving', true);
Ember.run.later(function() {
user.toggleProperty('you_follow');
user.set('isSaving', false);
}, 1000);
}
});
App.UserView = Ember.View.extend({
isFollowingBinding: 'user.you_follow',
toggleFollowing: function() {
var user = this.get('user');
var controller = this.get('controller');
controller.toggleFollowing(user);
}
});