Binding model changes to view in EmberJS - javascript

I'm trying to implement an interface for a logger with EmberJS and has been great but I run into an wall.
Currently I have the following code:
logs.hbs
...
{{input action="search" onEvent="keypress" value=searchText class="search" placeholder="Search"}}
...
<table id="log" class="log">
<thead>
<th>Service</th>
<th>Start Time</th>
<th>End Time</th>
<th>URL</th>
<th>Method</th>
<th>User</th>
<th>Response Code</th>
</thead>
<tbody>
{{render "dashboard/logTableLine" model}}
</tbody>
</table>
...
logTableLine.hbs
{{#each model}}
{{#link-to "dashboard.log" _id tagName="tr"}}
<td>{{ServiceID}}</td>
<td>{{DateString StartTime}}</td>
<td>{{DateString EndTime}}</td>
<td>{{URL}}</td>
<td>{{Method}}</td>
<td>{{AuthName Auth}}</td>
<td>{{StatusCode}}</td>
{{/link-to}}
{{/each}}
and my app.js
App = Ember.Application.create({
LOG_TRANSITIONS: true,
LOG_TRANSITIONS_INTERNAL: true
});
App.Router.map(function(){
this.resource("dashboard", { path: "/" }, function(){
this.route("logs", { path: "/logs" });
this.route("log", { path: "/logs/log/:_id" });
});
});
App.DashboardLogsRoute = Ember.Route.extend({
model: function(){
return Ember.$.getJSON("/logs.json");
},
actions: {
search: function(value){
if(value != '')
this.set("content", Ember.$.getJSON("/logs.json?search=" + value));
}
}
});
App.DashboardLogRoute = Ember.Route.extend({
model: function(params){
return Ember.$.getJSON("/logs/log/" + params._id + ".json");
}
});
My problem is binding that model to the view, so that after the search call to the server the view rerenders. I want to search all data, not only what I have in the interface (last 100 records).
So, first question: why the view isn't updating (binded) to the model?
Second: Is there a better way (best pratice) to do this?
Thanks in advance for any help.

Right now you're implementing search without triggering a re-route. So if the user searched and then wanted to go back to the page before they were at before they searched, they would have no way to do that. What you want to do is re-route the user to the same page, but pass in query parameters. Then in the model function you can see what parameters were passed in and fetch the data accordingly. Ember's latest and greatest way of doing this is the query-params feature: http://emberjs.com/guides/routing/query-params/ It's been in the works for a while and is now in beta. I'd give it a go, because it's a really clean, intuitive way of solving this problem. Good luck!

Related

Implementing Vue.js + DataTables properly

Im trying to implement Vue.js + jQuery's DataTables but there's a weird things happening.
Check this fiddle on firefox (not working on chrome):
http://jsfiddle.net/chrislandeza/xgv8c01y/
when I change the state of DataTable (e.g. sort, search, etc.):
Newly added data on the list disappears
The DOM is not reading the directives or the vue properties
I'm pretty sure anyone who tried to mix vue.js+datatables experienced this problem. what did you do to solve this?
or is there a pure Vue.js script/plugin that has the same (or close) functionality like jquery's DataTable? (pagination, searching, sorting, number of entries to show, etc.).
here's the code from the fiddle above:
HTML:
<div class='container-fluid' id="app">
<div class='row'>
<div class='col-md-9'>
<table class="table table-bordered" id="app-datatable">
<thead>
<tr>
<th>Name</th>
<th>Age</th>
<th></th>
</tr>
</thead>
<tbody>
<tr v-repeat="user: users">
<td>{{ user.name }}</td>
<td>{{ user.age }}</td>
<td>
<button type="button" v-on="click: foo(user)">Action</button>
</td>
</tr>
</tbody>
</table>
</div>
<div class='col-md-3'>
<div class="form-group">
<label>Name</label>
<input type="text"
class="form-control"
v-model="newUser.name"
>
</div>
<div class="form-group">
<label>Age</label>
<input type="name"
class="form-control"
v-model="newUser.age"
>
</div>
<button type="submit" class="btn btn-primary" v-on="click: addUser()">Add</button>
</div>
</div>
</div>
JavaScript:
$(document).ready(function () {
var dT = $('#app-datatable').DataTable();
});
var vm = new Vue({
el: '#app',
data: {
newUser: {},
users: [
{name: 'Chris', age: 1},
{name: 'John', age: 2}
]
},
methods:{
addUser: function(){
this.users.push(this.newUser);
this.newUser = {};
},
foo: function(user){
console.log(user.name);
}
}
});
any suggestions are greatly appreciated.
To get the DataTables plugin integrated correctly with Vue, there are a few things to keep in mind:
Per your example, you can use var dT = $('#app-datatable').DataTable(); to initialize the DataTables if you already have the data ready and rendered to the DOM. If you don't have the DOM <table></table> fully rendered (perhaps due to data populated via a delayed ajax call), you can't initialize the DataTables until the data is ready. As an example, if you have a fetchData method in your component, you can initialize the DataTable once the promise has been fulfilled.
To update the table once initialized, perhaps due to a change in the underlying table data, the best (perhaps the only way), is to first destroy the table, before the new data is received and written to the DOM by Vue:
var dT = $('#app-datatable').DataTable();
dT.destroy();
Then, once the data (in your case, the users array) has been updated,
re-initialize the DataTable as so:
this.$nextTick(function() {
$('#app-datatable').DataTable({
// DataTable options here...
});
})
The $nextTick is necessary to ensure Vue has flushed the new data to the DOM, before re-initializing. If the DOM is updated after the DataTable plugin has been initialized, you'll see the table data, but the usual sorting, paging, etc. won't work.
Another important point, is to have a row id in your dataset, and set the key in the <tr></tr>:
<tr v-repeat="user: users" track-by="id">
Without the track-by, Vue will complain when flushing new data to the DOM after DataTables has been initializing, likely due to not finding DOM elements hi-jacked by DataTables.
maybe you can use lifecycle hooks, infact these weird things are caused by competition of manipulating the DOM. in your vue instance, add a created() hook then initialize the DataTable, just like the following:
var vm = new Vue({
el: '#app',
data: {
newUser: {},
users: [
{name: 'Chris', age: 1},
{name: 'John', age: 2}
]
},
methods:{
addUser: function(){
this.users.push(this.newUser);
this.newUser = {};
},
foo: function(user){
console.log(user.name);
}
},
created(){
this.$nextTick(function() {
$('#app-datatable').DataTable();
})
}
});
I have externally supplied table data (not an Ajax call from DataTable) and destroying / re-creating the table using only these lifecycle hooks is working for me:
beforeUpdate: function() {
if (this.dataTable) {
this.dataTable.destroy()
}
},
updated: function() {
this.dataTable = $(this.$el).DataTable()
}
From https://alligator.io/vuejs/component-lifecycle/
"The beforeUpdate hook runs after data changes on your component and the update cycle begins, right before the DOM is patched and re-rendered. It allows you to get the new state of any reactive data on your component before it actually gets rendered."
"The updated hook runs after data changes on your component and the DOM re-renders. If you need to access the DOM after a property change, here is probably the safest place to do it."

How to pass model in Nested routes - emberjs

I have some nested routes.
App.Router.map(function() {
this.route("dashboard", { path: "/dashboard" });
this.resource("customers", { path: "/customers" },function(){
this.resource("customer",{ path: "/:customer_id" },function(){
this.resource("customer.contact",{path:'/contact'});
});
});
});
TEMPLATES
customers/index
<script type="text/x-handlebars" data-template-name="customers/index">
<h3>Customers</h3>
<table>
{{#each item in model}}
<tr>
<td>{{item.name}}</td>
{{#link-to "customer" item tagName="td"}}Info{{/link-to}}
</tr>
{{/each}}
</table>
</script>
customer
<script type="text/x-handlebars" data-template-name="customer">
<h3>Customer {{name}}</h3>
{{#link-to}}Gallery{{/link-to}}
{{#link-to "customer.contact" this}}Contact{{/link-to}}
{{outlet}}
</script>
contact
<script type="text/x-handlebars" data-template-name="customer/contact">
<h3>Contact info of customer {{name}}</h3>
{{contact}}
</script>
Go Customers->Info
Everything works fine, the link from "customers/index" template passes the item to the customer template where {{name}} will be used. but if i want to pass the context to "contact" template, it doesnt work.
here is the JsBin
http://emberjs.jsbin.com/EveQOke/107
You need to specify a route for customer contact (as well for customer). The reason it works initially is because the link-to is passing the model to the route, so it can skip the non-existent model hook. But when you refresh the page, or hit the contact route, which has no dynamic segment, you need to tell ember that you want to use a model. There is a beta feature that allows all the routes under a resource to use the resource if they don't have another resource defined, but that's still a feature, and isn't yet gold.
App.CustomerRoute = Ember.Route.extend({
model: function(param){
return this.store.find('customer', param.customer_id);
}
});
App.CustomerContactRoute = Ember.Route.extend({
model: function(){
return this.modelFor('customer');
}
});
http://jsbin.com/EveQOke/110/edit

Data duplicating on all items when saving with Ember-Data

I have a page where a user edits an uploaded photo and applies a tag for individual photos on the model using Ember-Data. However, after saving on the controller, and transitioning to a page with all of the photos listed, the tag appears on all of the items and replaces any that existed before. If I reopen the page the tag has not saved at all.
I'm not quite sure what is causing this issue. Any help would be appreciated.
//The photo model
App.Photo = DS.Model.extend({
title: attr(),
description: attr(),
image: attr(),
width: attr(),
height: attr(),
important_top: attr(),
important_left: attr(),
important_bottom: attr(),
important_right: attr(),
created: attr('date'),
authors: hasMany('author'),
app_data: {
tags: []
},
imageURL: function() {
return document.location.origin + '/media/' + this.get('image');
}.property('image'),
});
// Photo edit route
App.PhotoseditRoute = Ember.Route.extend({
model: function() {
this.store.find('photo');
// Populate model with photos from the lowest upload ID to higest.
return this.store.filter('photo', function(image){
return image.get('id') >= App.Upload.uploadedImages[0]; // Get data from uploader
});
},
activate: function() {
$('#page-title').text('Edit Photos');
},
});
// Photo Edit Controller
App.PhotoseditController = Ember.ObjectController.extend({
parsedTags: function() {
// Get tags from the view's input field
var tagData = this.get('app_data').tags;
// Convert tags to an array
return tagData.join(',');
}.property('app_data'),
// Watch parsedTags and apply array to model when converted
parsedDataChanged: function() {
Ember.run(this, function() {
this.get('app_data').tags = this.get('parsedTags').split(',');
});
}.observes('parsedTags'),
actions: {
save: function() {
var that = this;
that.get('model').save().then(function(success) {
that.transitionToRoute('photos');
});
}
}
});
// Photo edit template
<h2>Edit Photo Meta Data</h2>
<button {{action 'save'}} style="float:right;">Save All</button>
<table>
<thead>
<tr>
<th></th>
<th>Title</th>
<th>Description</th>
</tr>
</thead>
<tbody>
{{#each object in content itemController='photosedit'}}
<tr>
<td><img {{bind-attr src="imageURL"}} width="200" /></td>
<td>{{input title valueBinding="title"}}</td>
<td>{{input description valueBinding="description"}}</td>
<td>{{input parsedTags valueBinding="parsedTags"}}</td>
</tr>
{{else}}
<tr>
<td colspan="6">No photos yet.</td>
</tr>
{{/each}}
</tbody>
</table>
The problem comes from the way you declare app_data. This variable will be shared across all instances of App.Photo, which explains why you see all photos getting the same tags.
You can solve this by initializing the attribute differently:
App.Photo = DS.Model.extend({
init: function() {
this._super();
this.app_data = { tags: [] };
}
});
instead of
App.Photo = DS.Model.extend({
app_data = { tags: [] }
});
See this JsBin for an example highlighting the problem and the solution http://emberjs.jsbin.com/wawoq/3
You need to check that the store gets called with the correct data when you call save() and trace back from there.
Aside from this, parsedTags and parsedDataChanged seem to be referring to each other.

Insert new line dynamically into a Table context, using Ember.js

I'm new to Ember and Handlebars.
I am trying to create a table with a dynamic content.
I add the code into jsfiddle
I have the following code:
<script type="text/x-handlebars">
<table border="1" bordercolor="FFCC00" style="background-color:FFFFCC" width="400" cellpadding="3" cellspacing="3">
{{#each Table.tableController}}
<tbody>
<tr>
<td width ="30%">{{title}}</td>
<td width ="30%">{{artist}}</td>
<td width ="40%">{{genre}}</td>
</tr>
</tbody>
{{/each}}
</table>
A simple table binding a content from my controller.
And here is my controller and application:
Table = Ember.Application.create({});
Table.Cell = Ember.Object.extend({
title: null,
artist: null,
genre: null,
listens: 0
});
Table.tableController = Ember.ArrayProxy.create({
content: [],
init: function() {
var data = Table.Cell.create({
title: 'Ruby',
artist: 'Kaiser Chiefs',
genre: 'Indie Rock',
});
this.pushObject(data);
data = Table.Cell.create({
title: 'Somebody Told Me',
artist: 'Killers',
genre: 'Indie Rock',
});
this.pushObject(data);
},
createSong: function(title, artist, genre) {
var cell = Table.Cell.create({
title: title,
artist: artist,
genre: genre,
});
this.pushObject(cell);
},
});
That's working just fine.
Now I want to add a new song, and I am doing this via Chrome console with the following line:
Table.tableController.createSong('Feiticeira', 'Deftones', 'Alternative');
I can verify that a new Object is created and inserted into my content array.
But there is no changes into my table.
What am I doing wrong?
Why my table is not creating a new content?
I pushed this code into jsfiddle to get help you help me.
Here's the link to the code again.
UPDATED ANALYSIS
You forgot calling this._super(); in the init, which prevented your instance to have its binding management setup. (Fixed revision)
You felt in a typical Ember gotcha: initial values should never be objects, as they would be shared by all class instances... See this article for more informations.
FIX SAMPLE - Still recommended implementation
Updated your JSFiddle # http://jsfiddle.net/MikeAski/AgyAk/12/ with a few improvements (The most notable: make use of a controller rather than directly ArrayProxy. It is more idiomatic and ready to switch to Router infrastructure).

#collection using contentBinding does not automatically update when rendering in a tbody

I'm new with emberjs so it may be possible that I totally missed a few things.
I am trying to render the body of a table with content I retrieve from an Ajax request.
I use an ArrayController and a {{#collection}} in my view that I bind to the content of the controller (as the doc suggests).
It works when I set an initial value to content inside my ArrayController but it doesn't automatically update when I call .set('content', [...]).
Note that it only fails when I set the tagName of my view to 'tobdy', it works if for example I use 'pre' (or pretty much anything else).
JSFiddles that show that issue:
http://jsfiddle.net/midu/DjxG4/10/ (with tbody, should update after 2s, but doesn't)
http://jsfiddle.net/midu/DjxG4/11/ (with pre, updates after 2s)
My questions are:
is this a bug or did I not understand how it should be working?
any advice for rendering the body of a table in a template?
This is what my code looks like:
index.html
<table id="the-table">
<thead>
<tr>
<th>status</th>
<th>title</th>
<th>url</th>
<th># messages</th>
<th>judging status</th>
</tr>
</thead>
</table>
<script type="text/x-handlebars" data-template-name="submissions">
{{#collection contentBinding="App.submissionsController"}}
<tr>
<td>{{content.status}}</td>
<td>{{content.title}}</td>
<td>note</td>
<td>{{content.nbMessages}}</td>
<td>{{content.judgingStatus}}</td>
</tr>
{{/collection}}
</script>
index.js
var App = Em.Application.create({
ready: function () {
App.submissionsController.index();
this._super();
}
});
// model
App.Submission = Em.Object.extend({
title: null,
judgingStatus: null,
status: null,
url: null,
nbMessages: 0
});
// controller... not really.
App.submissionsController = Em.ArrayController.create({
content: [ App.Submission.create({title: 'kevin', status: 'a', judgingStatus: 'b', url: 'http://google.com', nbMessages: 1}) ],
index: function () {
// simulates data that arrives to the page after a few seconds (think ajax request)
window.setTimeout(function () {
App.submissionsController.set('content', [
App.Submission.create({title: 'stefano', status: 'c', judgingStatus: 'd', url: 'http://stackoverflow.com', nbMessages: 2})
]);
}, 2000);
}
});
Em.View.create({
templateName: 'submissions',
tagName: 'tbody'
}).appendTo('#the-table');
Two things I did to fix the code which can be seen on jsfiddle.
I would intermittently get uncaught exception: Error: <Ember.View:ember161> - Unable to find template "submissions" because the handlebar script is not evaluated before the app is inserted into the DOM. See here.
Change the view to {{#each}} instead of the deprecated {{#collection}}. I haven't found a definite source on the deprecation – but it's indicated here and here.

Categories