I am building an Ember tooltip module to create dynamic content on hover.
<div class="custom-tool-wrapper">
{{#custom-tool-tipster
side="right"
content=(or getContent question.id)
contentAsHTML=true
class="tool-tipster-field"}}
Preview
{{/custom-tool-tipster}}
</div>
in the ember controller - the function doesn't return the variable "question.id" --- it comes back as 0 always - when it should be a string "q-1"
export default Ember.Component.extend({
getContent(tips){
console.log("tips1")
console.log("question", tips);
},
});
I think what you're actually trying to achieve is best done via computed property on the question model object (your question is still really vague).
content: computed('id', function(){
//this.tips is a part of the model object
//compute and return whatever the content is
return "content";
}
and then just say:
{{#custom-tool-tipster
side="right"
content=model.content
contentAsHTML=true
class="tool-tipster-field"}}
Preview
{{/custom-tool-tipster}}
If you needed to actually invoke a function (which it's rare to think of an instance where the computed property isn't a better solution whenever state is involved), you would use a custom handlebars helper.
(or a b) is (a || b) and isn't function invocation like you're attempting if you're using the ember truth helpers lib for the or helper. It looks like you're trying to accomplish what ember-invoke allows
import Ember from 'ember';
import { helper } from '#ember/component/helper';
export function invokeFunction([context, method, ...rest]) {
if (typeof context[method] !== 'function') {
throw new Error(`Method '${method}' is not defined or cannot be invoked.`);
}
return Ember.get(context,method).apply(context, rest);
}
export default helper(invokeFunction);
which can be used like content=(invoke this "getContent" question.id) to invoke and return the value of a function on the passed in context object (the controller if this in the case of a route's template). Let me be clear, I think this invoke approach is a terrible idea and really gets rid of your separation of concerns and I'm not advocating that you do it. Templates shouldn't contain your logic and definitely shouldn't be calling arbitrary functions on the controller when you have such a nice facility like computed properties.
Related
I am using init() in my component to load some data for a dropdown. On refresh it works but when I leave the tab to another tab then come back I get the following error:
index.js:143322 Uncaught TypeError: this.get(...).then is not a function
This code is in my init function and I suspect it has something to do with how ember.js renders but I am struggling to figure out how to make it work. I tried using the other lifestyle hooks but none of them worked.
This is the init function which is in a component:
init() {
this._super(...arguments)
this.get('popularTags').then(function(result) {
const newArray = []
for (var i = 0; i < result.length; i++) {
newArray.push({
name: result[i],
value: result[i],
group: 'Popular'
})
}
const popularTags = this.get('popularTags')
this.set('popularTags', newArray)
this.get('queryResults').then(function(result) {
const tagArray = []
for (var i = 0; i < result.length; i++) {
let popular = newArray.filter(tag => tag['value'] === result[i].value)
if (popular.length === 0) {
tagArray.push({
name: result[i].value,
value: result[i].value,
group: ''
})
}
}
const queryResults = this.get('queryResults')
return this.set('queryResults', tagArray)
}.bind(this))
}.bind(this))
},
There is something about your above example that I just don't understand. You seem to be getting and setting both the popularTags and queryResults properties. I'm not sure if that's just an issue in your example or something else - I'm going to assume it's an example issue and break this down a bit more generally:
Doing this much work in init isn't generally a good idea, so much so that it is slated for removal from the upcoming glimmer component API. In particular set inside any of the lifecycle hooks is a recipe for weird errors when the component gets removed from the DOM. While you can use a tool like ember-concurrency to help break this up and deal with set my suggestion would be to split this up into several computed properties. This might look something like:
import Component from '#ember/component';
import { computed } from '#ember/object';
export default Component.extend({
popularTags: computed('tags.[]', function(){
return this.tags.filter(tag => tag.isPopular);
}),
queryResults: computed('popularTags.[]', function(){
return this.popularTags.map(tag => {
return {
name: tag.name,
value: tag.description
};
});
}),
});
Computed Properties like these are the way to express data transformations in Ember. They rely on some initial data that is passed into the component and then modify it for use. In my above example I've assumed that tags gets passed in, but you can see that queryResults relies on the results of popularTags, in this way several different data transformations can be executed in order.
While loading asyncronous data in components can work just fine when you are first building and Ember.js application I would suggest that you confine all of your data loading to the Route's Model Hook as it is better suited to async work and will then give you data you can pass directly into the component without needing to worry about the difficulties in loading it there.
Your problem could be that youre calling the component with curlies and passing popularTags:
{{your-component popularTags=something}}
This is two-way bound. Precisely this means that changing popularTags inside the component will change something on the caller.
This means that if you remove this component and re-create it later (what your mention of some tabbing indicates) you've changes something on the outside. And your component expects popularTags (and so something) to be a promise (when calling this.get('popularTags').then). However because you changes it (with this.set('popularTags', newArray)) its no longer a promise but an array.
Generally I would recommend you to be careful when changing passed attributes.
In a template, I am calling component as:
{{comp-name data=controllerData}}
If I change the data in component, controller's controllerData also getting changed due to implicit two way binding. I have two questions:
1) How to make it as one way binding. So that only changes from controller's controllerData propagate to component's data.
2) There should be no binding at all. Means, if I change data in either component or controller. It should not reflect in other thing.
1) Use the readonly helper:
{{comp-name data=(readonly controllerData)}}
2) Use the unbound helper:
{{comp-name data=(unbound controllerData)}}
See http://emberup.co/bindings-with-htmlbars-helpers/ for more information.
In future versions of Ember with angle bracket components, bindings will be one way by default and you will need to use the mut helper to make it two way.
This article in the guides should help:
https://guides.emberjs.com/v2.18.0/object-model/bindings/
It gives this example:
import EmberObject, { computed } from '#ember/object';
import Component from '#ember/component';
import { oneWay } from '#ember/object/computed';
user = EmberObject.create({
fullName: 'Kara Gates'
});
UserComponent = Component.extend({
userName: oneWay('user.fullName')
});
userComponent = UserComponent.create({
user: user
});
// Changing the name of the user object changes
// the value on the view.
user.set('fullName', 'Krang Gates');
// userComponent.userName will become "Krang Gates"
// ...but changes to the view don't make it back to
// the object.
userComponent.set('userName', 'Truckasaurus Gates');
user.get('fullName'); // "Krang Gates"
Hope that helps.
How can I access a variable declared in /component/sales-order.js from /routes/sales-order.js
Is it possible?
Assuming you mean a property of the component, then basically you can't, and shouldn't. Why do you want to?
A route manages the route; it doesn't know about the details of what is eventually being rendered. A route might, for example, instantiate the same component twice. Then which once would you want to retrieve the value from?
The fact you feel the need to do this indicates some kind of problem with the way your app is structured.
Looking at this as a more general problem of how to communicate between component and route, there are various approaches, but the most basic one is to have the component send an action upward:
// thing/route.js
// Define the ultimate action to be invoked.
export default Ember.Route.extend({
actions: {
hiccup() { console.log("Hiccup!"); }
}
});
// thing/template.hbs
// Invoke the component, and tie the action to something specific
{{component action='hiccup'}}
// component/component.js
// Define the component with an action.
export default Ember.Component.extend({
actions: {
go() { this.sendAction(); }
}
});
//component/template.hbs
// Provide a button
<button {{action 'go'}}>Go!</button>
you first export it and then import it
exporting in /component/sales-order.js
export const MY_VARIABLE = 2;
importing in /routes/sales-order.js
import { MY_VARIABLE } from '../components/sales-order'
console.log(MY_VARIABLE) // 2
I needs to apply an "active" class to a bootstrap tab depending on the current route name. The route object contains "routeName" but how to I access this from a controller or component?
Use this this.controllerFor('application').get('currentRouteName');
In fact, you don't need to apply active class by yourself. A link-to helper will do it for you.
See here:
{{link-to}} will apply a CSS class name of 'active' when the application's current route matches the supplied routeName. For example, if the application's current route is 'photoGallery.recent' the following use of {{link-to}}:
{{#link-to 'photoGallery.recent'}}
Great Hamster Photos
{{/link-to}}
will result in
<a href="/hamster-photos/this-week" class="active">
Great Hamster Photos
</a>
In the absolutely desperate case, you can look up the router, or the application controller (which exposes a 'currentRouteName' property) via this.container.lookup("router:main") or this.container.lookup("controller:application") from within the component.
If it was a common trend for me, I would make a CurrentRouteService and inject it into my component(s) so that I can mock things more easily in my tests.
There may also be a better answer to come along - but the container.lookup() should knock down your current blocker.
Since Ember 2.15 you can do this through the public Router service.
router: service(),
myRouteName: computed('router.currentRouteName', function () {
return this.get('router.currentRouteName') + 'some modification';
}
https://www.emberjs.com/api/ember/release/classes/RouterService
Which worked really well for me since I wanted something computed off of the current route. The service exposes currentRouteName, currentURL, location, and rootURL.
currentURL has the query params, but you would need to parse them from the URL.
For Ember 2, from a controller you can try :
appController: Ember.inject.controller('application'),
currentRouteName: Ember.computed.reads('appController.currentRouteName')
Then you can pass it to component.
Try this.
export default Ember.Route.extend({
routeName: null,
beforeModel(transition){
//alert(JSON.stringify(transition.targetName) + 'typeof' + typeof transition.targetName);
this.set('routeName', transition.targetName);
},
model(){
// write your logic here to determine which one to set 'active' or pass the routeName to controller or component
}
`
Using insights from #maharaja-santhir's answer, one can think of setting the routeName property on the target controller to use, e.g., in the target's template. This way there's no need for defining the logic in multiple locations and hence code-reusability. Here's an example of how to accomplish that:
// app/routes/application.js
export default Ember.Route.extend({
...
actions: {
willTransition(transition) {
let targetController = this.controllerFor(transition.targetName);
set(targetController, 'currentRouteName', transition.targetName);
return true;
}
}
});
Defining this willTransition action in the application route allows for propagating the current route name to anywhere in the application. Note that the target controller will retain the currentRouteName property setting even after navigating away to another route. This requires manual cleanup, if needed, but it might be acceptable depending on your implementation and use case.
I'm trying to extend Ember's TextField with UrlField so that if someone forgets to include http://, it does it for them.
Here's my View:
views/input-url.js
import Ember from 'ember';
export default Ember.TextField.extend({
type: 'url',
didInsertElement: function() {
this._super.apply(this, arguments);
this.formatValue();
},
onValueChange: function() {
this.formatValue();
}.observes('value'),
formatValue: function() {
var pattern = /^https{0,1}:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+/g;
if (pattern.test(this.get('value')))
return;
if (!pattern.test('http://' + this.get('value')))
return;
this.set('value', 'http://' + this.get('value'));
}
});
If I use it in my template like this, it works fine:
{{view "input-url" value=url}}
I prefer to use custom view helpers, so I created this (following the guide at the bottom of this page: http://guides.emberjs.com/v1.11.0/templates/writing-helpers/):
helpers/input-url.js
import Ember from 'ember';
import InputUrl from '../views/input-url';
export default Ember.Handlebars.makeBoundHelper(InputUrl);
Now trying to render this in my template doesn't work:
{{input-url value=url}}
I've also tried different permutations of this, including what's shown in the guide Ember.Handlebars.makeBoundHelper('input-url', InputUrl); (which throws an error), but I can't seem to get my input field to show up. What am I doing wrong?
Not sure what you are doing wrong with your view helper, but there is a much simpler solution: take advantage of the fact that Ember.Textfield is a component. http://emberjs.com/api/classes/Ember.TextField.html
Simply move views/input-url.js to components/input-url.js and get rid of your view helper.
Then {{input-url value=url}} should work automatically.
If you want do to this using a helper, you cannot extend Ember.TextField because extends Ember.Component and is not a Handlebars helper.
The way to do this using a helper would actually be simpler. Since you are using Ember-CLI, you can create a helper called "input-url" with the command ember g helper input-url and the only code you would need is the code within your formatValue() function:
helpers/input-url.js
// define patter globally so it's not recreated each time the function is called
var pattern = /^https{0,1}:\/\/[A-Za-z0-9]+\.[A-Za-z0-9]+/g;
export function inputUrl(value) {
if (pattern.test(value)) {
return value;
}
if (!pattern.test('http://' + value)) {
return value;
}
return 'http://' + value;
};
export default Ember.Handlebars.makeBoundHelper(inputUrl);
And you can use it like:
{{input-url PASS_YOUR_URL_HERE}}
Where the value you pass will be the value of the value variable within the helper.
You could also create a component, as #Gaurav suggested, using the exact code you have above, just in components/input-url.js instead and delete the helper cause it is not necessary anymore. You also have to edit the corresponding template of the component if you want it to display the value with a single handlebars expression:
templates/components/input-url.hbs
{{value}}
The usage with a component would be:
{{input-url value=PASS_YOUR_URL_HERE}}