Is there an elegant way of using methods defined in an Ember.Mixin object within a custom handlebars helper?
For example, in my case I have a mixin, App.Format which contains a bunch of different methods for formatting numbers, dates, ect and I would like to make some of these methods accessible directly from templates via custom helpers (e.g. {{formatNumber 450324}}). The reason I don't just put the method implementation directly in the helper instead of the mixin is that I also need these methods to be available in controllers, views, ect. And in the interest of keeping my code DRY, I definitely do not want to have duplicate code in the mixin and the helpers.
Is there any canonical, "emberish" way to accomplish this, or am I just thinking about it in the completely wrong way altogether?
Edit: Here is a jsbin to better illustrate the problem:
http://emberjs.jsbin.com/wunug/1/edit?html,js,output (look at lines 33-37)
The method Mixin.apply makes the job.
Ember.Handlebars.helper('formatNumber', function(num, decimals) {
var format = App.Format.apply({});
return format.formatNumber(num, decimals || 2);
});
I am using Ember 2.12 and this is what I worked for me. Hopefully this helps somebody.
Create a new mixin named formatter:
../mixins/formatters.js
import Ember from 'ember';
export default Ember.Mixin.create({
shortDate: function(date) {
return `Date should be formatted: ${date}`;
}
});
Create a new helper that imports the above mixin and uses it.
../helpers/short-date.js
import Ember from 'ember';
import FormatDateMixin from '../mixins/formatters';
export default Ember.Helper.extend(FormatDateMixin, {
compute: function(params /*, hash*/) {
return this.shortDate(params);
}
});
In the template *.hbs file, use the helper as below:
{{short-date today}}
Related
I have declared a properties in a Mirage factory as below and using Dependent attributes as found in the docs.
price() {
return faker.finance.amount(100000, null, 0);
},
priceDisplay() {
return '$' + this.price;
}
When I do a patch to update price I expect priceDisplay to update as well like a computed property however this is not the case.
Does anyone know if this is possible or is this a Mirage limitation?
Mirage factory are meant to generate testing data:
Factories are classes that help you organize your data-creation logic, making it easier to define different server states during development or within tests.
The factory is only run once by server.create('foo') or server.createList('foo', 10) to create initial data for the records. This helps you to avoid code duplication in tests and scenarios. But it's not a model representing that record.
Actually Mirage does not support something like computed properties out of the box. But you could achieve it by customizing the serializer used. Overriding the serialize method should do the trick:
// serializers/product.js
import { JSONAPISerializer } from 'ember-cli-mirage';
export default JSONAPISerializer.extend({
// This is how to call super, as Mirage borrows [Backbone's implementation of extend](http://backbonejs.org/#Model-extend)
let json = Serializer.prototype.serialize.apply(this, arguments);
json.priceDisplay = '$' + json.price;
return json;
});
But from your example given I would question if returning a formatted string from the API is the right approach. Formatting data should be a concern of the client in my opinion. Otherwise you will quickly run into limitations if you need to support localization or require different formats in your client.
I am using Ember 2.16.0 and I can import string into Handlebar but when I try to access JSON property I am not getting expected result. Is there a helper that will convert a string into JSON inside Handlebar template?
Similar to the answer the OP posted, except I'd make the schemaJson property a computed property so that if schemasString changed, it would automatically update:
export default Component.extend({
schemasJson: computed('schemasString', function() {
return JSON.parse(this.schemasString);
}
});
Generally the intent of Handlebars is to keep as much logic out of the template as possible, so more typically in Ember you might perform the JSON parsing in a class, like a controller or component. That way by the time the data is sent to the template it’s already in the final data format you need. Is that an option for you?
I ended up using something like this to make data available to the component template. Not 100% sure if this is the best approach.
export default Component.extend({
init() {
this._super(...arguments);
this.set('schemasJson', Ember.$.parseJSON(this.schemasString));
}
});
--Using Ember Data 2.7.1--
I am trying to reverse the order of a collection of records without first turning them into an array using toArray(). This collection of objects comes from the promise returned by this.store.findAll('history-item').
I want to do this the ember way instead of making them plain javascript. I am getting a TypeError: internalModel.getRecord coming from record-array.js. For some reason when it is trying to do objectAtContent(), the content it is looking seems to not have a type. Through the stack trace I can see that the object I am dealing with is [Class], class being the history-item model. A few stack calls before the objectAtContent(), the object being dealt with switches from that history-item model to some other Class object that has no type attribute.
I am able to use Ember Inspector to see my data correctly, and if I just displayed the original collection of records on my template, it shows properly.
Has anyone run into this?
Some thoughts and considerations:
-Is there anything special about how findAll() works with its promise that doesn't allow for reversal since it is reloading in the background? I do want it to keep reloading live data.
-I am using ember-cli-mirage to mock my db and endpoints and I've follow the instructions to the letter I think. I am using an unconfigured JSONAPISerializer for mirage and and a unconfigured JSONAPIAdapter for ember. Could it have anything to do with metadata that is being sent from the back? Could it have something to with the models or records not being set up? Is there something special I have to do?
Route Segment that defines model and tries to reverse it:
[note: I know it may not be convention to prep the data (ordering) in the route but I just put it in here for ease of description. I usually do it outside in the controller or component]
model(){
return this.get('store').findAll('history-item').then(function(items){
return items.reverseObjects();
}).catch(failure);
History list model declaration:
export default DS.Model.extend({
question: DS.attr('string'),
answer: DS.attr('string')
});
Ember-Cli-Mirage config.js end points:
this.get('/history-items', (schema) => {
return schema.historyItems.all();
});
Ember-Cli-Mirage fixture for history-items:
export default [
{id: 1, question: "1is this working?", answer: "Of course!"}
}
Error:
TypeError: internalModel.getRecord coming from record-array.js
This issue also happens when I try to create a save a record. The save is successful but when the model gets reloaded (and tries to reverse), it fails with the same error. It doesn't matter if I the fixture or not.
Controller:
var newHistoryItem = this.store.createRecord('history-item', {
question: question,
answer: answer
});
newHistoryItem.save().then(success).catch(failure);
The result returned from store.findAll and store.query is an AdapterPopulatedRecordArray (live array), mutation methods like addObject,addObjects,removeObject,removeObjects,
unshiftObject,unshiftObjects,pushObject,pushObjects,reverseObjects,setObjects,shiftObject,clear,popObject,removeAt,removeObject,removeObjects,insertAt should not be used.
Have a look at corresponding discussion and
Proposed PR to throw error and suggestions to use toArray() to copy array instead of mutating.
I think using toArray is fine, no need to reinvent the wheel. Even Ember's enumerable/array methods are implemented using toArray under the hood.
I like keeping transforms on controllers/components, so Routes are only concerned with [URL -> data] logic. I think here I would keep the model hook returning the server data, and use a computed property on the controller:
import Ember from 'ember';
export default Ember.Controller.extend({
reversedItems: Ember.computed('model.[]', function() {
return this.get('model').toArray().reverse();
})
});
Twiddle: https://ember-twiddle.com/6527ef6d5f617449b8780148e7afe595?openFiles=controllers.application.js%2C
You could also use the reverse helper from Ember Composable Helpers and do it in the template:
{{#each (reverse model) as |item|}}
...
{{/each}}
I want to be able to write a component which has three sources of classNames:
A set of static classes from the classNames property of the component (static JS code)
A set of user provided classes provided through the classNames property on the HB-helper
Up to here I have tested successfully -- Ember will merge the two sources which I think is nice. I cannot however find where this behaviour is documented.
Now I want to be able to add further classes to this list with a computed property.
The only way to do this seems to be with classNameBindings but this is not sufficient in my case! As the list of classes is quite dynamic and I want to calculate it explicitly.
What I have also tried is to define the classNames as property() but this seems not to be possible according to this issue.
Does anyone know a way to achieve this?
This is one case where I think you don't want to use Ember and instead use jQuery directly. Use Ember to take care of the static classes and the classes provided through the Handlebars helper, but use jQuery to add the computed property classes. For instance, let's says that you have a computed property called extraClasses. Try something like this:
extraClasses: function() {
return ['one', 'two', 'three'];
}.property(),
previousExtraClasses: [],
manageClasses: function() {
var previousExtraClasses = this.get('previousExtraClasses');
var extraClasses = this.get('extraClasses');
// Remove the classes that got removed from `extraClasses`
previousExtraClasses.forEach(function(className) {
if (extraClasses.indexOf(className) < 0) {
this.$().removeClass(className);
}
}, this);
// Add the classes that got added to `extraClasses`
extraClasses.forEach(function(className) {
if (previousExtraClasses.indexOf(className) < 0) {
this.$().addClass(className);
}
}, this);
this.set('previousExtraClasses', extraClasses);
).observesImmediately('extraClasses')
Every time you update extraClasses with a new set of classes, the manageClasses observer will take care of managing the classes on the component. As long as extraClasses returns an array of strings, it can be computed any way you like.
I'm not sure I fully understood your reasoning against classNameBindings. You can get dynamic classes list with help of that property. I've prepared an example in jsbin.
It uses rule that applies to classNameBindings: if no conditional classes have been listed (eg. classNamesBindings: ['isEnabled:enabled:disabled']), than use provided by computed property class name as is (eg. true||false would be in classes list of HTMLElement). So, you need to declare a computed property which returns a string with classes names and to list this CP in component's classNameBindings.
I have a route that has a dynamic segment:
this.resource('dog', {path: '/dog/:pet_id'});
For debugging purposes, I would like to linkTo dog with the specific dynamic segment of '666'. But
{{#linkTo 'dog' '666'}}Click to go to dog{{/linkTo}}
is giving me "undefined" instead of "666". Do you know why?
See it running on jsbin.
See the code on jsbin.
Your working jsbin: http://jsbin.com/iwiruw/346/edit
The linkTo helper does not accept strings as a parameter, but instead model from which to pick up the dynamic segments defined in your router map. If you don't have a model at hand leave the parameter out, and all you need to do is to hook into the serialize function of your DogRoute (if you don't have one defined just define it to instruct ember to use yours instead of the automatically defined) and return an object/hash containing the dynamic segments your route expects, this could be anything you want:
App.DogRoute = Ember.Route.extend({
serialize: function(model) {
return {pet_id: 666};
}
});
Hope it helps.
I cleaned up the code a little bit by removing unused bits and switching to the fixture adapter. Here's a working version without the need for a serialize method: http://jsbin.com/iwiruw/347
Ultimately, nothing needed to be changed in the base code beyond using a newer version of Ember and properly setting up the actual model classes and data.