Hello StackOverflow experts,
I would like to know if it would be possible to use Ember.js' computed properties to modify the value of the property before returning to whatever object requests it.
Imagine this simple example:
I have a User object with mail property
When I set the property, I want the email address to change from first.last#example.com to first.last#anotherexample.com, then return it
When I request the property ( via User.get ) I want to get the modified property back.
I think it should be pretty simple by utilising another 'helper' property, like formatted_mail, where I would store and retrieve the formatted value, but I wonder if something like this can be done without additional model properties.
So far, I have this coffescript code, but I always get 'undefined' when reading the property, even though I set it before, so I suspect the value does not get saved by Ember anywhere:
mail: ( ( key, value ) ->
if arguments.length == 1
return this.get 'mail'
else
return value.split( '#' )[0] + '#anotherexample.com'
).property 'mail'
Thank you for your assistance!
You are close to solution.
As computed properties are always cached by default in Ember (you could disable this behaviour using .volatile()), you do not have to specify what to do when arguments.length is 1, except if you want to specify a default value.
So here it should looks like:
App.User = Ember.Object.extend({
mail: function(key, value) {
if (arguments.length === 2) {
return value.split('#')[0] + "#tieto.com";
}
return null;
}.property()
});
The return null just specify the default value.
When you set the mail property, it will cache the returned value and always returns it without recomputing this property.
Note that you can do that only because the mail property does not depend on other properties. If you were declaring it with .property('anotherProperty'), the mail property will be recomputed any time anoterProperty changes. So in the example above it will reset it to null.
You can try it in this JSFiddle.
Related
Example here: https://codesandbox.io/s/j4mo8qpmrw
Docs here: https://www.apollographql.com/docs/link/links/state.html#default
TLDR: This is a todo list, the #client query parameters don't filter out the list.
This is the query, taking in $id as a parameter
const GET_TODOS = gql`
query todos($id: Int!) {
todos(id: $id) #client {
id
text
}
}
`;
The query passes the variable in there
<Query query={GET_TODOS} variables={{ id: 1 }}>
/* Code */
</Query>
But the default resolver doesn't use the parameter, you can see it in the codesandbox.io example above.
The docs say it should work, but I can't seem to figure what I'm missing. Thanks in advance!
For simple use cases, you can often rely on the default resolver to fetch the data you need. However, to implement something like filtering the data in the cache or manipulating it (like you do with mutations), you'll need to write your own resolver. To accomplish what you're trying to do, you could do something like this:
export const resolvers = {
Query: {
todos: (obj, args, ctx) => {
const query = gql`
query GetTodos {
todos #client {
id
text
}
}
`
const { todos } = ctx.cache.readQuery({ query })
return todos.filter(todo => todo.id === args.id)
},
},
Mutation: {},
}
EDIT: Every Type we define has a set of fields. When we return a particular Type (or List of Types), each field on that type will utilize the default resolver to try to resolve its own value (assuming that field was requested). The way the default resolver works is simple -- it looks at the parent (or "root") object's value and if it finds a property matching the field name, it returns the value of that property. If the property isn't found (or can't be coerced into whatever Scalar or Type the field is expecting) it returns null.
That means we can, for example, return an object representing a single Todo and we don't have to define a resolver for its id or text fields, as long as the object has id and text properties on it. Looking at it another way, if we wanted to create an arbitrary field on Todo called textWithFoo, we could leave the cache defaults as is, and create a resolver like
(obj, args, ctx) => obj.text + ' and FOO!'
In this case, a default resolver would do us no good because the objects stored in the cache don't have a textWithFoo property, so we write our own resolver.
What's important to keep in mind is that a query like todos is just a field too (in this case, it's a field on the Query Type). It behaves pretty much the same way any other field does (including the default resolver behavior). With apollo-link-state, though, the data structure you define under defaults becomes the parent or "root" value for your queries.
In your sample code, your defaults include a single property (todos). Because that's a property on the root object, we can fetch it with a query called todos and still get back the data even without a resolver. The default resolver for the todos field will look in the root object (in this case your cache), see a property called todos and return that.
On the flip side, a query like todo (singular) doesn't have a matching property in the root (cache). You need to write a resolver for it to have it return data. Similarly, if you want to manipulate the data before returning it in the query (with or without arguments), you need to include a resolver.
checked: ((key, value) ->
selected = #get 'controllers.a.selected'
a = #get 'model'
if arguments.length > 1
if value
selected.addObject a
else
selected.removeObject a
return selected.contains a
).property('controllers.a.selected.length')
Now I am trying to call the checked property.
I tried to use #controller.get('checked'),
but how do I pass key, value arguments so that I can test the property.
I do not know how to call it. Thanks a lot.
I hate setting computed properties, I think it's a terrible pattern, but here's how it's done #controller.set('checked', 'foo').
http://emberjs.com/guides/object-model/computed-properties/#toc_setting-computed-properties
I expected a second argument to be passed to my computed property method, but it's not. I need this so I can call a setter to save my model with new data. Instead of that behavior, it appears that my computed property is called again right before I save the model, and clobbering the new values - the setter is never called at all because I only get one argument. Computed property:
changeBananas: function(k, v) {
var bananas = this.get('bananas'), bananaList = [];
console.log('args: ');
console.log(arguments);
bananaList = bananas.map(function(b) {
return { color: b.get('color') };
});
if (arguments.length > 1) {
console.log('I never get called!');
return bananaList;
}
return bananaList;
}.property('bananas.#each')
Full JSBin:
http://jsbin.com/razimaxu/2/edit
I tried propertyWillChange() and friends to try to stop observers, but it did not do anything. Is there another way to do this? My computed property is there to do some formatting of the items before displaying them in editable fields. I expected to be able to change said fields and save just like any other fields that are connected to regular model properties.
The only time it will receive both arguments is if you attempt to set the computed property, such as this.set('changeBananas', []).
It doesn't get called with both arguments if it has noticed a dependent property has changed.
Using Ember-Data 0.13-59 & Ember 1.0.0 RC 6 (from starter kit)
Problem: upon save() to a new record made from App.Userstat.createRecord({ ... }) the server gets the POST and successfully returns an id but the new record is not available in the Userstat model.
To better understand example: this is a quiz app(for multiple choice questions). Each question has several choices and when a user selects a choice, their choice to the corresponding question is stored in a Model, App.Userstat.
At each question, the app needs to know whether the user has already answered this question or if it's new.
I use a computed property as a setter and getter. The setter is called when a user selects a choice (the choice's value is passed to computed property). First it checks if a record exists for the user's current question. If it doesn't exist it will create a new record. If it does exist, it should only issue a PUT request with updated content.
Code Updated(July 8, 11AM)
App.UserstatsController = Ember.ArrayController.extend();
App.QuestionController = Ember.ObjectController.extend({
needs: "userstats",
chosen = function(key, value) {
// getter
if(value === undefined) {
// something goes here
// setter
} else {
// the question.id is used to compare for an existing record in Userstat mdoel
var questionId = this.get('id');
var questionModel = this.get('model');
// does any Userstat record belong to the current question??
var stats = this.get('controllers.Userstats');
var stat = stats.get('model').findProperty('question.id', questionId);
// if no record exists for stat, means user has not answered this question yet...
if(!stat) {
newStat = App.Userstat.createRecord({
"question" : questionModel,
"choice" : value // value passed to the computed property
)}
newStat.save(); // I've tried this
// newStat.get('store').commit(); // and this
return value;
// if there is a record(stat) then we will just update the user's choice
} else {
stat.set('choice', value);
stat.get('store').commit();
return value;
}
}.property('controllers.Userstats')
No matter how many times I set chosen it always sends a POST (as opposed to an update only sending a PUT request) because it never adds the record to the model the first time.
To demonstrate further, in the setter part of the computed property, when I put this code:
var stats = this.get('controllers.Userstats')
console.log stats
the Userstats controller shows all previously existing records, but not newly submitted records!
How come the new record isn't available after I save() or commit() it???
Thanks :)
EDIT
maybe it has something to do with me adding a record to the singular model App.Userstat and then when I look for it, I'm searching using the UserstatsController which is an Array controller???
I don't know if it's a typo, but the computed property is defined the wrong way and should be like this:
App.QuestionController = Ember.ObjectController.extend({
needs: 'userstats',
choice: 'controllers.userstats.choice',
chosen: function(key, value) {
...
}.property('choice')
...
});
Inside the property() you should also define properties that trigger the computed property if they change. This way if choice changes the chosen cp will be triggered.
Please let me know if it helps.
I'm looping through a content of an ArrayController whose content is set to a RecordArray. Each record is DS.Model, say Client
{{# each item in controller}}
{{item.balance}}
{{/each}}
balance is a property of the Client model and a call to item.balance will fetch the property from the model directly. I want to apply some formatting to balance to display in a money format. The easy way to do this is to add a computed property, balanceMoney, to the Client object and do the formatting there:
App.Client = DS.Model({
balance: DS.attr('balance'),
balanceMoney: function() {
// format the balance property
return Money.format(this.get('balance');
}.property('balance')
});
This serves well the purpose, the right place for balanceMoney computed property though, is the client controller rather than the client model. I was under the impression that Ember lookup properties in the controller first and then tries to retrieve them in the model if nothing has been found. None of this happen here though, a call to item.balanceMoney will just be ignored and will never reach the controller.
Is it possible to configure somehow a controller to act always as a proxy to the model in all circumstances.
UPDATE - Using the latest version from emberjs master repository you can configure the array controller to resolve records' methods through a controller proxy by overriding the lookupItemController method in the ArrayController. The method should return the name of the controller without the 'controller' suffix i.e. client instead of clientController. Merely setting the itemControllerClass property in the array controller doesn't seem to work for the moment.
lookupItemController: function( object ) {
return 'client';
},
This was recently added to master: https://github.com/emberjs/ember.js/commit/2a75cacc30c8d02acc83094b47ae8a6900c0975b
As of this writing it is not in any released versions. It will mostly likely be part of 1.0.0.pre.3.
If you're only after formatting, another possibility is to make a handlebars helper. You could implement your own {{formatMoney item.balance}} helper, for instance.
For something more general, I made this one to wrap an sprintf implementation (pick one of several out there):
Ember.Handlebars.registerHelper('sprintf', function (/*arbitrary number of arguments*/) {
var options = arguments[arguments.length - 1],
fmtStr = arguments[0],
params = Array.prototype.slice.call(arguments, 1, -1);
for (var i = 0; i < params.length; i++) {
params[i] = this.get(params[i]);
}
return vsprintf(fmtStr, params);
});
And then you can do {{sprintf "$%.2f" item.balance}}.
However, the solution #luke-melia gave will be far more flexible--for example letting you calculate a balance in the controller, as opposed to simply formatting a single value.
EDIT:
A caveat I should have mentioned because it's not obvious: the above solution does not create a bound handlebars helper, so changes to the underlying model value won't be reflected. There's supposed to be a registerBoundHelper already committed to Ember.js which would fix this, but that too is not released yet.