Ember Octane and 2-Way-Binding. What are you recommend?

Ember 3.20 still has the Input-Template-Component

<Input @value=this.value />

which changes the value of the parent-component? What’s your recommendation for new Octane apps? Should we use the Input-component? Or should we go with native inputs? The second feels more correct, doesn’t it?

<input type="text" value={{this.value}} {{on "change" this.updateValue}} />

Another report: In this scene paramObject is an Ember-Object. Therefore value of paramObject gets changed at the child, although a param (@) is passed.

{{log @paramObject.value this.args.paramObject.value}}
<Input @value=@paramObject.value />
1 Like

I’d personally shy away from the built-in Input component, and would only use native ones:

<input type="text" value={{this.name}} {{on "input" this.updateName}} />
3 Likes

100% agree, I almost always use native inputs, even pre-octane.

FWIW, this stance is also what caused you to hit Why do I not need to mark the property as tracked? :smiling_imp:

Note, I don’t think it’s particularly bad to use native <input> by default, I just found the two posts funny/ironic when I viewed them together this morning :smile_cat:.

2 Likes

Fair enough :slight_smile: Now that I mostly understand why I needed to mark the property as tracked with a regular input tag (and why it seemed to work without it), I think I still prefer the native input but I get the point: Input “just works”.

Ya, and IMHO the (hopefully) simple mantra of “if I need to react to a change in this value, it should be @tracked” is really powerful.

1 Like

Thanks for your responses! Unfortunately I don’t quite understand it. So, if one uses the native-input, the input-value should marked as @tracked?

Please consider following use-case and please let me know your thoughts :grinning:

//routes/user-edit.js
export default class EditUserRoute extends Route {
  model() {
    // i.e. GraphQL Request, which returns a POJO
    return {
      firstName: 'Michael',
      lastName: 'Knight',
      address: {
        street: 'Schoolstreet 22',
        town: 'New York'
      }
    };
  }
}
//templates/user-edit.js
<UserEdit @user={{this.model}} />
//components/user-edit.js
export default class UserEditComponent extends Component {
  @tracked user = null; // I think this tracked is for nothing, isn't it?

  constructor() {
    super(...arguments);
    this.user = this.args.user;
  }

  @action updateFirstName({ target }) {
     this.user.firstName = target.value; // I know, that {{user.firstName}} is not rerendered if this action is triggered. I just want to know, what's your recommended solution to this issue. Please see below.
   }
}
{{!-- ... --}}
<input placeholder="Firstname" type="text" value={{this.user.firstName}} {{on "input" this.updateFirstName}}>
{{!-- ... --}}
<input placeholder="Street" type="text" value={{this.user.address.street}} {{on "input" this.updateStreet}}>
{{!-- ... --}}
{{user.firstName}}
<br>
{{user.lastName}}
<br>
{{user.address.street}}

Solutions:

  1. Convert the POJO from the model hook into a native Class.
class Address {
  @tracked street;
  @tracked town;

  constructor({ street, town }) {
    this.street = street;
    this.town = town;
  }
}
class User {
  @tracked firstName;
  @tracked lastName;

  constructor({ firstName, lastName, address }) {
    this.firstName = firstName;
    this.lastName = lastName;
    this.address = new Address(address);
  }
}
//routes/user-edit.js
export default class EditUserRoute extends Route {
  model() {
    // i.e. GraphQL Request, which returns a POJO
    return new User({
      firstName: 'Michael',
      lastName: 'Knight',
      address: {
        street: 'Schoolstreet 22',
        town: 'New York'
      }
    });
  }
}
  1. Keep the POJO and use { set } from '@ember/object'
  @action updateFirstName({ target }) { 
     set(this.user, 'firstName', target.value);
  }

For me 1. seems a bit costly. But what is better for the long run? Thanks

I understand @wuarmin because I’m new to Ember and the OFFICIAL guide uses Input yet I see many people discouraging the use of it.

This is a bit of a tricky spot. The use of <Input /> is still well-supported, and it handles a lot of things for you that you otherwise need to implement yourself. However, it’s also tricky because it comes from Ember Classic and does not work the way that a “normal” Ember Octane component would. In a Glimmer component (in Octane), you have to do Data Down, Actions Up. In Classic components, you don’t, and many didn’t. The result is sometimes confusing to people who have only ever worked in the Octane world.

The reason is that if you’re just used to working with Octane components, and you write this:

import Component from '@glimmer/component';

export default class MyComponent extends Component {
  myValue = 'hello';
}
<SomeOtherComponent @value={{this.myValue}} />

—well, you would not expect that SomeOtherComponent could change the value in your backing class. It’s not using @tracked, there’s no action being passed into SomeOtherComponent, so you’d normally assume that SomeOtherComponent cannot change myValue. But Input does change myValue if you do the same thing.

This “gotcha” is why a lot of people in the Ember community now prefer to use the native <input> element and wire it up manually. But as I said at the start of this comment: that’s a lot more work, and <Input> handles a lot of edge cases for you! So there’s a tradeoff here. At some point in the future, Ember might reimplement <Input> to use a Glimmer component and make it work the way you’d expect—but there’s a surprising amount of work to be done there to make that possible, so I wouldn’t expect to see that any time soon.

In the meantime, you can (and even should) confidently use the existing <Input> component if it solves your needs—just be aware that its behavior is different from other parts of the Octane experience.

6 Likes

Thanks @chriskrycho that was well explained. I wish the guides provided more context when referencing things that belong more to the Ember Classic way than the Octane way.

I think in general there needs to be a way to mention DDAU. Seems it’s a big part of Ember Octane.

2 Likes

thank you @chriskrycho for the explanation. That helped a lot. With that knowledge, I will use the <Input> component.

1 Like

In the meantime, you can (and even should ) confidently use the existing <Input> component

@chriskrycho If you need to handle oninput event of an input, there’s no other “Octane”-way as using the native input. Am I right?

<input
   value={{this.filterValue}}
   {{on "input" this.filter}}
>

Should this.filterValue should be marked as tracked in following case?

import Component from '@glimmer/component';
import { action } from '@ember/object';
import { tracked } from '@glimmer/tracking';

export default class FilterComponent extends Component {
  @tracked filterValue = null; // is this necessary

  @action
  filter({ target }) {
    //imagine the use of ember-concurrency to wait i.e. 40 ms before calling onFilter(value)
    const { value } = target
    this.filterValue = value;
    this.onFilter(value);
  }
}

That’s correct. The modifier is the Octane way of doing it—and it works nicely with server-side rendering, whereas oninput= does not.

@chriskrycho Can you go into more detail into what those edge cases are? Back in the day when we only had {{input}} and we always imported the whole of Ember (2015!) I spent some time “researching this” and resumed my findings in a blog post.

The bug that using the native <input> had back then was that the cursor jumped to the end each time you edited but this had been fixed a long time ago.

What else remains?

Thank you.

2 Likes

I believe that particular bug is indeed long-since gone. The main gotchas I know of at this point are (a) that wiring everything up is an enormous amount of work if you actually want to fully cover the API of input, and (b) that it’s possible to end up with surprising and incoherent behavior if you don’t do the wiring correctly, because all browser form elements are inherently stateful and that state does not especially know or care about your own backing state. The resulting bugs can be subtle, because what you actually see and interact with seems fine, but if it ends up out of sync stuff can get very weird. Classic two-way binding took care of that second concern for you, but with all the attendant downsides of two-way binding.

Now, those two things being true, I still personally prefer to use input directly, because Ember’s {{on}} modifier plus auto-tracking make for a really delightful way of managing the interaction between the two.

4 Likes

Thanks, Chris.

Summing up it seems like the native input works perfectly fine for 99% of the use cases. When it doesn’t work fine, it’s very hard to tell what’s wrong and to fix. Right?

I’d tweak it slightly: native input will work for 100% of use cases (after all, that’s how Input is built!) but needs more manual setup and can be a little tricky (not “very hard”; my mistake if I gave that impression) to debug: you need to actually understand its events rather than just rely on two-way binding.

To provide more context, in the Ember Guides, I plan to reorganize the contents in Built-In Components (the page will be renamed to Input Components), then add a section that explains how to use {{on}} with native inputs. My hope is to cover normal (common) use cases.

You can track the progress in Example of built-in component uses dasherized event · Issue #1485 · ember-learn/guides-source · GitHub.

3 Likes

that sounds great and will help lots of devs.

1 Like