In my ember app I want to reuse a model attribute as soon as the form is submitted. But the store seems to keep it as string unless I reload the whole route. I am using this and the following components:
Ember : 1.12.0
Ember Data : 1.0.0-beta.18
jQuery : 1.11.3
/app/models/purchase.js
import DS from 'ember-data';
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
.. other callback and associations..
});
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function () {
return {
newPurchase: this.store.createRecord('purchase', {
name: null,
amount: null,
player: null
})
}
}
});
/app/templates/ledger/purchases/new.hbs
<div class="row">
<div class="col-xs-12">
<h4>New purchase</h4>
<form>
<div class="form-group">
<label for="name" class="sr-only control-label">name</label>
{{input id='name' type="text" value=newPurchase.name placeholder="What" class="form-control"}}
</div>
<div class="form-group">
<label for="amount" class="sr-only control-label">amount</label>
{{input id='amount' type='number' value=newPurchase.amount placeholder="How much" class="form-control"}}
</div>
<div class="form-group">
<button type="submit" class="btn btn-success" {{action "create"}}>create</button>
{{#link-to 'ledger.purchases' tagName="button" class="btn btn-link" }}cancel{{/link-to}}
</div>
</form>
</div>
</div>
/app/controllers/ledger/purchases/new.js
import Ember from 'ember';
export default Ember.Controller.extend({
newPurchase: Ember.computed.alias('model.newPurchase'),
actions: {
create: function() {
var np = this.get('newPurchase');
console.log(Ember.typeOf(np.get('amount')));
........
save np etc...
}
}
});
the console log call clearly shows that the type is a string. The ember inspector shows the same. However data are correctly saved to the backend because after reloading everything is fine. But I need the amount as a number as soon as it is submitted because I use it to make and show the sum of all purchases.
Okay, I think I know what's going on. Setting input type to number won't help here. Value is still recognized as string. Usually when you submit form, backend anyway returns this value formatted as a number and problem's gone. You can see this even when you mock your data with a number, without a backend.
My solution would be to use a computed property for input component. Model:
export default DS.Model.extend({
name: DS.attr('string'),
amount: DS.attr('number'),
createdAt: DS.attr('date', {
defaultValue: function() { return new Date(); }
}),
amountAsNum: Ember.computed('amount', {
get: function () {
return parseFloat(this.get('amount'));
},
set: function (key, value) {
var valueToSet = parseFloat(value);
this.set('amount', valueToSet);
return valueToSet;
}
}),
.. other callback and associations..
});
Template:
{{input id='amount' type='number' value=newPurchase.amountAsNum placeholder="How much" class="form-control"}}
Now, you can check typeof(amount) before save and it'll give you number. Demo on JS Bin.
Related
I have been struggling with making my Ember Application work with Firebase.
I went through all the posts here on Stackoverflow about the similar matter but I did not find the answer to my problem. So here it is:
Whenever I try to put data into input fields and submit them with a button i get the console error:
EmberError
code : undefined
description : undefined
fileName : undefined
lineNumber : undefined
message :
"Nothing handled the action 'createBook'. If you did handle the action, this error can be caused by returning true from an action handler in a controller, causing the action to bubble."
My model:
import DS from 'ember-data';
export default DS.Model.extend({
title: DS.attr('string'),
author: DS.attr('string'),
picture: DS.attr('string'),
buyer: DS.attr('string'),
bought: DS.attr('boolean', { defaultValue: false }),
createdAt: DS.attr('date', { defaultValue() { return new Date(); } })
});
And my Controller:
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
createBook: function(){
var newBook = this.store.createRecord('book', {
title: this.get('title'),
author: this.get('author'),
picture: this.get('picture'),
buyer: this.get('buyer'),
bought: this.set(false),
createdAt: new Date().getTime()
});
newBook.save();
//reset values after create
this.setProperties({'title' : '',
'author' : '',
'picture' : '',
'buyer' : ''
});
}
}
});
The template:
{{outlet}}
<div style ="margin-left:130px;">
<h1> blabla </h1>
{{input type="text" value=title placeholder="Add Title"}}<br>
{{input type="text" value=author placeholder="Add author"}}<br>
{{input type="text" value=picture placeholder="Add picture"}}<br>
{{input type="text" value=buyer placeholder="buyer"}}<br>
</div>
<button class="btn btn-default" {{action "createBook" }}> Create</button>
{{#each model as |book|}}
<ul>
<li>{{book.title}}</li>
</ul>
{{/each}}
The connection between the Firebase and Ember is set up 100 % properly.
For now the rules on firebase have been set to true for both read and write.
The only problem is that it does not post the data to Firebase.
Thanks #Lux for your advice.
There were several things wrong with my code.
I created model and controller called book, but route called books.
I did not know that it will have an effect on my model and controller.
So i ended up with:
app/controllers/book.js
app/model/book.js
app/routes/book.js
app/templates/book.hbs
This was not enough. I also had to edit content of my controller
import Ember from 'ember';
export default Ember.Controller.extend({
actions: {
createBook: function(){
var newBook = this.store.createRecord('book', {
title: this.get('title'),
author: this.get('author'),
picture: this.get('picture'),
buyer: this.get('buyer')
});
newBook.save();
//reset values after create
this.setProperties({'title' : '',
'author' : '',
'picture' : '',
'buyer' : ''
});
}
}
});
As you can see I have removed the lines that were setting the default values of bought and createdAt. It was enough just to set them inside the model itself.
Binding to a model relationship property fails in the each helper as demonstrated below:
Here are my models:
//app/models/category.js
export default DS.Model.extend({
name: DS.attr(),
image: DS.belongsTo('image', { async: true }),
});
//app/models/image.js
export default DS.Model.extend({
name: DS.attr('string'),
thumbfullfilepath: DS.attr('string'),
category: DS.belongsTo('category', { async: true })
});
When I run the category model in the each handlebars helper below to retrieve the 'thumbfullfilepath' for an image tag, no value is bound to the img src:
{{#each model as |category|}}
<div class="small-element item">
<div class="cat-name">{{category.name}}</div>
<div class="cat-name edit">{{#link-to 'admin.categories.edit' category}}Edit{{/link-to}}</div>
<span class="entry-thumb">
<img src={{category.image.thumbfullfilepath}} alt="">
</span>
</div>
{{/each}}
However, I have verified the relationship binding works on display of a single model as when I visit the "admin.categories.edit" route which loads a single category model, the {{category.image.thumbfullfilepath}} path is retrieved and reflected in the template. This has led me to believe that for some reason, model relationship bindings fail in the each handlebars helper within templates.
Would someone shed some light here.
## The solution that has worked for me
I created an image component "image-atom" whose component.js is as below:
//pods/components/image-atom.js
export default Ember.Component.extend({
tagName: 'img',
attributeBindings: ['src', 'alt'],
alt: '',
src: Ember.computed(function () {
this.get('source').then((image) => {
this.set('src', image.get('thumbfullfilepath'));
});
return null;
})
});
Which I use like so here below and it works but it feels hacky:
{{#each model as |category|}}
<div class="small-element item">
<div class="cat-name">{{category.name}}</div>
<span class="entry-thumb">
{{image-atom source=category.image alt=""}}
</span>
</div>
{{/each}}
Here below are the environment details:
ember cli version: "2.2.0-beta.2"
ember-data: "^2.2.1"
ember: "2.2.0"
node: "0.12.7"
npm: "2.14.10"
os: "darwin x64 El Capitan"
Let me know.
You might have a better time simply wrapping an {{if helper around you image.
<span class="entry-thumb">
{{#if category.image.thumbfullfilepath}}
<img src={{category.image.thumbfullfilepath}} alt="">
{{/if}}
</span>
the issue is likely the image.thumbfullfilepath is not resolved before the image is trying to render, becuase it is an async promise.
Edit: For the record, working with promises in a computed property in your example, is not recommended. It might be more headache than anything.
I have ember data models hooked with firebase, characters and spells. I can create new models and save them to firebase. Now I wanted to add spells to character. I defined that character has many spells:
export default DS.Model.extend({
chClass: DS.attr(),
chName: DS.attr(),
chImage: DS.attr(),
chSpells: DS.hasMany('spell', {async: true}),
});
In my hbs I listed spells in <select> element, there is also input fields and add button.
Add new character <br>
name {{input value=mchName }}<br>
class {{input value=mchClass }}<br>
image {{input value=mchImage }}<br>
<br>
Choose Spells:<br>
<select name="spellslist" multiple>
{{#each spells as |spell index|}}
<option value="{{index}}">{{spell.spName}}</option>
{{/each}}
</select>
<button {{action 'addChar' spells}}>add</button><br>
So when user types in character name, level and picks some spells I want to call addChar action function on add button and pass this data.
export default Ember.Controller.extend({
mchName:'',
mchClass:'',
mchImage:'',
store: Ember.inject.service(),
actions: {
addChar: function(spells) {
var newChar = this.store.createRecord('character');
newChar.set("chName", this.mchName);
newChar.set("chClass", this.mchClass);
newChar.set("chImage", this.mchImage);
newChar.get("chSpells").addObject(?????? how to get spell here ?????);
newChar.save();
I know how to pass string from inputs, but I dont know how to pass selected spells to this function, its killing me.
I'm assuming that you (as admin) are going to populate the spells table. Now ... assuming that a character can have many spells and a spell can have many characters, here's how one can approach this (note that I'm using a controller ... you should ideally be doing this in a component):
Character model is simplified:
//app/models/character.js
import DS from 'ember-data';
export default DS.Model.extend({
chName: DS.attr(),
chSpells: DS.hasMany('spell', {async: true})
});
Spells model is also simplified for this example:
//app/models/spell.js
import DS from 'ember-data';
export default DS.Model.extend({
spName: DS.attr(),
spellChar: DS.hasMany('character', {async: true})
});
We need an include helper for the multiline select. Review this article for details:
//app/helpers/include.js
import Ember from 'ember';
export function include(params/*, hash*/) {
const [items, value] = params;
return items.indexOf(value) > -1;
}
export default Ember.Helper.helper(include);
Here's the application route:
app/routes/application.js
import Ember from 'ember';
export default Ember.Route.extend({
model: function(){
var spells = this.store.findAll('spell');
return spells;
}
});
And the application controller:
//app/controllers/application.js
import Ember from 'ember';
export default Ember.Controller.extend({
selectedSpellIds: [],
actions: {
selectSpell(event){
const selectedSpellIds = Ember.$(event.target).val();
this.set('selectedSpellIds', selectedSpellIds || []);
},
addChar: function(){
var charName = this.get('mchName');
var _this = this;
var spells = this.get('selectedSpellIds');
var spellObjArray = spells.map(function(spellId){
return _this.store.peekRecord('spell', spellId );
});
var charToSave = this.store.createRecord('character', {
chName: charName,
chSpells: spellObjArray
});
charToSave.save();
},
}
});
And the application template:
//app/templates/application.hbs
Add new character <br>
name {{input value=mchName }}<br>
<br>
Choose Spells:<br>
<select multiple onchange={{action "selectSpell"}}>
{{#each model as |spell|}}
<option value={{spell.id}} selected={{include selectedSpellIds spell.id}}>{{spell.spName}}</option>
{{/each}}
</select>
<button {{action 'addChar'}}>add</button><br>
I'm using EmberJS 1.13. I want to show username (on application.hbs) when an user is logged in.
templats/application.hbs
<div id="main-frame" class="container-fluid">
<div id="page-header">
<img src="/assets/logo-main.png">
{{#if isLoggedIn}}
<span>{{currentUser.displayName}}</span>
{{else}}
<a id="loginButton" class="text-button" data-toggle="modal" data-target="#loginWindow">Войти</a>
{{/if}}
</div>
<div id="content-frame" class="container">
<ul class="nav nav-pills">
<li>{{#link-to 'builder'}}Конструктор{{/link-to}}</li>
<li>Мой бар</li>
<li>Админ-панель</li>
</ul>
<hr>
<hr>
{{outlet}}
</div>
</div>
{{login-window authSuccess="authSuccess"}}
{{signup-window}}
routes/application.js
import Ember from 'ember';
import ApplicationRouteMixin from 'ember-simple-auth/mixins/application-route-mixin';
export default Ember.Route.extend(ApplicationRouteMixin, {
currentUser: Ember.inject.service('current-user'),
isLoggedIn: Ember.computed.bool('currentUser.isAuthenticated'),
actions: {
authSuccess: function(userInfo) {
this.get('currentUser').setUser(userInfo);
}
}
});
services/current-user.js
import Ember from 'ember';
export default Ember.Service.extend({
session: Ember.inject.service("session"),
username: "",
password: "",
displayName: "",
accessLevel: -1,
isLoggedIn: false,
isAuthenticated: function () {
return this.get("isLoggedIn") && this.get('session').isAuthenticated;
}.property('isLoggedIn'),
setUser: function(userInfo) {
this.set("username", userInfo.username);
this.set("password", userInfo.password);
this.set("displayName", userInfo.displayName);
this.set("accessLevel", userInfo.accessLevel);
this.set("isLoggedIn", true);
}
});
I have next behavior: currentUser.setUser() called, currentUser.isLoggedIn setted, but currentUser.isAuthenticated doesn't recalculates and no username in html. What I do wrong?
UPDATE:
I also tried different ways implement isLoggedIn property:
isAuthenticatedObserver: Ember.on('init', Ember.observer('isLoggedIn', function () {
this.set('isAuthenticated', this.get("isLoggedIn") && this.get('session').isAuthenticated);
}))
////
isLoggedIn: function() {
return this.get('currentUser').isAuthenticated;
}.observes('currentUser.isAuthenticated')
////
isLoggedIn: function() {
return this.get('currentUser').isAuthenticated;
}.property('currentUser.isAuthenticated')
But nothing changed.
I got answer at ember forum. So all properties that was used in template must be in controller but not in route.
How can I force Ember to update a template when a child record is added/removed to my model?
Customer model
Docket.Customer = DS.Model.extend({
name: DS.attr('string'),
initial: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
projects: DS.hasMany('project',{ async: true })
});
Project model
Docket.Project = DS.Model.extend({
name: DS.attr('string'),
description: DS.attr('string'),
number: DS.attr('string'),
archived: DS.attr('boolean'),
customer: DS.belongsTo('customer', { async: true })
});
When a project is added/deleted, this template should be updated:
{{#each filteredProjects}}
<h2>Customer: {{customer.name}}</h2>
<ul class="entries">
{{#each projects}}
<li>
<div class="actions">
<button {{action "remove" id}} class="icon-close"></button>
</div>
<div class="link" {{action "edit" id}} data-uk-modal="{target:'#project-modal'}">
<span class="before">{{number}}</span>{{name}}
</div>
</li>
{{else}}
<li>No projects</li>
{{/each}}
</ul>
{{/each}}
Example actions (extract)
remove: function (id) {
this.get('store').find('project', id).then(function (data) {
data.deleteRecord();
data.save();
});
},
save: function() {
// create new record
var project = this.store.createRecord('project', _this.getProperties('name', 'number', 'description', 'archived'));
// set customer
project.set('customer', this.get('selectedCustomer'));
// validate and save if validation passes, otherwise show errors
project.save().then(function () {
_this.closeForm();
}, function (response) {
_this.set('errors', response.errors);
});
}
Update 2
I openend an issue here, but it hasn't been resolved until now.
Your problem is, because you are using map to group the data, the returned array isn't a DS.RecordArray instance, so when a item is added or removed, the content isn't updated.
I think the easy way to handle it, is to reload the data, when a item is added or removed. So extract the method that load the data and call it in the save and remove action. Here I created a loadData method:
route
Docket.OrganizationProjectsIndexRoute = Docket.AuthenticatedRoute.extend({
setupController: function() {
this.loadData();
},
loadData: function () {
var projectsController = this.controllerFor('organization.projects');
this.store.find('customer').then(function(customers) {
var promises = customers.map(function(customer) {
return Ember.RSVP.hash({
customer: customer,
projects: customer.get('projects').then(function(projects) {
return projects.filter(function(project) {
return !project.get('archived');
});
});
});
});
Ember.RSVP.all(promises).then(function(filteredProjects) {
projectsController.set('filteredProjects', filteredProjects);
});
});
},
actions: {
remove: function (project) {
var _this = this;
project.destroyRecord().then(function() {
_this.loadData();
});
},
save: function() {
// create new record
var project = this.store.createRecord('project', _this.getProperties('name', 'number', 'description', 'archived'));
// set customer
project.set('customer', this.get('selectedCustomer'));
// validate and save if validation passes, otherwise show errors
projects.save().then(function () {
_this.closeForm();
_this.loadData();
}, function (response) {
_this.set('errors', response.errors);
});
}
}
});
template
{{#each filteredProjects}}
<h2>Customer: {{customer.name}}</h2>
<ul class="entries">
{{#each projects}}
<li>
<div class="actions">
<button {{action "remove" this}} class="icon-close"></button>
</div>
<div class="link" {{action "edit" this}} data-uk-modal="{target:'#project-modal'}">
<span class="before">{{number}}</span>{{name}}
</div>
</li>
{{else}}
<li>No projects</li>
{{/each}}
</ul>
{{/each}}
Some tips:
You can use project.destroyRecord() instead of project.deleteRecord() project.save().
You can pass the project instance directlly to the action using {{action "remove" this}} instead of the id {{action "remove" id}} so no need to reload using:
this.get('store').find('project', id)...
I hope it helps