I'm getting my feet wet with a bit of Ember.js. I'm trying to create a super simple form that lets you submit a query.
App = Ember.Application.create();
App.QueryFormView = Ember.View.extend({
submitQuery: function () {
App.queries.pushObject({
firstName: this.get('name'),
message: this.get('message')
});
App.queries.save();
}
});
// Model
App.Query = Ember.Object.extend();
// Collection
App.queries = Ember.ArrayController.create({
content: [],
save: function () {
$.post('api/query', JSON.stringify(this.toArray()), function (data) {
// Queries saved
});
}
});
Each time the query form it submitted, I push an object to the queries ArrayController, and then run save.
However, I'm struggling to understand where the Ember.Object aka model comes into play. It's not being used at all here, and I'd like to know how to properly utilise it.
You don't have to use Ember.Object. If you never want to do any bindings, have calculated properties or observe any property changes, there's no need.
However if you do want to do any of those things you'd modify your code like this:
Document expected fields in the model.
// Model
App.Query = Ember.Object.extend({
firstName: null, // just to indicate what props you're expecting
lastName: null
});
Create your model object instead of anonymous object.
submitQuery: function () {
App.queries.pushObject(App.Query.create({ // .create() here
firstName: this.get('name'),
message: this.get('message')
});
App.queries.save();
}
And now for the big drawback. JSON.stringify() will serialize internal stuff you don't want. So each object sent over the wire must be simplified to the properties you want first. Help with this can be found here: Reflection on EmberJS objects? How to find a list of property keys without knowing the keys in advance
save: function () {
var obj = buildSimpleObject(this); // implements this somehow
$.post('api/query', JSON.stringify(obj.toArray()), function (data) {
// Queries saved
});
}
Related
I have a custom shopping cart object that I created and put it in the lib folder.
ShoppingCart = function ShoppingCart() {
this.Items = new Array();
this.grandTotal = 0.00;
}
ShoppingCart.prototype.addItem = function(Item){
this.Items.push(Item);
this.Items.sort();
this.calculateTotal();
}
I initialized the shopping cart and store it as Session.set('shoppingCart') during the page created phase.
Template.loginStatus.created = function() {
Session.set('loginShown',false);
if(!Session.get('shoppingCart')){ //set default if session shopping cart not exist
var cart = new ShoppingCart();
Session.setDefault('shoppingCart',cart);
}
Then when user click add item to cart, it will trigger this logic:
var cart = Session.get('shoppingCart');
cart.addItem(item);
Session.set('shoppingCart',cart);
Somehow, it does not work. When I take a look ad the chrome console it says undefined is not a function, pointing at cart.addItem(item) line. If I change it to this, it will work , but of course since everytime new shopping cart is created, I cannot accumulate items in the cart.
var cart = new ShoppingCart();
cart.addItem(item);
Session.set('shoppingCart',cart);
How should I store and retrieve the object from session properly? It looks like the returned object from the Session.get() somehow not considered as ShoppingCart. Did I miss any type cast?
As #Peppe L-G mentioned, you can only store EJSONs in Session. To store your custom object, you need to be able to manually transform it to and from EJSONs. Example:
_.extend(ShoppingCart, {
fromJSON: function(json) {
var obj = new ShoppingCart();
obj.grandTotal = json.grandTotal;
obj.Items = json.Items;
return obj;
},
});
_.extend(ShoppingCart.prototype, {
toJSON: function() {
return {
grandTotal: this.grandTotal,
Items: this.Items,
};
},
});
Then you can save it to Session:
Session.set('shoppingCart', cart.toJSON());
and restore:
ShoppingCart.fromJSON(Session.get('shoppingCart'));
I ran into the same problem. Essentially what is happening Meteor Sessions (and Collections) can only store EJSON types, so your ShoppingCart custom type is retrieved from the Session as a normal Object.
While you can manually transform to and from EJSONs, you may end up needing to do this repeatedly in a lot of different places. If your ShoppingCart is a member of another object, you'll have to also manually transform the member. It's better to use EJSON.addType to tell Meteor how to handle it automatically anywhere you store or retrieve an object of that type.
There's a great demo of this here: https://www.eventedmind.com/feed/meteor-create-a-custom-ejson-type. Full docs are also here: http://docs.meteor.com/#/full/ejson. But a short version is this:
Add a method to your custom type called typeName:
ShoppingCart.prototoype.typeName = function(){
return "ShoppingCart";
};
Add another method called toJSONValue:
ShoppingCart.prototype.toJSONValue = function(){
/* return a JSON compatible version of your object */
};
And finally, add the custom type to EJSON with:
EJSON.addType("ShoppingCart", function fromJSONValue(value){
/* return an object of your custom type from the JSON object 'value' */
};
NOTE: the "Type Name" in steps 1 and 3 must match exactly.
So I've got a model that has a field of an array objects it looks like this
App.Post = DS.Model.extend({
...
codes: attr(),
...
});
and Codes looks like this
codes: [
{
code: stuff
comment: stuff_1
other_things: other_stuff
},
{
...
},
{
...
}
...
]
So now I have an add / remove button which has actions attached to them and this is what they do
add_code_input: function() {
var codes = this.get('model.codes');
var self = this;
var last_code = codes[codes.length-1];
// Cannot edit as an ember.set error is occurring
last_code.code = 'Add new (please change)';
last_code.code_type = "";
last_code.comment = "";
console.log(last_code);
codes.push(last_code);
this.set('model.codes', codes);
console.log(codes);
},
remove_code_input: function() {
var codes = this.get('model.codes');
codes.pop();
console.log(codes);
this.set('model.codes', codes);
}
So the remove works fine but the add doesn't work.
It gives me this error when I try to update last_code: Uncaught Error: Assertion Failed: You must use Ember.set() to access this property (of [object Object])
I essentially want to add a dummy object that user can change.
So first issue is figuring out how to add dummy objects into the array properly and secondly how to update the template as the model changes.
You should be using arr.pushObject(obj) and arr.popObject() for manipulating an array in Ember (think of it as the setter/getter of arrays).
is codes really just attr() because it appears to be behaving like a DS record.
If it is a record, you just use record.set('foo', 'bar') if it's just a POJO you can use Ember.set(obj, 'foo', 'bar').
It should be as easy as this (I'm assuming you're using and in the ObjectController here)
var newCode = {
code:'foo'
};
this.get('codes').pushObject(newCode);
In my application I have an event for adding a new nested object to my observable array. In my code I am trying to do this:
Change my observable array into a standard JS object. Lets call this objAgency.
Create a new object with properties. I will call this objContact.
I splice the old data from objAgency by using an ID that I have in the code.
I add objContact in its place.
I convert objAgency back into an observable.
I map objAgency to my bindings, but this gives me the below error:
Unhandled exception at line 1936, column 17 in http://localhost:13762/scripts/knockout-2.2.1.debug.js
0x800a139e - JavaScript runtime error: Unable to parse bindings.
Message: ReferenceError: 'router' is undefined;
Bindings value: css: { active: router.isNavigating }
This is my agency.js file
define(['services/datacontext'], function (dataContext) {
var initialized = false;
var agency;
agency = ko.observableArray([]);
brands = ko.observableArray([]);
var vm = { // This is my view model, my functions are bound to it.
//These are wired up to my agency view
activate: activate,
agency: agency,
brands: brands,
title: 'agency',
refresh: refresh, // call refresh function which calls get Agencies
save: save,
cacheForm: cacheForm,
addOffice: addOffice,
addBrand : addBrand,
removeBrand: removeBrand,
addContact: addContact,
removeContact: removeContact
};
return vm;
function activate() {
vm.agency;
if (initialized) {
return;
}
initialized = false;
refresh();
}
function refresh() {
dataContext.getAgency(agency);
dataContext.getBrands(brands);
}
function addBrand() {
brands.unshift({
brandName : ""
});
// Change td css to editable textbox
jQuery("#tblBrand td:first input").removeAttr('readonly');
}
function removeBrand(brand) {
brands.remove(brand);
}
This is my addContact function where my code breaks
function addContact(office) { // Passing in object array of agency. We no it contains correct office and agency ID
// Convert agency to object
objAgency = ko.toJS(agency);
// Get ID of office I am changing
var officeID = office.officeID._latestValue;
// Convert from observable to vanilla object
objOffice = ko.toJS(office);
// Fill new object with empty strings and related data
var contact = {
agencyID: office.agencyID._latestValue,
emailAddress: "",
firstName: "",
jobName: "",
office: "",
OfficeID: office.officeID._latestValue,
personID: "",
surName: "",
title: ""
}
// Unshift new object to front of object. It will be first row in table.
objOffice.contacts.unshift(contact);
// Convert back into observable
//obsOffice = ko.observableArray([ko.mapping.fromJS(objOffice)]);
// Splice where office ID match
for (i in objAgency[0].offices) {
if (!isNaN(i)) {
if (objAgency[0].offices[i].officeID === officeID) {
objAgency[0].offices.splice(i, 1); // At i remove one object
}
else {
}
}
}
objAgency[0].offices.unshift(objOffice);
agency = ko.observableArray([ko.mapping.fromJS(objAgency[0])]);
vm.agency = agency;
ko.applyBindings(objAgency);
}
function removeContact(contact) {
for (i in agency._latestValue[0].offices._latestValue) {
if (isNaN(i)) { // Escape if NaN, otherwise use index valI ha
}
else {
for (ii in agency._latestValue[0].offices._latestValue[i].contacts._latestValue) {
agency._latestValue[0].offices._latestValue[i].contacts.remove(contact);
}
}
}
}
});
I don't understand why ko.applyBindings(objAgency); does not work because the object hasn't changed apart from removing one nested object and adding a new one in it's place.
Before you execute the problematic code, the Composition lifecycle of Durandal has bound the the shell.jsand subsequently the ViewModel agency.js with the UI.
Meanwhile in the addContact function, at the last line you basically change the whole binding applied to the view:
ko.applyBindings(objAgency);
So your whole UI is now bound to this objAgency which I guess does not contain a router instance as the errors is pointing :
ReferenceError: 'router' is undefined;
This error comes probably from the binding of the shell because is no longer bound to the UI since your code applied new bindings.
SOLUTION:
The applyBindings is managed by Durandal in the composition process. If you want to modify some properties of the ViewModel there's no need to update the bindings.
I don't know if the code you provided is working with your solution, however the applyBindings should not be there for sure.
To replace the data:
vm.agency([ko.mapping.fromJS(objAgency[0])]);
Or:
vm.agency.removeAll()
vm.agency.push(ko.mapping.fromJS(objAgency[0]));
I am currently attempting to build my own JS framework as a way to learn. I have created a method for making an new model like so:
var myModel = new $n.model.make({
data: {
id: _contentID,
name: $('#frm_content_name').val()
},
route: '/my/path',
callback: function(){}
});
The make() function is simple:
$n.model.make = function(obj){
var self = this;
self.data = obj.data;
self.route = obj.route;
self.callback = obj.callback;
}
As you can see, the name parameter is assigned to a form input field's value. If I change this input field value on the UI, the name parameter is still assigned to the original value of the input field so if I try to save the data to the DB, nothing has changed.
Once I have established my model, what is a good way to "rebind" the data parameters to their assigned form fields to get the latest information?
As mentioned in a comment you could do this:
data: {
id: _contentID,
name: function() {
return $('#frm_content_name').val();
}
},
Calling data.name() would always fetch the latest information. Another way is this:
data {
id: _contentID,
name: $('#frm_content_name')
},
Now you would need to call data.name.val(); personally I don't really like this because of the missing encapsulation.
I found the answer I was looking for - two-way data binding. Here is the tutorial I used to solve my issue: http://www.lucaongaro.eu/blog/2012/12/02/easy-two-way-data-binding-in-javascript/
I'm having issues syncing JSON data received from the server with my views after a fetch.
I do not have a collection of "mainmodel", because I'm only working with one "mainmodel" at a time but numerous "mymodel", anyhow, the structure follows:
var mymodel = Backbone.Model.extend({
defaults: {k1:"",
k2:"",
k3:""}
});
var collection = Backbone.Collection.extend({model:mymodel,});
var mainmodel = Backbone.Model.extend({
defaults: {v1:"",
v2:"",
v3:"",
v4:new collection()
});
I create the nested views for "mymodel" from a render function in a parent view. This works..., only when I'm working with a new model.
// My ParentView render function
render: function() {
for (var i = 0; i < this.model.v4.length;i++) {
var view = new MyModelView({model:this.model.v4.at(i)});
this.$el.append($(view.render().el));
}
this.$el.append(this.template(this.model.toJSON()));
return this;
},
// The MyModelView render function below
render: function() {
this.$el.html(this.template(this.model.toJSON()));
return this;
},
Now, the above works if I open my application and create models from there. However, If I open my app and supply an id, I make a fetch to the server, retrieve the data, and create a new ParentView I end up getting an error that says "this.model.v4.at not a function". Ugh.
So now, if I change the FIRST render function to be, changing the at(i) to [i]
var view = new MyModelView({model:this.model.v4[i]});
And change the second render function, removing toJSON, to be:
this.$el.html(this.template(this.model));
It renders. But I still can't move around views without errors. No surprise.
I have used console.log(JSON.stringify(this.model)); as they arrive into the parentView and MyModelView. The JSON returned looks like this, whether fetched or created.
{"v1":"val1",
"v2":"val2,
"v3":"val3",
"v4":[{"k1":"key1","k2":"key2","k3","key"}, { ... }, { ... }]
}
The JSON data structures appear to be identical. I thought the JSON format was incorrect, so I tried using JSON.parse before handing the model to the view, but that didn't work. Maybe I'm way off, but I originally thought I had a JSON formatting issue, but now I don't know. The server is returning content as 'application/json'.
Edit: The JSON values for v1,v2,v3 render correctly.
Any ideas?
You have two problems: one you know about and one you don't.
The problem you know about is that your mainmodel won't automatically convert your v4 JSON to a collection so you end up with an array where you're expecting a collection. You can fix this by adding a parse to your mainmodel:
parse: function(response) {
if(response.v4)
response.v4 = new collection(response.v4);
return response;
}
The problem you don't know about is that your defaults in mainmodel has a hidden reference sharing problem:
var mainmodel = Backbone.Model.extend({
defaults: {
//...
v4: new collection()
}
});
Anything you define in the Backbone.Model.extend object ends up on your model's prototype so the entire defaults object is shared by all instances of your model. Also, Backbone will do a shallow copy of defaults into your new models. So if you m1 = new mainmodel() and m2 = new mainmodel(), then m1 and m2 will have exactly the same v4 attribute. You can solve this by using a function for defaults:
var mainmodel = Backbone.Model.extend({
defaults: function() {
return {
v1: '',
v2: '',
v3: '',
v4: new collection()
};
}
});