Wrapper Component for Form Controls - ember.js

I'm looking for a pattern to make a wrapper component for form controls.
Its role: wrap any kind of component, (handle error states, messages)
Syntax:
{{form-control-group type="my-input" label="Some fancy label:"}}
templates/components/form-control-group.hbs
{{#if hasError }}
{{error}}
{{/if}}
{{label}}
{{component type}}
It works nice, as long as I don't need any attribute for my-input component. Usually it is not the case. Let's say I need pass the value. I could do
{{component type value=value}}
But as not that flexible as I wanted to have it. I will gonna use it with different form controls, ex.: select, checkbox etc. They all need different attributes.
I also could prepare for this different attributes:
{{component type
value=value
content=content
contentPath=contentPath
disabled=disabled
checked=checked
...
}}
In this way it can be used only with the component it is prepared for. If it is published as part of a form addon, and its consumer want to use with a datapicker component, it won't pass down the attributes.
Question:
Is there a way to stream down ALL the attributes passed to form-control-group component?
Maybe the whole concept is wrong, and there is a better pattern to handle this. Any critics and / or ideas are most welcome.
Edit
To be clear: in this question: "setProperties" does not pass "attrs" as expected I tried to solve this very problem, passing down the attrs hash, which contains all the attributes. Maybe Ember is not ready for this, but I'm sure there is a workaround.

Related

Can I let a user customize a component's template?

I'm making a sortable component. You could imagine a simple implementation having the following API:
{{sortable-list content=orderedQuestions tag='ul' itemTag='li'}}
But, that offers pretty limited template customization. Ideally I'd like the API to allow something like this:
{{#sortable-list content=orderedQuestions tag='ul'}}
<li>
<h2>{{title}}</h2>
<div>Some more {{details}}</div>
</li>
{{/sortable}}
where this block template gets used for each sortable item. Is this possible? I tried something like this for the component's template:
{{#each item in content}}
{{#with item}}
{{yield}}
{{/with}}
{{/each}}
but this doesn't work, since the block template the user passes into the component has the controller as its context.
Is what I'm after possible?
The exact thing you're trying to do is not currently possible.
Take a look at this Github issue to see a lot of discussion around component use cases like this. In particular take a look at Trek's first comment.
An alternative is to break your component into multiple components:
{{#sortable-list content=orderedQuestions tag='ul'}}
{{#each orderedQuestions}}
{{#sortable-item tag='li'}}
<h2>{{title}}</h2>
<div>Some more {{details}}</div>
{{#sortable-item}}
{{/each}}
{{/sortable}}
If you think about components in terms of existing HTML elements you can see that it kind of looks a little bit like a select which has options inside. To make this work it's likely that the sortable-items would need to communicate with the sortable list. One option is to use parentView in your component to get access to the parent.
Take a look at this talk from Ryan Florence to get more ideas about how to deal with composable components.
Update
Updated my answer to include an each around {{sortable-item}} to change the context.
Reading through the Ember each helper it seems that it would be possible for a component to do what you want but it looks really hairy.

Handlebars debug missing {{property}} of model

I would like to see in the console some kind of output if a template doesn't get all {{properties}} (for debugging)
e.g. If a template
<script type="text/x-handlebars" id="demo">
<h1>{{name}}</h1>
<p>{{desc}}</p>
</script>
is called with a model
{
name:"Max",
}
there should be an output like console.warn
Template "demo" could not find property "desc"
Is there any helper, callback, ... that is called every time a Handlebars expression like {{property}} is called?
You can do whatever you like with javascript. But do it carefully. :)
There are many way to achieve your goal depending on what you really need and how many time you can spend on hacking ember/handlebars/ember-data packages.
First and easiest variant is to override render method of SimpleHandlebarsView/_HandlebarsBoundView, which displays simple binded values (there are few other views: for #each, with, etc). This is simple sequence: override method, call an old method, check the result of getting value for rendering, console.warn if result === undefined. You can't get template name inside that method, but you can get parent view for rendered value. So, I suggest to turn on LOG_VIEW_LOOKUPS to easily match views with templates.
Example of console logs:
Rendering application with default view <(subclass of Ember.View):ember226> Object {fullName: "view:application"}
Rendering index with default view <(subclass of Ember.View):ember251> Object {fullName: "view:index"}
Rendering <(subclass of Ember.View):ember251> , property is undefined: model.undefined
JSBin with example.
Another way is to override handlebarsGet method from ember-handlebars-ext package: https://github.com/emberjs/ember.js/blob/v1.7.0/packages/ember-handlebars/lib/ext.js#L75.
Third, if you need to catch accessing to undefined properties of Ember.Objects, you can override unknownProperty method for those objects.
If you need to check a rendered data quickly, you can use {{log model}} method in Handlebar's template.
You can get deeper into patching and rewriting core of Ember, of course. But I advise not to rely much on inner structure of libraries. Good luck!
No, they use Ember.get(context, propertyName) There is no metadata returned saying the property is defined vs undefined.

Value binding to existing HTML elements

As Emberjs suggests in order to bind elements values/attributes etc.. to controllers I have to create that element using Emberjs. For example:
App.SearchTextField = Em.TextField.extend({ });
and the view:
{{view App.SearchTextField placeholder="Twitter username"
valueBinding="App.tweetsController.username"}}
This means that pretty much the entire content of the page should be under Emberjs control. This isn't very convenient.
Is is possible to do any type of value or event binding to an existing element?
In order for bindings to work, Ember needs to know when a bound value changes so that it syncs it. For Ember to know that a value has changed, you need to use Ember.set. Example:
object.set('value', 'I changed');
// This says: Ember, my value has changed, please sync!
So, when a certain property changes without using Ember.set, Ember doesn't know that it should sync it. As a result, we need to listen to this event change, and tell Ember that it changed using Ember.set.
That's what happens on input values. When you type, the value changes, but Ember.set was not called. So what Ember.TextField does, is listen to keyup and change and other events and calls Ember.set when one of these events is fired.
Of course, you don't have to use them, but then you will have to manually listen to these changes and tell Ember that the value changed: this.set('value', this.$().val()))
I don't see why you have a problem with using Ember.TextField as it can do almost anything, and all it does it save you the trouble of listening to the events yourself.
One thing that might help you is that you can directly use it in your template, instead of creating a view to extend it:
{{view Em.TextField placeholder="Twitter username" valueBinding="username" type="search"}}
which is almost the same amount of typing as:
<input type="search" placeholder="Twitter username" value="some value" />
Note: From the example you provided above, it looks like you are following an old tutorial, be careful, lots have changed since then.

How can we load multiple instances into a template?

We're trying to put together a portal, where a layout can have any number of core widgets in any sequence in the main layout.
To simulate this, we've got a number of outlets:
<h1>{{title}}</h1>
{{outlet pos1}}
{{outlet pos2}}
{{outlet pos3}}
{{outlet pos4}}
{{outlet pos5}}
{{outlet pos6}}
{{outlet pos7}}
{{outlet pos8}}
{{outlet pos9}}
{{outlet pos10}}
And in the router, we're attempting to load them in one by one:
connectOutlets: function(router, group) {
router.get('applicationController').connectOutlet('group', group);
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
However, when there are more than one, the final context is used. So in this example, we get two instances of the toDo object, both of which are for id #15.
Am I approaching this in the right way and is it possible to do this programatically, rather than having a fixed layout of outlets?
Thanks,
Dan
Edit: My answer is based on the assumption that this complex solution is really needed in your case. Based on your simple example one could also say, that you could use an ArrayController for all your ToDo items. But here is my try on the answer to the complex problem:
the problem are the following 2 lines:
router.get('groupController').connectOutlet('pos9', 'toDo', App.ToDo.find(41));
router.get('groupController').connectOutlet('pos3', 'toDo', App.ToDo.find(15));
What you basically do there is:
Connect the outlet with name pos9 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 41.
Connect the outlet with name pos3 with the Controller named 'todo'. Set the content of this controller to ToDo with Id 15 (so you are overriding the content of the same controller).
The result is that you end up with both outlets connected to the same instance of a controller. And you the same ToDos since you have set the content property of this single instance twice. The core problem is from my point of view: EmberJS uses single instances of controllers by default.
So my solution approach would be to instantiate a new Controller for each outlet you have. Unfortunately this also requires modification to the lookup of the View. As you likely know, Controller and View are matched by name. So roughly the algorithm would be in pseudocode:
Create new instance of Controller, e.g.: var newController = App.ToDoController.create();
Inject this controller into the router with the appropriate name, e.g. router.set('todoControllerForPos9', newController);
Based on this name, you must enable Ember to find the matching view, e.g. App.set('TodoControllerForPos9View', App.ToDoView);
Finally call connectOutlet on the router, e.g.: router.get('groupController').connectOutlet('pos9', 'todoControllerForPos9', App.ToDo.find(41));
As you might guess, i ran into this problem myself. I did ask this question before and this is the solution, i came up with. I think, this is a missing feature i ember. I call it dynamic outlet names. See my original question here: How to implement a list of multiple elements, where an element can be expanded by click? (Dynamic Outlet Names?)
Have a look at the Update section and the fiddle provided there and you will recognize my pseudo code provided here.
It would be still great, if someone could have a look at my solution, because it is still hacky at the moment, but seems valueable to me. Hope this will gain some attention now with this big answer :-)

Instantiate a controller class using the {{view}} helper?

While connectOutlet("basename") automatically creates an instance of BasenameController, I was wondering if there's a way to do the same using the {{view}}-helper.
I have tried several things I've seen in examples, but non of them seem to work:
{{view controllerBinding=App.BasenameController}}
{{view controllerBinding=App.basenameController}}
{{view controllerBinding="App.BasenameController"}}
{{view controllerBinding="App.basenameController"}}
I have also tried to do the same using controller instead of controllerBinding, unfortunately without success, and I was also unable to find out where exactly the difference is between the two of them.
Does anybody know how to achieve my goal?
You probably want to use an outlet. The connectOutlet/outlet functions are meant for rending other controller/view pairs.
Lets say we have a person view, but inside that view we want to have another controller/view pair. For this, we need to use a named outlet, our template would look like this.
Person View!
{{name}} = the person's name!
{{controller}} = PersonController!
{{outlet other}} = our outlet
Then inside the router when you want to attach another controller/view to that outlet you can simple use connectOutlet on the personController.
router.get('personController').connectOutlet('other', 'other');
That will wire OtherController and OtherView together and display them in your template. The first param is the outlet name, the 2nd is the controller/view.
This allows you to easily swap different controllers and views onto that outlet. For example, using a different connectOutlet api, we could
router.get('personController').connectOutlet({
outletName: 'other',
controller: router.get('carsController'),
viewClass: App.CarsView
});
...
Btw, to answer you original question. You can get access to other controllers from your view by doing this: {{view controllerBinding="controller.target.otherController"}}. Every controller will have a target property that points back to the router. However, I do not recommend using this code. It's brittle, hard to test, hard to debug, and will come back and bite you in the future.