Embedded Component question


#1

I have a requirement where a form contains a list of compound input fields, each consisting of a text field with a checkbox add-on:

It’s important to realize that each movement (flexion, extension, etc.) has a related opposite movement. For example, flexion is paired with extension, right lateral bending is paired with left lateral bending, and right rotation is paired with left rotation.

When ankylosis is NOT checked, all text fields are enabled. When ankylosis for a specific movement is checked, then its opposite movement must be disabled. For example, if the user clicked Ankylosis for flexion, then the entire extension input field and checkbox need to be disabled.

For this reason, each rom-field indicates its “opposite” control. Each rom-field is using an observer, so when a checkbox is checked, it gets notified, and I have access to all the details about the rom-field. However, when a checkbox is checked, I need somehow to notify the rom-group, so that it can look-up the opposite rom-field and disable it.

I cannot figure this puzzle out. Does anyone have any suggestions on how to send a message to the component’s parent without tightly coupling them?

Thanks


#2

There’s actually a feature in Canary (behind a feature flag) that is the implementation of this RFC that would help in this scenario…but for now, the one piece that you’re probably looking for is an emerging pattern in which you yield the “public API” of the parent component. In your case, it sounds like you need two things — first, a way to notify the parent that a field has been enabled, and some data yielded that describes which properties are disabled.

So, that might look like this (I’m leaving a bunch out of this — but hopefully it gives you a starting place):

// rom-group/component.js
export default Ember.Component.extend({
  enabledFields: {},
  fieldEnabled(fieldName) {
    this.set(`enabledFields.${fieldName}`, true);
  }
});

Here we yield the action and the data.

// rom-group/template.hbs
{{yield (action fieldEnabled) enabledFields}}

Then, your template that you showed would look something like:

// rom-field/template.hbs
{{#rom-group as |fieldEnabled enabledFields|}}
  {{rom-field enable=fieldEnabled allFields=enabledFields}}
  {{! -- etc... --}}
{{/rom-group}}

Then, your rom-field component will need to send that action (since it’s an action property, you’d do this.getAttr('enable')('fieldName')), and you’d need a way to determine wether that field is enabled based on the enabled fields data.


#3

Thank you for responding. Do you mind elaborating on a few concepts that are new to me?

  1. I’m not clear on the {{yield (action fieldEnabled) enabledFields}} line. I understand the yield, but what does the action do.

  2. I’ve tried to lookup what is going on with the 3rd part, {{#rom-group as |fieldEnabled enabledFields|}} but I’m familiar with the “as” only as it pertains to #each. What is that actually doing?

Would you be able to provide a little more description of what’s going on in each of those small code snippets?

Thanks


#4

You bet!

So, the action helper takes a function as an argument and makes sure that it is properly scoped for when it is later invoked by something else. I think that this blog gives a decent overview of the feature.

So, if you just passed that function directly — when it was later invoked, it would not be invoked in the parent component’s context. If you pass that function into the action helper first, that will essentially bind that function’s scope to the parent component.

If you take a look at this twiddle, you can see the difference of when you use the action helper vs when you don’t: https://ember-twiddle.com/a4107d0c70ef8291d808

In regards to the “as”…this corresponds to what you pass to the yield. So,

// my-component/template.hbs
{{yield propA propB propC}}

could be invoked as (argument names are irrelevant — only the order matters).

{{#my-component as |a b c|}}
  {{!-- content --}}
{{/my-component}}

This twiddle shows how that might look https://ember-twiddle.com/e8e0f86859a2b2ec87cb

Let me know if that makes sense! Happy to clarify.


#5

Thank you so much. The ember-twiddle was perfect, and I think I get it now. So, basically, the yield is passing parent parameters to the child. So, when something changes, the child can now execute the closure, which is actually in the parent.

I will tinker around with this and see if I can make it work. Thank you!


#6

OK, I have this partially working - when a checkbox is checked, it is now calling a parent component function with the id of the opposing control. How does the parent then disable that control? I am sure I could figure out a jquery way to do this, but is there a clean Ember way to do this?


#7

I think that if the parent component yields the list of ids that should be disabled, then the child components can compare itself against that list to see if it should be disabled.

Here’s a quick mock-up of what I mean: https://ember-twiddle.com/e5a7bad81cbe705df9f3

Hope that helps!!


#8

That is extremely helpful and very generous of you to take the time to put the mock-up together for me. I understand how it works except for one small detail that maybe you can shed light on. In the parent, the collection of disabled id’s are kept in an Ember.computed. What is the reason it must be in a computed?

Again, thanks so much!


#9

No problem — helps me stay fresh with Ember.

The reason for that is to make sure that we have a unique object for each of the instances of that component. This demonstrates the problem

let objType = Ember.Object.extend({
  propA: {},
  propB: Ember.computed(function() {
    return {};
  })
});

let objA = objType.create();
let objB = objType.create();

objA.set('propA.name', 'spencer');
objA.set('propB.name', 'price');

objB.set('propA.name', 'darth');
objB.set('propB.name', 'vader');

objA.get('propA.name'); // 'darth' (we'd expect/want 'spencer')
objB.get('propA.name'); // 'darth'

objA.get('propB.name'); // 'price'
objB.get('propB.name'); // 'vader'

#10

I think I encountered that problem while working on this. I found this https://github.com/emberjs/ember.js/issues/3908 discussion which I then initialized propA in a setter, which then made a unique array for each instance.

Is there really no way that a component can simply find out all of its embedded child components?

Anyway, this has been really helpful. Getting over this learning curve of Ember is a struggle and this clarified a few more pieces.