I have Ember displaying a list of things.
I want the user to upload a file and change the list of things to the stuff in that file.
HTML
<script type="text/x-handlebars" id="index">
<h2>List of things</h2>
<ul>
{{#each item in model}}
<li>{{item}}</li>
{{/each}}
</ul>
Change List of Things:
{{#view Ember.View}}
{{view App.FileUp}}
{{/view}}
</script>
Javascript
App = Ember.Application.create();
App.Router.map(function() {
// put your routes here
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.ApplicationController = Ember.Controller.extend({
actions: {
file_upload: function(data) {
alert("I want to change the model to: " + data);
}
}
});
App.FileUp= Ember.TextField.extend({
type: 'file',
change: function(evt) {
var input = evt.target;
if (input.files && input.files[0]) {
var that = this;
var reader = new FileReader();
reader.onload = function(e) {
var data = e.target.result;
var c = App.ApplicationController.create();
c.send('file_upload', data);
}
reader.readAsText(input.files[0]);
}
},
});
At the line with the alert, I have access to the text from the uploaded file, and I'm in the application controller. How can I change the model so that the list of the page reflects what's in the text file?
JS Fiddle: http://jsfiddle.net/LJx9t/
If this code isn't idiomatic Ember, please feel free to correct.
Being a component you should use sendAction to pass the action out of the component. This allows the component to be used multiple times and doesn't tie it to the context.
{{view App.FileUp uploadComplete='file_upload'}}
{{view App.FileUp uploadComplete='file_upload2'}}
Then inside your component
change: function(evt) {
var input = evt.target,
self = this;
if (input.files && input.files[0]) {
var that = this;
var reader = new FileReader();
reader.onload = function(e) {
var data = e.target.result;
self.sendAction('uploadComplete', data);
}
reader.readAsText(input.files[0]);
}
},
then your action gets sent to the controller, then route, then up the route tree until it's handled.
Create an index controller to handle it
App.IndexController = Ember.Controller.extend({
actions: {
file_upload: function(data) {
this.set('model', data.split('\n'));
},
file_upload2: function(data) {
alert(data);
}
}
});
http://jsfiddle.net/rmCYk/1/
The model you want to change has been created by the IndexRoute, so you need to set it to the IndexController. Furthermore the App.FileUp is a component, so in order to send the action to the currently active controller, which is IndexControler it is required to use targetObject or get('controller') from parentView .
(related info, https://github.com/emberjs/ember.js/blob/master/packages/ember-handlebars/lib/controls.js#L81-84 and a discussion https://github.com/emberjs/ember.js/issues/3393 )
So assuming a file with csv is uploaded the following code can achieve what you require,
http://jsfiddle.net/9u3Cd/
js
App = Ember.Application.create();
App.Router.map(function() {
// put your routes here
});
App.IndexRoute = Ember.Route.extend({
model: function() {
return ['red', 'yellow', 'blue'];
}
});
App.IndexController = Ember.ObjectController.extend({
actions: {
file_upload: function(data) {
alert("I want to change the model to: " + data);
this.set('model',data.split(","));
}
}
});
App.FileUp= Ember.TextField.extend({
type: 'file',
change: function(evt) {
var self = this;
var input = evt.target;
if (input.files && input.files[0]) {
var that = this;
var reader = new FileReader();
reader.onload = function(e) {
var data = e.target.result;
//self.get('parentView').get('controller').send('file_upload', data);
self.get('targetObject').send('file_upload', data);
}
reader.readAsText(input.files[0]);
}
},
});
Related
I've created a search bar, but when the data is gathered from the user, it displays the default data over again rather then the users new search criteria.
I'm resetting the collection and giving it a new URL when the user searches, but it doesn't seem to update correctly, and I'm having trouble figuring out where my problem(s) are.
(function(){
'use strict';
var red = red || {};
//model////////////////////////////////////////////////
red.RedditModel = Backbone.Model.extend({
defaults: {
urlTarget: $('#textBox').val(),
urlStart: 'https://www.reddit.com/r/',
urlEnd: '.json'
},
initialize: function() {
this.on('change:urlTarget', function() {
console.log('The Url Target has changed to ' + this.get("urlTarget"));
});
this.on('change:concatURL', function() {
console.log('The model Url has changed to ' + this.get("concatURL"));
});
this.on('change:url', function() {
console.log('The collection url has changed to: ' + this.get('url'));
});
}
});
var redditModel = new red.RedditModel();
var fullURL = new red.RedditModel({
concatURL: redditModel.attributes.urlStart + redditModel.attributes.urlTarget + redditModel.attributes.urlEnd
});
var listElmement,
$list = $('.list');
//collections//////////////////////////////////////////
red.redditCollection = Backbone.Collection.extend({
model: red.RedditModel,
url: fullURL.attributes.concatURL,
parse: function(response) {
var redditData = response.data.children;
return redditData;
}
});
//view////////////////////////////////////
red.RedditView = Backbone.View.extend({
model: fullURL,
collection: redditCollection,
el: '.searchBar',
events: {
'click .searchButton': function(e) {
this.updateModel(e);
this.updateCollection(e);
},
'change #textBox': 'initialize'
},
updateModel: function() {
this.$urlTarget = $('#textBox').val()
this.model.set('urlTarget', this.$urlTarget);
this.model.set('concatURL', redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd);
},
updateCollection: function() {
this.collection.reset();
this.$urlTarget = $('#textBox').val();
var newUrl = redditModel.attributes.urlStart + this.$urlTarget + redditModel.attributes.urlEnd;
this.collection.add({ urlTarget: this.$urlTarget });
this.collection.add({ url: newUrl });
console.log(newUrl);
},
tagName: 'li',
className: 'listItems',
initialize: function() {
$list.html('');
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
})
}
});
},
render: function() {
}
});
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
var myRedditView = new red.RedditView({
model: redditModel,
collection: redditCollection
});
$('.page').html(myRedditView.render());;
})();
Parse within the model, and use it for its intended purpose. No need to store the reddit url and other search related info in a model.
red.RedditModel = Backbone.Model.extend({
parse: function(data) {
return data.data;
},
})
Since you already take care of the reddit url here. Don't be afraid to make yourself some utility functions and getters/setters in your Backbone extended objects (views, model, collection, etc).
red.RedditCollection = Backbone.Collection.extend({
url: function() {
return 'https://www.reddit.com/r/' + this.target + this.extension;
},
initialize: function(models, options) {
this.extension = '.json'; // default extension
},
setExtension: function(ext) {
this.extension = ext;
},
setTarget: function(target) {
this.target = target;
},
parse: function(response) {
return response.data.children;
}
});
Don't be afraid to have a lot of views, Backbone views should be used to wrap small component logic.
So here's the item:
red.RedditItem = Backbone.View.extend({
tagName: 'li',
className: 'listItems',
render: function() {
this.$el.text(this.model.get('title'));
return this;
}
});
Which is used by the list:
red.RedditList = Backbone.View.extend({
tagName: 'ul',
initialize: function() {
this.listenTo(this.collection, 'sync', this.render);
},
render: function() {
this.$el.empty();
this.collection.each(this.renderItem, this);
return this;
},
renderItem: function(model) {
var view = new red.RedditItem({ model: model });
this.$el.append(view.render().el);
}
});
And the list is just a sub-component (sub-view) of our root view.
red.RedditView = Backbone.View.extend({
el: '.searchBar',
events: {
'click .searchButton': 'onSearchClick',
},
initialize: function() {
// cache the jQuery element for the textbox
this.$target = $('#textBox');
this.collection = new red.RedditCollection();
this.list = new red.RedditList({
collection: this.collection,
// assuming '.list' is within '.searchBar', and it should
el: this.$('.list'),
});
},
render: function() {
this.list.render();
return this;
},
onSearchClick: function(e) {
this.collection.setTarget(this.$target.val());
console.log(this.collection.url());
this.collection.fetch({ reset: true });
},
});
Then, you only need the following to use it:
var myRedditView = new red.RedditView();
myRedditView.render();
Notice the almost non-existent use of the global jQuery selector. If you're using Backbone and everywhere you're using $('#my-element'), you're defeating the purpose of Backbone which is, in part, to apply MVC concepts on top of jQuery.
Some notes on the code posted
Take time to understand what's going on. There are several lines of code in your question that doesn't do anything, or just don't work at all.
Though it's been removed in your answer, the following doesn't make sense because the collection constructor is Backbone.Collection([models], [options]) and what you have here translates to passing an options object (using ES6 shorthand property names { a, b, c}) to the models parameter.
var redditCollection = new red.redditCollection({
redditModel,
fullURL
});
This line does nothing, because .render() doesn't do anything and doesn't return anything.
$('.page').html(myRedditView.render());
Here, you're creating a new element manually using jQuery while you have Backbone which does this for you.
$('<li></li>').text(redditData);
Don't use the attributes directly, always use .get('attributeKey') unless you have a good reason not to.
redditModel.attributes.urlStart
Favor local variables whenever you can. The listElement var here is defined at the "app" level without a need for it.
listElmement = $('<li></li>').text(redditData);
$list.append(listElmement);
A Backbone collection is automatically filled with the new instances of models on success. You do not need to re-parse that in the success callback (in addition to the ambiguity with redditData).
this.collection.fetch({
success: function(redditData) {
redditData.each(function(redditData) {
redditData = redditData.attributes.data.title;
I don't mean to be rude and I took the time to write that long answer to try to help, you, and any future reader that comes by.
I have several Backbone Models rendered in a Collection View, and also I have a route that should render a view of that model. So, here come the views
resume.js
// this renders a single model for a collection view
var ResumeView = Backbone.View.extend({
model: new Resume(),
initialize: function () {
this.template = _.template($('#resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON));
return this;
}
});
#resume template
<section id="resume">
<h1><%= profession %></h1>
<!-- !!!!! The link for a router which should navigate to ShowResume view -->
View Details
</section>
Collection view:
var ResumeList = Backbone.View.extend({
initialize: function (options) {
this.collection = options.collection;
this.collection.on('add', this.render, this);
// Getting the data from JSON-server
this.collection.fetch({
success: function (res) {
_.each(res.toJSON(), function (item) {
console.log("GET a model with " + item.id);
});
},
error: function () {
console.log("Failed to GET");
}
});
},
render: function () {
var self = this;
this.$el.html('');
_.each(this.collection.toArray(), function (cv) {
self.$el.append((new ResumeView({model: cv})).render().$el);
});
return this;
}
});
The code above works perfectly and does exactly what I need -- an array of models is fetched from my local JSON-server and each model is displayed within a collection view. However, the trouble starts when I try to navigate through my link in the template above. Here comes the router:
var AppRouter = Backbone.Router.extend({
routes: {
'': home,
'resumes/:id': 'showResume'
},
initialize: function (options) {
// layout is set in main.js
this.layout = options.layout
},
home: function () {
this.layout.render(new ResumeList({collection: resumes}));
},
showResume: function (cv) {
this.layout.render(new ShowResume({model: cv}));
}
});
and finally the ShowResume view:
var ShowResume = Backbone.View.extend({
initialize: function (options) {
this.model = options.model;
this.template = _.template($('#full-resume').html());
},
render: function () {
this.$el.html(this.template(this.model.toJSON()));
}
});
I didn't provide the template for this view because it is quite large, but the error is following: whenever I try to navigate to a link, a view tries to render, but returns me the following error: Uncaught TypeError: this.model.toJSON is not a function. I suspect that my showResume method in router is invalid, but I can't actually get how to make it work in right way.
You are passing the string id of the url 'resumes/:id' as the model of the view.
This should solve it.
showResume: function (id) {
this.layout.render(new ShowResume({
model: new Backbone.Model({
id: id,
profession: "teacher" // you can pass data like this
})
}));
}
But you should fetch the data in the controller and react accordingly in the view.
var AppRouter = Backbone.Router.extend({
routes: {
'*otherwise': 'home', // notice the catch all
'resumes/:id': 'showResume'
},
initialize: function(options) {
// layout is set in main.js
this.layout = options.layout
},
home: function() {
this.layout.render(new ResumeList({ collection: resumes }));
},
showResume: function(id) {
// lazily create the view and keep it
if (!this.showResume) {
this.showResume = new ShowResume({ model: new Backbone.Model() });
}
// use the view's model and fetch
this.showResume.model.set('id', id).fetch({
context: this,
success: function(){
this.layout.render(this.showResume);
}
})
}
});
Also, this.model = options.model; is unnecessary as Backbone automatically picks up model, collection, el, id, className, tagName, attributes and events, extending the view with them.
I am using a plugin for dropdowns found here: http://patrickkunka.github.io/easydropdown/
I've got it working in Backbone but I had to activate it manually and make sure it runs after the render is complete. It works when I refresh the page but if i leave the page and then come back to it the plugin does not take effect. The render function is running when each time so i dont know why it wont work when im navigating normally.
render: function() {
setTimeout(function(){
$(function(){
var $selects = $('select');
$selects.easyDropDown({
cutOff: 5,
wrapperClass: 'dropdown',
onChange: function(selected){
// do something
}
});
});
}, 0);
console.log("Rendering");
this.$el.html(template());
return this;
}
Here is my router code:
return Backbone.Router.extend({
initialize: function() {
// Render the layout view only once and simple change the contents of #content
// as per the desired route
var $body = $('body');
var layoutView = new LayoutView({ el: $body }).render();
this.$el = $("#content", $body);
this.currentView = null;
// Init the subrouters
this.bookRouter = this.addSubRouter(BookRouter, "books");
this.quoteRouter = this.addSubRouter(QuoteRouter, "quotes");
this.employeeRouter = this.addSubRouter(EmployeeRouter, "employees");
this.saleRouter = this.addSubRouter(SaleRouter, "sales");
// When the route changes we want to update the nav
this.bind("route", _.bind(this.updateNav, this));
},
// These are the base routes
// Other routes can be attached by creating subroutes
routes: {
// viewIndex is the main site index
// All other routes are handled by sub-routers
"": "viewIndex",
"upload": "upload",
"export": "export",
"test": "test",
},
// Add a sub route at the given route and listen for events
addSubRouter: function(subRouterClass, route) {
var router = new (subRouterClass)(route, { createTrailingSlashRoutes: true });
router.on("view", _.bind(this.switchView, this));
router.on("route", _.bind(function(route, section) {
this.trigger("route", route, section);
}, this));
return router;
},
// Change from this.currentView to newView
switchView: function(newView) {
// Do we need to remove the old view?
if (this.currentView) {
this.currentView.remove();
}
this.currentView = newView;
// Add the new view
this.$el.append(newView.render().$el);
newView.addedToDOM();
},
updateNav: function(route, section) {
// Get hold of the nav element
var $nav = $("#nav");
// Clean up the route string
route = route.replace("route:", "");
// Remove the currently active item
$(".active", $nav).removeClass("active");
// Apply .active to any navigation item that has a matching data-route attribute
$("[data-route=\"" + route + "\"]", $nav).addClass("active");
},
viewIndex: function () {
var view = new IndexView();
this.switchView(view);
},
upload: function (){
var view = new UploadIndexView();
this.switchView(view);
},
export: function() {
var view = new ExportIndexView();
this.switchView(view);
},
test: function() {
var view = new TestIndexView();
this.switchView(view);
}
});
});
I am not far from it to get the file upload working with Ember-data. But I do not get the value binding right. Below the relevant code.
This is the App.js
App.LandcodeNewRoute = Ember.Route.extend({
model: function () {
return this.store.createRecord('landcode');
},
actions: {
saveLandcode: function () {
this.currentModel.save();
}
}
});
// REST & Model
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
App.Store = DS.Store.extend({
adapter: 'App.ApplicationAdapter'
});
App.Landcode = DS.Model.extend({
code: DS.attr('string'),
image: DS.attr('string')
});
// Views
App.UploadFile = Ember.TextField.extend({
tagName: 'input',
attributeBindings: ['name'],
type: 'file',
change: function (e) {
var reader, that;
that = this;
reader = new FileReader();
reader.onload = function (e) {
var fileToUpload = e.target.result;
console.log(e.target.result); // this spams the console with the image content
console.log(that.get('controller')); // output: Class {imageBinding: Binding,
that.get('controller').set(that.get('name'), fileToUpload);
};
return reader.readAsText(e.target.files[0]);
}
});
HTML
<script type="text/x-handlebars" data-template-name="landcode/new">
Code: {{input value=code}}<br />
Image: {{view App.UploadFile name="image" imageBinding="Landcode.image" }}
<button {{action 'saveLandcode'}}>Save</button>
</script>
As you can see in the HTML part is that I try to bind the imagecontent to the Landcode model attribute image. Tried it also without capital L.
I think I cant bind the image as such, because it is a custom view object? And also normally it would bind automatically I think. Maybe I am just doing some things twice.
References:
http://emberjs.com/api/classes/Ember.Binding.html
http://devblog.hedtek.com/2012/04/brief-foray-into-html5-file-apis.html
File upload with Ember data
How: File Upload with ember.js
http://discuss.emberjs.com/t/file-uploads-is-there-a-better-solution/765
http://chrismeyers.org/2012/06/12/ember-js-handlebars-view-content-inheritance-image-upload-preview-view-object-binding/
I updated your code to the following:
App.LandcodeNewRoute = Ember.Route.extend({
model: function () {
return this.store.createRecord('landcode');
},
actions: {
saveLandcode: function () {
this.currentModel.save();
}
}
});
// REST & Model
App.ApplicationAdapter = DS.RESTAdapter.extend({
namespace: 'api'
});
App.Landcode = DS.Model.extend({
code: DS.attr('string'),
image: DS.attr('string')
});
// views
App.UploadFile = Ember.TextField.extend({
tagName: 'input',
attributeBindings: ['name'],
type: 'file',
file: null,
change: function (e) {
var reader = new FileReader(),
that = this;
reader.onload = function (e) {
var fileToUpload = e.target.result;
Ember.run(function() {
that.set('file', fileToUpload);
});
};
return reader.readAsDataURL(e.target.files[0]);
}
});
In the App.UploadFile instead of reference the controller directlly, I set the file property. So you can bind your model property, with the view using:
{{view App.UploadFile name="image" file=image }}
The Ember.run is used to you don't have problems when testing the app.
Please give a look in that jsfiddle http://jsfiddle.net/marciojunior/LxEsF/
Just fill the inputs and click in the save button. And you will see in the browser console, the data that will be send to the server.
I found that using attaching a data URI to a model attribute didn't allow files more than about 60k to be uploaded. Instead I ended up writing a form data adapter for ember-data. This will upload all the model data using FormData. Sorry that it's in CoffeeScript rather than JS but it's pasted from my blog post. You can read the whole thing at http://blog.mattbeedle.name/posts/file-uploads-in-ember-data/
`import ApplicationAdapter from 'appkit/adapters/application'`
get = Ember.get
FormDataAdapter = ApplicationAdapter.extend
ajaxOptions: (url, type, hash) ->
hash = hash || {}
hash.url = url
hash.type = type
hash.dataType = 'json'
hash.context = #
if hash.data and type != 'GET' and type != 'DELETE'
hash.processData = false
hash.contentType = false
fd = new FormData()
root = Object.keys(hash.data)[0]
for key in Object.keys(hash.data[root])
if hash.data[root][key]
fd.append("#{root}[#{key}]", hash.data[root][key])
hash.data = fd
headers = get(#, 'headers')
if headers != undefined
hash.beforeSend = (xhr) ->
for key in Ember.keys(headers)
xhr.setRequestHeader(key, headers[key])
hash
`export default FormDataAdapter`
What is the best way to re-render a view after an event takes place (eg. submitting a note). In the below code, I want to re-render the view to show the note that was just added.
var NotesView = SectionBaseView.extend({
model: app.models.Note,
events: {
'submit': 'Notes'
},
Notes: function (e) {
e.preventDefault();
var details = new this.model($('form').serializeObject());
details.url = '/email/Notes';
details
.save()
.done(function () {
app.notifySuccess('Note added successfully.');
});
views['#Notes'].render();
}
});
Notes view is initialized in the document.ready function as follows:
views['#Notes'] = new NotesView({ el: '#Notes', template: app.templates.Notes });
I tried using views['#Notes'].render(); but this doesn't seem to be working.
The default implementation of render is a no-op. Override this function with your code that renders the view template from model data, and updates this.el with the new HTML. A good convention is to return this at the end of render to enable chained calls. Docs
var NotesView = SectionBaseView.extend({
model: app.models.Note,
events: {
'submit': 'Notes'
},
render : function(){
//your code
return this;
},
Notes: function (e) {
var that = this;
e.preventDefault();
var details = new this.model($('form').serializeObject());
details.url = '/email/Notes';
details
.save()
.done(function () {
app.notifySuccess('Note added successfully.');
});
that.render();
}
});
on document.ready
views['#Notes'] = new NotesView({ el: '#Notes', template: app.templates.Notes });
views['#Notes'].render();