How to update nested state in Ember Octane - ember.js

So my situation is as follows:
I got a component with a couple of input fields that represent a contact and are filled with data from a service:
#service('contact-data') contact;
Each field stands for one property that is accessed via
{{contact.properties.foo}}
I have the properties saved as a JS object to easily filter out empty fields when using them and I tracked it with #tracked like so:
export default class MyService extends Service {
#tracked properties = {id: 0, foo: "", bar : "abc", };
#action updateProperty(name, value) {
this.properties[name] = value;
}
}
However, the properties do not re-render properly in the component and the textfields do not get updated.
I'd appreciate any help with this! Thanks!

Any time you have a bunch of nested state like that which needs to be tracked, just tracking the top-level object won't cause updates to the internals of that object to propagate out. You need to track the internal properties, or you need to reset the whole object you're tracking.
You have basically two rough options for dealing with updates to those internal properties:
If the object has a well-known shape, extract it into a utility class which uses #tracked on the fields, and instantiate the utility class when you create the service. Then updates to those fields will update.
If the object is really being used like a hash map, then you have two variant options:
Use https://github.com/pzuraq/tracked-built-ins, if you don't need IE11 support
Do a "pure functional update", where you do something like this.properties = { ...this.properties, foo: newValue };
Of these, (1) is pretty much always going to be the cheapest and have the best performance. Doing (2.1) will be a little more expensive, because it requires the use of a Proxy, but not enough that you would normally notice. Doing (2.2) will end up triggering a re-render for every property in the properties used anywhere in the app, even if it didn't change.
In the case you've described, it appears the fields are well known, which means you should reach for that class. The solution might look something like this:
import Service from '#ember/service';
import { action } from '#ember/object';
import { tracked } from '#glimmer/tracking';
class TheProperties {
#tracked id;
#tracked foo;
#tracked bar;
}
export default class MyService extends Service {
properties = new TheProperties();
#action updateProperty(name, value) {
this.properties[name] = value;
}
}
Note that #tracked installs getters and setters in place of plain class properties, so if you need to use this for a JSON payload somewhere or similar, you'll also want to implement toJSON on the utility class:
class TheProperties {
#tracked id;
#tracked foo;
#tracked bar;
toJSON() {
let { id, foo, bar } = this;
return { id, foo, bar };
}
}

There's another add-on that does basically the same thing for Array and Objects as tracked-built-ins.
It's a proxy that basically notifies the root that an update has occurred somewhere. The advantage against tracked-built-ins is that the nesting depth is not limited as it's common for JSON to have deep nesting.
The drawbacks are similar to tracked-built-ins in terms of performance. Use it sparingly and try not to use it in tables with hundreds/thousands of rows as re-rendering is going to be not performant.

Related

How to pass a #tracked object from an Ember route model hook

My question is two-fold:
Where is the best place to put some kind of polling logic - in the route file right?
How do I pass this constantly updating value from the Route to some child component? Labeling some variable as "#tracked" and then passing the tracked variable via the model hook?
Let's say I have something like this:
routes/index.js
export default class IndexRoute extends Route {
#tracked
recent: {
A: 0,
...
},
constructor() {
super(...arguments);
this.getRecent();
}
getRecent() {
// poll data / fetch latest
const {A, ...partialObject} = this.recent;
this.recent = { ...partialObject, A: <some new value fetched>};;
later(this, this.getRecent, 2000);
}
model() {
return this.recent;
}
}
application.hbs
<p>constantly updating "this.recent": {{ this.model.A }} </p>
I thought if I use the model hook like this, it would be tracked and therefore auto-update but that was not the case. I have this sample Ember Twiddle that emulates what I'm trying to do. I tried to force a re-compute by reassigning the entire variable but it didn't work.
This question is a deeper dive from my initial question here.
You are returning a reference to object stored in this.recent in your model hook. But the getRecent method does not change that object but overrides this.recent. After the first execution of getRecent method the model of the route and this.recent aren't the same object anymore. The model of the route, which you can access through this.modelFor(this.routeName) is the initial value and this.recent is the new value.
You want to mutate the object returned from model hook instead.
The object given in your example has a fixed schema. This allows you to mark the property A as tracked:
recent: {
#tracked A: 0,
...
}
As currently you return the value of this.recent in your model hook. But instead of overwriting it in getRecent method, you are only changing the value of it's property A:
getRecent() {
this.recent.A = <some new value fetched>;
later(this, this.getRecent, 2000);
}
If you don't know the schema of the object returned in model hook or if you are dealing with an array, it's a little bit more complicated. You wouldn't have a property to decorate with #tracked. I would recommend to use the tracked-built-ins package in that case.
For arrays you can also fallback to legacy MutableArray from #ember/array/mutable package. But you must make sure in that case that you use it's custom methods to manipulate the array (e.g. pushObject instead of push).

computed.oneWay not working with Ember Data

I have an EmberData snapshot which I'd like decorate with a few additional attributes before handing over to the UI for presentation. This decoration will be setting "properties" of the Ember-Data record not actual attributes. It looks like this:
let foo = Ember.computed.oneWay(this.get('store').find('activity'));
foo.map(item => {
item.set('foobar', 'baz');
return item;
}
I would then hope that foo would be the beneficiary of the promised record (it is) and the setting of the foobar property would be localized to the foo property (it's not, it seems to be globally scoped to the record's properties).
As for me this is expected behavior.
1) OneWay means only you are not bind set method on foo. It used to work between properties of objects, but this.get('store').find('activity') is a just promise.
2) foo is store.find() result of DS.RecordArray type (assuming promise is resolved). So you are iterating on records returned and set foobar property on them.
Achieving your goal, you could decorate activity record by component (Ember 2.0 way) for UI presentation.
So far the only way I've been able to do this is using Ember's ObjectProxy like so:
const FooDecorator = Ember.ObjectProxy.extend({
foobar: 'baz'
});
let foo = Ember.computed(function() {
return this.get('store').find('activity').map(item => {
FooDecorator.create({content: item});
});
}
This works but I thought I'd been hearing things about Object proxies not being a welcome part of Ember 2.0 roadmap so not sure if this approach is best. I'll keep this open for a few days before closing.

Ember.js: Is it possible to inject a dependency on a specific Route/Controller Mixin?

Let's say I have a SessionManager instance which I want to be accessible in every Route extending my ProtectedRoute Mixin, is it possible to inject this dependency into a "group of routes" as I can reference a single Route instance?
So instead of:
App.inject('route:protected1', 'sessionManager', 'session_manager:main');
App.inject('route:protected2', 'sessionManager', 'session_manager:main');
....
I could do something like
App.inject('route:protectedmixin', 'sessionManager', session_manager:main);
You certainly can, but it might involve a bit of juggling. You could define any logic to decide what to inject and where if you want to rely on the default conventions you could manually find this objects and then use the fullname when injecting.
Another option would be to do it for each route, regardless of whether they include the Mixin or not. Inject doesn't need the full name, if you call `App.inject('route', ...) it would work by default.
If going with option one, it would look something like this. You basically need to find those routes implementing their mixins and then inject into all of those.
var guidForMixin = Ember.guidFor(App.YourMixin);
var routesToInjectInto = Ember.keys(App).filter(function (key) {
var route, mixins;
if (key.match(/Route$/))
route = App[key];
mixins = Ember.meta(route).mixins;
if (mixins) {
!!mixins[guidForMixin];
}
return false;
);
routesToInjectInto.each( function (key) {
var keyForInjection = Ember.decamelize(key);
App.inject('route:' + keyForInjection, 'sessionManager', 'session_manager:main');
});
Also I would suggest doing all of this inside an initializer, but that might be a minor consideration.
Ember.onload('Ember.Application', function(Application) {
Application.initializer {
name: "sessionManager"
initialize: function (container, application) {
// do the above here. Refer to app as the namespace instead of App.
// use the container instead of App.__container__ to register.
};
});

Force a controller to always act as a proxy to a model in Ember

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.

Entire monorail action invocation in tests

BaseControllerTest.PrepareController is enough for controller properties setup, such as PropertyBag and Context
[TestClass]
public ProjectsControllerTest : BaseControllerTest
{
[TestMethod]
public void List()
{
// Setup
var controller = new ProjectsController();
PrepareController(controller);
controller.List();
// Asserts ...
Assert.IsInstanceOfType(typeof(IEnumerable<Project>),controller.PropertyBag["Projects"]);
}
}
But now to run the entire pipeline for integration testing, including filters declared in action attributes?
EDIT:
I'm not interested in view rendering, just the controller logic along with declarative filters.
I like the idea of moving significant amount of view setup logic into action filters, and i'm not sure if i need extra level of integration tests, or is it better done with Selenium?
you can get a hold of the filters, and run them.
so, assuming action is Action<YourController>, and controller is an instance of the controller under test,
var filtersAttributes = GetFiltersFor(controller); // say by reflecting over its attributes
var filters = filtersAttributes
.OrderBy(attr => attr.ExecutionOrder)
.Select(attr => new { Attribute = attr, Instance =
(IFilter)Container.Resolve(attr.FilterType) }); // assuming you use IoC, otherwise simply new the filter type with Activator.CreateInstance or something
Action<ExecuteWhen> runFilters = when =>
{
// TODO: support IFilterAttributeAware filters
foreach (var filter in filters)
if ((filter.Attribute.When & when) != 0)
filter.Instance.Perform(when, Context, controller, controllerContext);
};
// Perform the controller action, including the before- and after-filters
runFilters(ExecuteWhen.BeforeAction);
action(controller);
runFilters(ExecuteWhen.AfterAction);
Getting the view-engine to play is trickier (though possible), but I think that testing generated views along with the controller logic is involving way too many moving and incur unjustified maintenance effort