I need to create an Ember component to select a file.
My page will include multiple "upload component"
I have read a post trying to implement that: (https://stackoverflow.com/questions/9200000/file-upload-with-ember-data) BUT the UploadFileView is directly linked to the controller.
I would like to have something more generic...
I would like to remove the App.StoreCardController.set('logoFile'..) dependency from the view or pass the field (logoFile) from the template...
Any idea to improve this code ?
App.UploadFileView = Ember.TextField.extend({
type: 'file',
attributeBindings: ['name'],
change: function(evt) {
var self = this;
var input = evt.target;
if (input.files && input.files[0]) {
App.StoreCardController.set('logoFile', input.files[0]);
}
}
});
and the template:
{{view App.UploadFileView name="icon_image"}}
{{view App.UploadFileView name="logo_image"}}
I completed a full blown example to show this in action
https://github.com/toranb/ember-file-upload
Here is the basic handlebars template
<script type="text/x-handlebars" data-template-name="person">
{{view PersonApp.UploadFileView name="logo" contentBinding="content"}}
{{view PersonApp.UploadFileView name="other" contentBinding="content"}}
<a {{action submitFileUpload content target="parentView"}}>Save</a>
</script>
Here is the custom file view object
PersonApp.UploadFileView = Ember.TextField.extend({
type: 'file',
attributeBindings: ['name'],
change: function(evt) {
var self = this;
var input = evt.target;
if (input.files && input.files[0]) {
var reader = new FileReader();
var that = this;
reader.onload = function(e) {
var fileToUpload = reader.result;
self.get('controller').set(self.get('name'), fileToUpload);
}
reader.readAsDataURL(input.files[0]);
}
}
});
Here is the controller
PersonApp.PersonController = Ember.ObjectController.extend({
content: null,
logo: null,
other: null
});
And finally here is the view w/ submit event
PersonApp.PersonView = Ember.View.extend({
templateName: 'person',
submitFileUpload: function(event) {
event.preventDefault();
var person = PersonApp.Person.createRecord({ username: 'heyo', attachment: this.get('controller').get('logo'), other: this.get('controller').get('other') });
this.get('controller.target').get('store').commit();
}
});
This will drop 2 files on the file system if you spin up the django app
EDIT (2015.06): Just created a new solution based on a component.
This solution provides an upload button with a preview and remove icon.
P.S. The fa classes are Font Awesome
Component handlebars
<script type="text/x-handlebars" data-template-name='components/avatar-picker'>
{{#if content}}
<img src={{content}}/> <a {{action 'remove'}}><i class="fa fa-close"></i></a>
{{else}}
<i class="fa fa-picture-o"></i>
{{/if}}
{{input-image fdata=content}}
</script>
Component JavaScript
App.AvatarPickerComponent = Ember.Component.extend({
actions: {
remove: function() {
this.set("content", null);
}
}
});
App.InputImageComponent = 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;
that.set('fdata', data);
};
reader.readAsDataURL(input.files[0]);
}
}
});
Usage example
{{avatar-picker content=model.avatar}}
Old Answer
I took Chris Meyers example, and I made it small.
Template
{{#view Ember.View contentBinding="foto"}}
{{view App.FotoUp}}
{{view App.FotoPreview width="200" srcBinding="foto"}}
{{/view}}
JavaScript
App.FotoPreview= Ember.View.extend({
attributeBindings: ['src'],
tagName: 'img',
});
App.FotoUp= 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;
that.set('parentView.content', data);
}
reader.readAsDataURL(input.files[0]);
}
},
});
Marek Fajkus you cannot use JQuery's .serialize, it makes no mention of file uploads in the documentation at JQuery UI docs
However, you could use JQuery Upload Plugin
Actually it does mention it, it says:
". Data from file select elements is not serialized."
In case of uploading multiple files, you may want to use
{{input type='file' multiple='true' valueBinding='file'}}
^^^^
This is a solution that you would use in normal HTML upload.
Additionally, you can use 'valueBinding' which will allow you to set up an observer against that value in your component.
Related
Caveat: This is part of my first ember app.
I have an Ember.MutableArray on a controller. The corresponding view has an observer that attempts to rerender the template when the array changes. All the changes on the array (via user interaction) work fine. The template is just never updated. What am I doing wrong?
I'm using Ember 1.2.0 and Ember Data 1.0.0-beta.4+canary.7af6fcb0, though I guess the latter shouldn't matter for this.
Here is the code:
var ApplicationRoute = Ember.Route.extend({
renderTemplate: function() {
this._super();
var topicsController = this.controllerFor('topics');
var topicFilterController = this.controllerFor('topic_filter');
this.render('topics', {outlet: 'topics', controller: topicsController, into: 'application'});
this.render('topic_filter', {outlet: 'topic_filter', controller: topicFilterController, into: 'application'});
},
});
module.exports = ApplicationRoute;
var TopicFilterController = Ember.Controller.extend({
topicFilters: Ember.A([ ]),
areTopicFilters: function() {
console.log('topicFilters.length -> ' + this.topicFilters.length);
return this.topicFilters.length > 0;
}.property('topicFilters'),
getTopicFilters: function() {
console.log('getTopicFilters....');
return this.store.findByIds('topic', this.topicFilters);
}.property('topicFilters'),
actions: {
addTopicFilter: function(t) {
if(this.topicFilters.indexOf(parseInt(t)) == -1) {
this.topicFilters.pushObject(parseInt(t));
}
// this.topicFilters.add(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
},
removeTopicFilter: function(t) {
this.topicFilters.removeObject(parseInt(t));
console.log('topicFilters -> ' + JSON.stringify(this.topicFilters));
}
}
});
module.exports = TopicFilterController;
var TopicFilterView = Ember.View.extend({
topicFiltersObserver: function() {
console.log('from view.... topicFilters has changed');
this.rerender();
}.observes('this.controller.topicFilters.[]')
});
module.exports = TopicFilterView;
// topic_filter.hbs
{{#if areTopicFilters}}
<strong>Topic filters:</strong>
{{#each getTopicFilters}}
<a {{bind-attr href='#'}} {{action 'removeTopicFilter' id}}>{{topic}}</a>
{{/each}}
{{/if}}
var TopicsController = Ember.ArrayController.extend({
needs: ['topicFilter'],
all_topics: function() {
return this.store.find('topic');
}.property('model', 'App.Topic.#each'),
actions: {
addTopicFilter: function(t) {
App.__container__.lookup('controller:topicFilter').send('addTopicFilter', t);
}
}
});
module.exports = TopicsController;
// topics.hbs
<ul class="list-group list-unstyled">
{{#each all_topics}}
<li class="clear list-group-item">
<span class="badge">{{entryCount}}</span>
<a {{bind-attr href="#"}} {{action 'addTopicFilter' id}}>{{topic}}</a>
</li>
{{/each}}
</ul>
your observes should just be controller.topicFilters.[]
And honestly this is a very inefficient way of doing this, rerendering your entire view because a single item changed on the array. If you show your template I can give you a much better way of handling this.
Here's an example, I've changed quite a few things, and guessed on some others since I don't know exactly how your app is.
http://emberjs.jsbin.com/uFIMekOJ/1/edit
I would like to be able to define the model for a component template inside the Ember.Component js instead of inside the route where the component is sitting. I have not seen any examples which are doing this...
Here I have my component template:
<script type="text/x-handlebars" id="components/info-box">
<div class="infoBox box">
<p>
<label>
{{preUnits}}
</label>
<span>
{{value}}
</span>
</p>
</div>
</script>
And here is how I am placing it inside one route template:
{{info-box title='Total Area' dataDef='buddhaData:DataGet/site/areaNum'}}
What I would like to do is use my relevant Ember.Component to do some stuff with the parameters of the info-box and then return a model for it.
App.InfoBoxComponent = Ember.Component.extend({
buildIt: function(){
var container = $('#' + this.get('elementId') );
var title = this.get('title');
var preUnits = this.get('preUnits') || '';
var dataDef = this.get('dataDef');
// Do stuff with dataDef.
var model = {
preUnits: '$',
value: 5000
}
// Hopefully return model somehow.
},
didInsertElement: function(){
this.buildIt();
}
});
I want to be able to use this component inside a bunch of different routes, and I do not want to have to refer to the route that a particular info-box is inside of in order to give the info-box its model, is this possible, or should I use some other feature, like a regular template and the render helper?
Once you have the model object, just set properties on the component itself:
App.InfoBoxComponent = Ember.Component.extend({
buildIt: function(){
var container = $('#' + this.get('elementId') );
var title = this.get('title');
var preUnits = this.get('preUnits') || '';
var dataDef = this.get('dataDef');
// Do stuff with dataDef.
var model = {
preUnits: '$',
value: 5000
}
// Set component's preUnits and value properties directly
this.setProperty('preUnits', model.preUnits);
this.setProperty('value', model.value);
// or
this.setProperties(model);
// Hopefully return model somehow.
},
didInsertElement: function(){
this.buildIt();
}
});
You should use render if you'd like to define which model you want to use (if the model is different than the current context). If it's the same context, you should just use partials. You could also generate helper and pass in the model to that.
Ember.Handlebars.helper('autocomplete', Ember.View.extend({
templateName: 'controls/autocomplete',
filteredList: function() {
var list = this.get('list'),
filter = this.get('filter');
if (!filter) { return list; }
return list.filter(function(item) {
return item.name.indexOf(filter) !== -1;
});
}.property('list.[]', 'filter')
}));
Usage:
<script type="text/x-handlebars" data-template-name="application">
{{autocomplete list=list1}}
{{autocomplete list=list2}}
</script>
<script type="text/x-handlebars" data-template-name="controls/autocomplete">
<p>{{input type="text" value=view.filter}}</p>
<ul>
{{#each view.filteredList}}
<li >{{name}}</li>
{{/each}}
</ul>
</script>
Full example
I am trying to create autocomplete behavior with emberjs. I am observing in the controller an input field (searchCity) and making ajax calls according to that field. I have a view which handles additional logic, and should display a list of clickable cities as suggestions. So far I have the following code:
var labelView = Ember.View.extend({
templateName: 'searchedCities',
geoData: null,
click:function(){
debugger;
}
}),
ModalView = Ember.View.extend({
layoutName: 'modal_layout',
cities: null,
didInsertElement: function() {
this.cities = Ember.CollectionView.create({
tagName: 'span',
itemViewClass: labelView
});
},
cityList: function(){
var callback,
cities = this.get('cities'),
searchCity = this.get('controller').get('searchCity'),
regExp = new RegExp(searchCity, 'i');
if (!searchCity){
return;
}
callback = function(data){
var aux = data.filter(function(geoObjects){
return geoObjects.city.match(regExp);
}).slice(0,9);
cities.clear();
aux.forEach(function(geoData){
cities.pushObject(labelView.create({geoData:geoData}));
});
};
Store.resolveGeoData(callback);
}.property('controller.searchCity')
});
In my template I have the following code:
{{view Ember.TextField valueBinding=searchCity placeholder="City"}}
{{#each view.cities}}
{{this.geoData.city}}
{{/each}}
{{#each view.cityList}}
{{this.city}}
{{/each}}
Even though in the callback I can check that the cities are populated with the views in my template nothing is shown if i remove the {{#each view.cityList}}{{/each}}, but it displays if I leave it there.
I'm trying to use Handlebars helper, but the helper view does not get updated.
The view,
<script type="text/x-handlebars">
{{#view App.NumberView}}
<button {{action changeNumber}}>Change number</button><br>
{{formatNumber view.number}} <br>
{{view.number}}
{{/view}}
</script>
The code,
App = Ember.Application.create({});
App.NumberView = Ember.View.extend({
number: 5,
changeNumber: function(e) {
this.set('number', this.get('number') + 1);
}
});
Em.Handlebars.registerHelper('formatNumber', function(timePath, options) {
var number = Em.Handlebars.getPath(this, timePath, options);
return new Handlebars.SafeString("Formated number: " + number);
});
The live example in jsfiddle http://jsfiddle.net/LP7Hz/1/
So what's wrong ?
Because Handlebars helpers in Ember which aren't bound helpers. So you can instantiate an auxiliary view to do that like this fiddle:
http://jsfiddle.net/secretlm/qfNJw/2/
HTML:
<script type="text/x-handlebars">
{{#view App.NumberView}}
<button {{action changeNumber}}>Change number</button><br>
{{formatNumber view.number}} <br>
{{view.number}}
{{/view}}
</script>
Javascript:
App = Ember.Application.create({});
App.NumberView = Ember.View.extend({
number: 5,
changeNumber: function(e) {
this.set('number', this.get('number') + 1);
}
});
App.registerViewHelper = function(name, view) {
Ember.Handlebars.registerHelper(name, function(property, options) {
options.hash.contentBinding = property;
return Ember.Handlebars.helpers.view.call(this, view, options);
});
};
inlineFormatter = function(fn) {
return Ember.View.extend({
tagName: 'span',
template: Ember.Handlebars.compile('{{view.formattedContent}}'),
formattedContent: (function() {
if (this.get('content') != null) {
return fn(this.get('content'));
}
}).property('content')
});
};
App.registerViewHelper('formatNumber', inlineFormatter(function(content) {
return new Handlebars.SafeString("Formated number: " + content);
}));
This link is useful: http://techblog.fundinggates.com/blog/2012/08/ember-handlebars-helpers-bound-and-unbound/ from #Jo Liss
You're looking for a bound helper, which doesn't exist just yet. There is a Pull Request and associated discussion.
I'm looking for advice on how to trigger this view function insertNewLine from a button (see view and template below). I'm guessing there's probably a better way to structure this code. Thanks for your help.
// view
App.SearchView = Ember.TextField.extend({
insertNewline: function() {
var value = this.get('value');
if (value) {
App.productsController.search(value);
}
}
});
// template
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search"}}
<button id="search-button" class="btn primary">Search</button>
</script>
You could use the mixin Ember.TargetActionSupport on your TextField and execute triggerAction() when insertNewline is invoked. See http://jsfiddle.net/pangratz666/zc9AA/
Handlebars:
<script type="text/x-handlebars">
{{view App.SearchView placeholder="search" target="App.searchController" action="search"}}
{{#view Ember.Button target="App.searchController" action="search" }}
Search
{{/view}}
</script>
JavaScript:
App = Ember.Application.create({});
App.searchController = Ember.Object.create({
searchText: '',
search: function(){
console.log('search for %#'.fmt( this.get('searchText') ));
}
});
App.SearchView = Ember.TextField.extend(Ember.TargetActionSupport, {
valueBinding: 'App.searchController.searchText',
insertNewline: function() {
this.triggerAction();
}
});