Lets say I have an array of widget objects on my controller and each widget object has member variable that is assigned the name of a component class. How can I get my template to invoke that component?
//widgets[0].widget.componentClass="blog-post"
{{#each widget in widgets}}
{{widget.componentClass}}
{{/each}}
Obviously the above example just spits out a series of string versions of the widget component classes. This however does work (as long as you got everything set up right):
//widgets[0].widgets.viewClass="blogPost"
{{#each widget in widgets}}
{{view widget.viewClass}}
{{/each}
That was our previous implementation, but we weren't happy with it. We're currently using a custom {{renderWidget ...}} tag with a handlebars helper as described here: Calling Handlebars {{render}} with a variable name. The default render helper has a similar problem where it would not invoke a render on the contents of a variable name. I'd be willing to write a custom component handlebars helper but I can't even figure out where to start. Thanks.
I tried this and it seems to work, but its just a lot of guesswork on my part:
Ember.Handlebars.registerHelper('renderComponent', function(componentPath, options) {
var component = Ember.Handlebars.get(this, componentPath, options),
helper = Ember.Handlebars.resolveHelper(options.data.view.container, component);
helper.call(this, options);
});
and you use it the same way:
{{#each widget in widgets}}
{{renderComponent widget.componentClass widget=widget}}
{{/each}}
In Ember 1.11, the new component helper allows you to do this:
{{#each widget in widgets}}
{{component widget.componentClass}}
{{/each}}
As of today, Jan 19th, 2015, 1.11 is not a stable release but this feature is in the canary version.
Sounds like I've run into a lot of the same problems as you. All components are registered as top level helpers, which means you can do a similar method to the one you linked of creating a handlebars helper that does the lookup. Like this:
Ember.Handlebars.registerHelper('lookup', function(component, options) {
component = Ember.Handlebars.get(this, component, options);
Ember.Handlebars.helpers[component].call(this, options);
});
Then in your template:
{{#each widget in widgets}}
{{lookup widget.componentClass}}
{{/each}}
Here's a jsbin with a working example: http://jsbin.com/ucanam/2482/edit
Hope that helps!
-- edit --
For some reason, the new version of handlebars makes calling helpers from other helpers impossible. Instead you can lookup the template through the Ember.TEMPLATES global. I've updated the JSBin to use this method.
You can also get the template via options.data.view.templateForName(component), but it feels a bit more brittle than Ember.TEMPLATES.
-- edit 2 --
It's changed again. Ember.Handlebars.resolveHelper is now the correct way to do it. See #StrangeLooper's answer.
If you are using Ember CLI and Coffeescript here is a version for that. Create the following file in app/helpers/render-component.coffee:
renderComponent = (componentPath, options)->
helper = Ember.Handlebars.resolveHelper(options.data.view.container, componentPath)
helper.call this, options
`export { renderComponent }`
`export default Ember.Handlebars.makeBoundHelper(renderComponent)`
From there, you can call {{render-component "foo-bar"}} from a template.
Since the Ember ecosystem is ever changing, here is the version I tested it on:
Ember-CLI v0.0.43
Ember v1.7.0
Handlebars 1.3.0
Related
I just started using Sapper (https://sapper.svelte.technology) for the first time. I really like it so far. One of the things I need it to do is show a list of the components available in my application and show information about them. Ideally have a way to change the way the component looks based on dynamic bindings on the page.
I have a few questions about using the framework.
First, I'll provide a snippet of my code, and then a screenshot:
[slug].html
-----------
<:Head>
<title>{{info.title}}</title>
</:Head>
<Layout page="{{slug}}">
<h1>{{info.title}}</h1>
<div class="content">
<TopBar :organization_name />
<br>
<h3>Attributes</h3>
{{#each Object.keys(info.attributes) as attribute}}
<p>{{info.attributes[attribute].description}} <input type="text" on:keyup="updateComponent(this.value)" value="Org Name" /></p>
{{/each}}
</div>
</Layout>
<script>
import Layout from '../_components/components/Layout.html';
import TopBar from '../../_components/header/TopBar.html';
let COMPONENTS = require('../_config/components.json');
export default {
components: {
Layout, TopBar
},
methods: {
updateComponent(value) {
this.set({organization_name: value});
}
},
data() {
return {
organization_name: 'Org Name'
}
},
preload({ params, query }) {
params['info'] = COMPONENTS.components[params.slug];
return params;
}
};
</script>
Now my questions:
I notice I can't #each through my object. I have to loop through its keys. Would be nice if I could do something like this:
{{#each info.attributes as attribute }}
{{attribute.description}}
{{/each}}
Before Sapper, I would use Angular-translate module that could do translations on strings based on a given JSON file. Does anyone know if a Sapper/Svelte equivalent exists, or is that something I might need to come up with on my own?
I'm not used to doing imports. I'm more use to dependency injection in Angular which looks a bit cleaner (no paths). Is there some way I can create a COMPONENTS constant that could be used throughout my files, or will I need to import a JSON file in every occurence that I need access to its data?
As a follow-up to #3, I wonder if there is a way to better include files instead of having to rely on using ../.. to navigate through my folder structure? If I were to change the path of one of my files, my Terminal will complain and give errors which is nice, but still, I wonder if there is a better way to import my files.
I know there has got to be a better way to implement what I implemented in my example. Basically, you see an input box beside an attribute, and if I make changes there, I am calling an updateComponent function which then does a this.set() in the current scope to override the binding. This works, but I was wondering if there was some way to avoid the function. I figured it's possible that you can bind the value of the input and have it automatically update my <TopBar> component binding... maybe?
The preload method gives me access to params. What I want to know if there is some way for me to get access to params.slug without the preload function.
What would be really cool is to have some expert rewrite what I've done in the best possible way, possibly addressing some of my questions.
Svelte will only iterate over array-like objects, because it's not possible to guarantee consistent behaviour with objects — it throws up various edge cases that are best solved at an app level. You can do this sort of thing, just using standard JavaScript idioms:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} ...</p>
{{/each}}
<!-- or, if you need the key as well -->
{{#each Object.entries(info.attributes) as [key, value]}}
<p>{{attr.description}} ...</p>
{{/each}}
Not aware of a direct angular-translate equivalent, but a straightforward i18n solution is to fetch some JSON in preload:
preload({ params, query }) {
return fetch(`/i18n/${locale}.json`)
.then(r => r.json())
.then(dict => {
return { dict };
});
}
Then, you can reference things like {{dict["hello"]}} in your template. A more sophisticated solution would only load the strings necessary for the current page, and would cache everything etc, but the basic idea is the same.
I guess you could do this:
// app/client.js (assuming Sapper >= 0.7)
import COMPONENTS from './config/components.json';
window.COMPONENTS = COMPONENTS;
// app/server.js
import COMPONENTS from './config/components.json';
global.COMPONENTS = COMPONENTS;
Importing isn't that bad though! It's good for a module's dependencies to be explicit.
You can use the resolve.modules field in your webpack configs: https://webpack.js.org/configuration/resolve/#resolve-modules
This would be a good place to use two-way binding:
{{#each Object.values(info.attributes) as attr}}
<p>{{attr.description}} <input bind:value=organization_name /></p>
{{/each}}
Yep, the params object is always available in your pages (not nested components, unless you pass the prop down, but all your top-level components like routes/whatever/[slug].html) — so you can reference it in templates as {{params.slug}}, or inside lifecycle hooks and methods as this.get('params').slug, whether or not a given component uses preload.
I am using paper-data-table which is a extension to ember-paper.
Both use a technique I did not see before which I would describe as "template helpers create template helpers".
Here is a simple example of the ember-paper toolbar component
{{#paper-toolbar as |toolbar|}}
{{#toolbar.tools}}
{{#paper-button}}
Go Back
{{/paper-button}}
<h2>Toolbar with Standard Buttons</h2>
<span class="flex"></span>
{{#paper-button raised=true}}
Learn More
{{/paper-button}}
{{#paper-button mini=true aria-label="Favorite"}}
{{paper-icon "favorite"}}
{{/paper-button}}
{{/toolbar.tools}}
{{/paper-toolbar}}
There is a new template helper created {{#paper-toolbar as |toolbar|}}.
In my use-case I want to pass the row template helper which is created by the paper-data-table template helper(/component?) down to another component to encapsulate the logic inside it.
I tried to pass it down as a argument:
{{#paper-data-table
sortProp='sort'
sortDir='asc'
as |table|
}}
{{#table.body as |body|}}
{{#each questions as |question index|}}
{{question-row
row=body.row
}}
{{/each}}
{{/table.body}}
{{/paper-data-table}}
But when trying to use the helper(/component) in the template of the question-row component
{{#row as |row|}}{{/row}}
I get the following error:
Assertion Failed: A component or helper named "row" could not be found Error
So I wanted to ask if thats possible and how that would work.
This method is called contextual components and I was able to solve it with the following code in my question-row component:
{{#component row as |row|}}
{{#row.cell}}
HALLO
{{/row.cell}}
{{/component}}
I am upgrading my
Emberjs => from 1.10.0 to 1.12.0
Ember-cli => from 0.1.12 to 0.2.5
While i am figuring out most of the deprecations there are few which i am not able to understand. PFB the same
DEPRECATION: Ember.required is deprecated as its behavior is inconsistent and unreliable. Where is this used and how to change it?
DEPRECATION: lookupFactory was called on a Registry. The
initializer API no longer receives a container, and you should use
an instanceInitializer to look up objects from the container.
I do understand this issue but my initializer does not use lookup at all. PFB the code of my initializer.
//app/initializer/abc
initialize: function(registry, app) {
app.register('store:main', Store);
// Inject into each route a store property with an instance of store:main
app.inject('route', 'store', 'store:main');
// Inject into each controller a store property with an instance of store:main
app.inject('controller', 'store', 'store:main');
}
//app/initializer/def
initialize: function(registry, app) {
// Register the session object.
app.register('session:main', Session);
// Inject the session object into all controllers.
app.inject('controller', 'session', 'session:main');
}
DEPRECATION: Using the context switching form of {{each}} is
deprecated. Please use the block param form ({{#each bar as
|foo|}}) instead. See
http://emberjs.com/guides/deprecations/#toc_more-consistent-handlebars-scope
for more details.
I understand here that {{#each foo in bar itemController="abc"}} should be changed to {{#each bar itemController="abc" as |foo|}}. But my code is as below and does not have "in", meaning using this context!
{{#each paged itemController="class.adm.man.stop-term"}}
How can i change this?
Following your list:
Seems like it's an ember-data related issue. If not, I'm sure there is another addon which use it, but not you. Thus nothing you can do.
The same thing. I've already introduced an example as a comment. You code looks good for me, so, I believe, nothing you can do there as well.
Ember tries to make more consistent and explicit scopes. in part is not deprecated for now, so the simplest solution is to add dummy in part, for example {{#each page in paged itemController="class.adm.man.stop-term"}}. But in general I'd recommend to use more complex solution - to create an ItemList component and refactor it as:
{{#each paged as |page|}}
{{item-list model=page}}
{{/each}}
I've got a sticky situation that I keep on running into: The need for a new instance of a controller inside a handlebars template.
Here is a brief example of my situation. (Please excuse my use of coffeecript)
In Ember, I have a model:
App.Foo = DS.Model.extend
attr: DS.attr()
...
Which I load from an endpoint etc.. And place into an array controller:
App.FooArray = Ember.ArrayController.extend
###*
* Array of App.Foo
* #type {Array}
*/
content:
method: ->
...
Finally, I have an 'instance' controller for this model, which implements further methods (i.e. this is not a singleton controller as would be found at the router level, but a decorator (or proxy) that augments the model with added methods and event handlers):
App.FooController = Ember.ObjectController.extend
###*
* Content
* #type {App.Foo}
*/
content: null
action: ->
...
In handlebars, I want to iterate over items in an App.FooArray:
{{#each myFooArray}}
Hi! My attr is {{attr}}
{{/each}}
etc.. This works splendidly for parameters and such.
However, the trouble starts when I want to use actions (or other properties which would belong to a FooController)
{{#each myFooArray}}
Hi! My attr is {{attr}} <a {{action 'action'}}>Action me!</a>
{{/each}}
Suddenly my actions are not working. That's because the action helper doesn't apply the action to 'this' but rather to a controller higher up, possibly even at the Route level!
So to work around this, I need to pass a target (i.e. a controller):
{{action 'action' target=**********}}
Well, the controller I want is an instance of App.FooController.
Up until now, I've been instantiating controllers inside the model (yuck!):
App.Foo = DS.Model.extend
attr: DS.attr()
...
attrn: DS.attr()
myController: Ember.computed (->
App.FooController.create
content: this
)
and thus iterating as follows:
{{#each myFooArray}}
Hi! My attr is {{attr}} <a {{action 'action' target=myController}}>Action me!</a>
{{/each}}
I know this is bad, but I can't think of a better way. Somebody, please help me see the light!
You can explicitly set the itemController in your each loop.
{{#each myFooArray itemController="foo"}}
Hi! My attr is {{attr}}
{{/each}}
This question poses an important and longstanding question about ArrayControllers, CollectionViews, Models and ObjectControllers.
At the time of writing, my knowledge of the inner workings of Ember was limited. However, I can rephrase my question more concisely as follows:
Given an ArrayController, CollectionView and instance controllers for a model, how can one leverage the itemControllerClass property of the ArrayController to iterate over its content and wrap each item in a unique (i.e. non-singleton) instance of itemController?
Turns out this problem is longstanding and the solution echoes #jeremy-green but I will expand on things here.
First off though: the Ember support thread that encapsulates the problem: https://github.com/emberjs/ember.js/issues/1637
I think the discussion there points very clearly to the need for non-singleton controllers in certain situations.
As well, here is the documentation for ArrayController that indicates the presence of an 'itemController' property on the ArrayController: http://emberjs.com/api/classes/Ember.ArrayController.html#property_itemController
Looking further into the docs, you will also note the presence of an 'lookupItemController' function: http://emberjs.com/api/classes/Ember.ArrayController.html#method_lookupItemController
These functions are for the express purpose of returning the content as an array of Controllers but how?
Well the first requirement is to use the ArrayController directly as the content in a loop. Unfortunately this is where things start to fall apart.
You may think it would be simply the case that a CollectionView can be used:
{{view myCollectionView controllerBinding=myArrayController}}
or
{{view myCollectionView contentBinding=myArrayController}}
But unfortunately this is not the case. More so in the situation where you are rendering a Controller on another Controller's route. CollectionView does not preserve the relationship between the ArrayController and its 'itemControllerClass`
The only way to make this work, as #jeremy-green points out:
{{#each myFooArray itemController="foo"}}
Hi! My attr is {{attr}}
{{/each}}
A more complete example would be:
<ol class="foo-item-list">
{{#each controllers.foo_items}}
<li>{{ view "foo" }}</li>
{{/each}}
</ol>
Wherein App.FooItemsController has either the property itemController or lookupItemController defined.
Unfortunately in this situation we lose the benefits of using the tagName or emptyView properties of CollectionView. Hopefully if an 'ArrayView' is ever created, it will bring the best of both worlds to this situation!
To the handlebars (version 1.0.0-rc.3) template I am passing two variables , one is the json and the other one is the string containing the current language on site.
self.template = template({ data: self.model, lang:self.lang });
Then inside the template file I have the following structure:
{{#each data}}
//this is working
{{../lang}}
{{#if this.title}}
{{this.desc}}
//i've tried this
{{../lang}}
//and this
{{lang}}
{{/if}}
{{/each}}
...but I couldn't access the lang value inside the if statement. What am I doing wrong?
I know you already solved your issue with a workaround but registering a Helper for doing a native way is cumbersome.
The thing is that every Handlebars helper overwrites the context and nest the new one inside the parent one, so you have to go up uone step further, like a UNIX like directory.
So, to access lang inside an each->if you have to use:
{{ ../../lang }}
I've find a solution by creating a handlebars helper function:
Handlebars.registerHelper('language', function() {
return self.lang; });
Then in the template i could use {{language}}
where ever I want.