Pass array to linkTo helper - ember.js

I want to create a component which needs to generate dynamics links. I tried passing the link data as an array, but this does not work.
var user1 = get("store").find("user", 1);
var data = {link: ["users.show", user1], title: "User1"};
{{#link-to data.link}}{{data.title}}{{/link-to}}
This should be equal to
{{#link-to "users.show" 1}}{{data.title}}{{/link-to}}
How to generate fully dynamic links from a variable?

You can specify an array as params argument into a link-to helper. Similar to nickiaconis' answer answer, but with just the default {{link-to}} helper:
{{#link-to params=data.link}}{{data.title}}{{/link-to}}
...will render something like:
User1
(tested with Ember 2.3.0)

Ember 1.13.x
The LinkComponent, which is what the link-to helper creates for you, is exposed as -link-to. I've created an example of its use here: http://emberjs.jsbin.com/rinukefuqe/2/edit?html,js,output
{{#-link-to params=(unbound link) hasBlock="true"}}
{{title}}
{{/-link-to}}
The params attribute is what the link-to helper normally bundles your positional parameters onto, although you must use the unbound helper here because the LinkComponent expects params to be an array rather than a value binding object. Additionally, the determination of use as block or inline component is not built into components yet, so you must pass hasBlock="true" unless you include the link text as the first parameter in your array.
Ember ≤ 1.12.x
Although it is not done already, you can manually expose the LinkView component, which is the equivalent of the new LinkComponent.
App.XLinkToComponent = Ember.LinkView.extend();
Then use it like:
{{#x-link-to params=link}}
{{title}}
{{/x-link-to}}
Using unbound and hasBlock="true" are not necessary as the internal logic of LinkView differs from LinkComponent.

I think that isn't possible to pass an array, but you can pass each argument directlly, like the following:
route
var user1 = this.store.find('user', 1);
var data = { data: { link: "users.show", model: user1, title: "User1" } };
return data;
template
{{#link-to data.link data.model.id}}{{data.title}}{{/link-to}}
I hope it helps

Related

Ember 2.8: Should I randomize an array as a handlebars helper or write a function in the server side?

I have a function that takes in an array and randomizes the values inside of it.
shuffle([1,2,3,4]) => [2,4,1,3];
Now, I have succesfully used this function in my server side code for my ember app like so:
mirage config.js file
this.get('/games/:id', function (db, request) {
var game = games.find((game) => request.params.id === game.id);
var cardsShuffled = shuffle(game.attributes.cards);
game.attributes.cards = cardsShuffled;
return {data: game};
});
And then rendering my handlebars view like so:
play.hbs
<div>
{{#each model.cards as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
</div>
But I was wondering if it is better to create some sort of handlebars helper? I'm new to ember, but it doesn't look too clean to me to have the backend do that kind of logic. So here's what I was thinking:
shuffle-array.js helper
import Ember from 'ember';
export function shuffleArray(array) {
var m = array.length, t, i;
while (m) {
i = Math.floor(Math.random() * m--);
t = array[m];
array[m] = array[i];
array[i] = t;
}
return array;
};
export default Ember.Helper.helper(shuffleArray);
And then somehow use it like so (tried this. failed):
play.hbs (revised)
<div>
{{shuffle-array model.cards}}
{{#each model.cards as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
</div>
So I guess my question is two-fold: Is it acceptable/unnaceptable to have this kind of logic coming from the mirage backend? Also, what is the best way to have a handlebars helper implement this?
Is it acceptable/unnaceptable to have this kind of logic coming from the mirage backend?
If the logic isn't meant to be persistent, I'd say no.
Also, what is the best way to have a handlebars helper implement this?
I'd prefer a computed property on the relevant controller. Using this shuffle function, it'd be:
import Ember from 'ember';
export default Ember.Controller.extend({
shuffledCars: Ember.computed('model.cars.[]', function(){
return shuffle(this.get('model.cars'));
})
});
Acceptable this or not depends on your needs. As I can see from code, you are shuffling some cards for some game. It possible that you need to do such kind of things on backend because of some security concerns.
But doing it with helper is also good and acceptable (but user with abilities to code can read/manipulate data). A few notes about helpers:
Helper function have two parameters: array of unnamed parameters and a hash of named parameters. So in your code you will access first parameter as array[0]
You should use helpers like this:
{{#each (shuffle-array model.cards) as |instance|}}
{{game-card symbol=instance.url}}
{{/each}}
More about helpers
Computed properties, mentioned in other answer, also are good for such kind of things. Maybe even better, because helper will shuffle array on each rendering (i.e. when user navigates back and forth), while computed prop will do that only when original array changes.

how to inject a store into a component (when using localstorage adapter)

Ember docs say to define a store like this
MyApp.Store = DS.Store.extend();
If you are looking up records in components, this doc says you can inject the store into the component like this
// inject the store into all components
App.inject('component', 'store', 'store:main');
However, I am using the local storage adapter which I define like this
App.ApplicationAdapter = DS.LSAdapter.extend({
namespace: 'my-namespace'
});
Therefore, I don't know how to inject this into the component (where I need to look up a record) following the above instructions.
Following the instructions of this SO answer, I tried to inject the store into a component by passing it in like store=store and/or store=controller.store
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}} </li>
or
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=controller.store}} </li>
The goal was then to be able to do this in an action in the componeent
var todo = this.get('store');
console.log(todo, "the new store");
todo.set('notes', bufferedTitle);
console.log(todo, "todo with notes set");
todo.save();
However, todo.save(); always triggers
Uncaught TypeError: undefined is not a function
Notice that I logged the store? this is what it shows
Class {_backburner: Backburner, typeMaps: Object, recordArrayManager: Class, _pendingSave: Array[0], _pendingFetch: ember$data$lib$system$map$$Map…}
If i inspect it(by opening the tree, which isn't shown here), it does indeed show that notes were set via todo.set('notes', bufferedTitle); however, it doesn't have any of the other attributes of my model that I defined for the index route, and this object doesn't have a 'save' method. Therefore, it doesn't seem to be the actual store, but rather just some backburner object.
I got the same results trying this SO answer where it says to get the store of the targetObject
var todo = this.get('targetObject.store');
Note, I also tried this, i.e. setting the store to be the store of the item.
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=item.store}} </li>
It should be noted that if I set the store in the component, I can print the store on the page by doing {{store}} which gives me
<DS.Store:ember480>
but I can't do var todo = this.get('store'); in the action that handles the click even in the application code.
Question, using the localStorage adapter, how am I able to look up a record in a component (with the aim of then being able to alter the record and then save it again)
Note, if it's important, I define a model for the (index) route like this
App.Index = DS.Model.extend({
title: DS.attr('string'),
version (unfortunately I don't know what version of Ember data or the adapter I'm using)
Ember Inspector
1.7.0
Ember
1.9.1
Ember Data
<%= versionStamp %>
Handlebars
2.0.0
jQuery
1.10.2
Update in response to request for more info
The code that sets up the problem is very simple.
here's the router (with a bad name for the resource :)
App.Router.map(function(){
this.resource('index', { path: '/'});
}
Here's the route that gets the record to use in the Index route
App.IndexRoute = Ember.Route.extend({
model: function{
var resource = this.store.find('index');
return resource;
}
});
I have an Index Controller which does nothing in particular for the component (unless I should be defining methods on the Controller that get triggered by component events)
In the html, I do this with handlebars to pass data to the component
{{#each item in items}}
<li> {{my-component id=item.customid data=item.stats notes=item.notes store=store}}
{{/each}}
Then, in components/my-component, I have a label that when clicked is supposed to trigger an action that will let me edit one of the attributes on the model
<label> {{action "editTodo" on="doubleClick">{{notes}}</label>
that click triggers this code in App.MyComponent, which triggers the error that prompted this question
var todo = this.get('store')
todo.set('notes', bufferedTitle);
todo.save()
IMHO injecting store into components is not the best idea... By design, components should be isolated and shouldn't have any knowledge about the store.
In the doc you've given, it's written: In general, looking up models directly in a component is an anti-pattern, and you should prefer to pass in any model you need in the template that included the component.
However, if you really need it for some reason, then why not just to pass the variable store to the component?
{{my-component store=store}}
Then, you can pass the store from your controller only in the components where you really need that.
Injecting the store in all your components will most likely lead you to the bad design (although it seems tempting at first).
Here's an updated answer for Ember 2:
Ember Data's store is now a Service, and we can easily inject it into all Components via an Initializer, e.g. app/initializers/inject-store-into-components:
export function initialize(application) {
application.inject('component', 'store', 'service:store');
}
export default {
name: 'inject-store-into-components',
initialize,
}
Then, in your Components, you can access the store with this.get('store'). The obviates the need to directly pass the store as an argument to Components, which requires a lot of boilerplate in your templates.
Whilst the accepted answer is sensible for simple applications it is perfectly acceptable to inject a store into a component if that component doesn't have a relationship with the url, like side bar content or a configurable widget on a dashboard.
In this situation you can use an initializer to inject the store into your component.
However, initializers can be a pain to mimic in testing. I have high hopes that the excellent Ember.inject API that is testing friendly will extend beyond services and accommodate stores. (Or that stores will simply become services).
According to this docThe preferred way to inject a store into a component is by setting a store variable to the record, for example
{{#each item in arrangedContent}}
<li> {{my-component store=item}} </li>
{{/each}}
Then in application code, you can do
var store = this.get('store');
store.set('todo', bufferedTitle);

#each over different models same route

I have two JS arrays, one route, I can only get one model per route and regardless of trying endless ways of creating ArrayControllers and using them I can't use a 2nd model in my template.
e.g.,
{{#each}} // uses content model and default controller... works fine
<label>{{name}}</label>
{{/each}
{{#each %%whatever%%}} // trying to use another model/ JS array
<label>{{name}}</label>
{{/each}
Either I get errors or no data shows up.
I've tried specifying the array directly, using an ArrayController and setting its content and/or model to be the JS array. I've tried using in...
For example, suppose I have
var x = [{name : "Jo"}, {name : "Bob"}];
var y = [{name : "Jake"}, {name : "Ben"}];
How can I display them as
<label>Jo</label><label>Bob</label>
<label>Jake</label><label>Ben</label>
?
(No reduction here, have to use two separate arrays. My example is more complex but the page does not require all out complicating things like creating extra views, templates, and routes)
it would be nice if one could do
{{#each x}}
<label>{{name}}</label>
{{/each}
{{#each y}}
<label>{{name}}</label>
{{/each}
but this doesn't work.
Any ideas, this is driving me crazy! (I know I could add a variable to each element that specifies which collection it part of then use #if but this is too much of a hack and not very convenient)
Thanks...
You can use the setupController hook in your Route to set properties on the controller. These controller properties are then rendered by the template.
// define some arrays
App.x = [{name: "Jo"}, {name: "Bob"}];
App.y = [{name: "Jake"}, {name: "Ben"}];
App.ApplicationRoute = Ember.Route.extend({
setupController: function (controller, model) {
// make these available to the controller
controller.set('x', App.x)
controller.set('y', App.y)
}
});
See: http://jsfiddle.net/n724p/

Ember, my template doesn't update on property change

Why doesn't my template get updated when I change the property it is rendering? The documentation states that {{each}} is bindings-aware, but there's obviously something to it that I am not aware of.
Like everything in Handlebars, the {{#each}} helper is bindings-aware.
If your application adds a new item to the array, or removes an item,
the DOM will be updated without having to write any code.
Here's my controller
App.MaintemplateController = Ember.Controller.extend({
alist: ['foo', "bar"],
actions : {
addel: function(){
this.alist.push('xpto');
console.log(this.alist);
}
}
});
In my template I have the following code.
{{#each alist}}
<li>{{.}}</li>
{{/each}}
<button {{action 'addel'}}>Add element</button>
The data is properly rendered, and when I click the button it does append elements to the property, but the template doesn't refresh. Why? How do I keep it in sync with my data?
Your template is not updating because you are using plain vanilla javascript .push instead of the by ember provided .pushObject and .removeObject respectively. Ember by default extends the array prototype to make it play nicely in a binding aware environment.
So to make your template update you should do:
App.MaintemplateController = Ember.Controller.extend({
alist: ['foo', "bar"],
actions : {
addel: function(){
this.get('alist').pushObject('xpto');
console.log(this.get('alist'));
}
}
});
The important line here is this.get('alist').pushObject('xpto');
Furthermore you should always use .get() and .set() to access objects in ember otherwise the binding mechanism will not be aware of changes made to the object.
Hope it helps.

How can I bind the element ID for an Ember View?

My model "content.id" contains a string, e,g "123":
{{view Em.TextArea idBinding="content.id"}}
Instead of just setting the id of this view to "123", I'd like it to be "message-123", basically customizing the string being used. Sadly, Ember does not allow bindings to be functions, which would solve my problem (I could define such a function on the controller).
What's the best way to achieve this?
You could define a computed property in the controller (or elsewhere):
The controller
MyApp.ApplicationController = Ember.Controller.extend({
content: "a-content",
editedContent: function() {
return "message-" + this.get('content');
}.property('content')
});
The view
MyApp.FooView = Ember.View.extend({
    tagName: 'p'
});
The template (where content is a String, here)
{{#view MyApp.FooView elementIdBinding="editedContent"}}
{{content}}
{{/view}}
And the JSFiddle is here.
EDIT
How can the view see the property editedContent since it belongs on the ApplicationController controller?
The router, after started, automatically render the ApplicationView, or its template when there is no ApplicationView defined. If you want more detail, I suggest you to read the Ember guide: Understanding the Ember.js Router: A Primer.
And {{editedContent}} directly get the controller editedContent property, because the default view context is its controller, as you can read in Ember Blog - 1.0 Prerelease:
The {{#view}} helper no longer changes the context, instead maintaining the parent context by default. Alternatively, we will use the controller property if provided. You may also choose to directly override the context property. The order is as follows:
Specified controller
Supplied context (usually by Handlebars)
parentView's context (for a child of a ContainerView)