Skip to content
This repository has been archived by the owner on Jul 19, 2019. It is now read-only.

Public API: The Basics #138

Open
CMTegner opened this issue Jul 18, 2016 · 19 comments
Open

Public API: The Basics #138

CMTegner opened this issue Jul 18, 2016 · 19 comments

Comments

@CMTegner
Copy link
Collaborator

tl;dr: We want to make Autocomplete's public API better, and we need your input!

Current API, for reference:

value: any,
onChange: func,
onSelect: func,
shouldItemRender: func,
sortItems: func,
getItemValue: func.isRequired,
renderItem: func.isRequired,
renderMenu: func,
menuStyle: object,
inputProps: object,
wrapperProps: object,
wrapperStyle: object,
debug: bool,

Some thoughts on the current API:

  1. The fact that we need to wrap the input in another element before returning it is an implementation detail (due to how React works) and should not be visible to the user. wrapperProps and wrapperStyle are the main culprits. There is a way to do away with the wrapper entirely - by rendering the menu as a new React tree - but that brings with it extra bookkeeping and logic that we may not want to have to deal with in the long run. It also prevents relative-positioning of the menu, which is only possible when the menu is a child of the same node as the <input>.
  2. The user should be able to think of Autocomplete is an augmented <input> field, and use existing knowledge of the native input controls to quickly configure it. Passing value, onChange, className, placeholder, etc, should just work like it does on a <input> element. Currently the native <input> props are "hidden" behind inputProps, while only the Autocomplete-specific props are exposed as top-level props.
  3. Similarly, the user should be able to expect Autocomplete to behave and react like a native <input> element. Calling instance.focus() should focus the input, and instance.select() should select any text in the input. onFocus and onBlur should be triggered accordingly, as well as any other native event. Arrow keys should navigate text, and return should submit any surrounding form. The consumer shouldn't have to consider tradeoffs when choosing Autocomplete, it should be purely additive.
  4. shouldItemRender, sortItems, and how String#indexOf is used to determine matches in maybeAutoCompleteText should maybe not be of a concern to Autocomplete. We did a big improvement by moving value out of state, and I think we can further improve by handing more responsibility to the consumer.

In my mind Autocomplete is this:

An <input> field with typeahead support and the ability to render an accessible list of options with keyboard and mouse navigation.

I keep this in the back of my mind because it helps me focus on what Autocomplete should be doing above all else.

I suggest - to get started - that we pass all top-level props to the wrapped <input> (minus the ones that are Autocomplete specific, naturally), and thus removing inputProps. This also includes ensuring that any event handler passed in (onBlur, onClick, etc) are attached correctly, even if we use them internally.

Next, I suggest adding all instance methods which one would expect to find on a <input>, e.g. focus, select, blur, etc..

I'll leave this issue open for a bit before we start any work. Hopefully we can get a discussion going to ensure we're heading down the correct path.

@CMTegner
Copy link
Collaborator Author

/cc @sprjr

@whichsteveyp
Copy link
Contributor

There is a way to do away with the wrapper entirely - by rendering the menu as a new React tree - but that brings with it extra bookkeeping and logic that we may not want to have to deal with in the long run.

Aye, I'm not even sure how to do this entirely. I think our goal should be to only render two things: 1) <input /> that is enhanced, and 2) a drop down of the items to select. I think the wrapper is something most composers would really rather have full control over. Really all we do is pass props for them any way. I'm wondering if it would be better to help users with this instead. Some ideas:

<Autocomplete wrapper={MyComponent} {...rest} />

<Autocomplete {...props}>
  <MyComponent />
</Autocomplete>

This might allow us to avoid div soup, giving users full control of the wrapper in which they'd like to render the enhanced input into. Because we need to render 2 things (and we can't return siblings) this could be controlled, or we could perhaps default it nicely (e.g. it might be cool to use a <label> as the default to address some of our other a11y issues out of the box?). Just thoughts, if using React.Children.only() or passing out children to a wrapper is more complex than the other tactic you mentioned above, I'd like to learn more about it.

The user should be able to think of Autocomplete is an augmented field, and use existing knowledge of the native input controls to quickly configure it. Passing value, onChange, className, placeholder, etc, should just work like it does on a element.

Agreed. The simpler our API, the better. It might even be nice if we identify the props required by Autocomplete that are not native, and only expose those in propTypes. The other native props that it might rely on, but are configurable by the composers, we could simply add to getDefaultProps() with sensible defaults?

shouldItemRender, sortItems, and how String#indexOf is used to determine matches in maybeAutoCompleteText should maybe not be of a concern to Autocomplete.

maybeAutoCompleteText I think (iirc) seems to bounce all over the internals, and then it turns in to a function that I can't override and makes the component unusable for a few of the use case I have where I'd rather just take the value entered and hand back a controlled list of the matches (e.g. - don't necessarily want to hand it all matches and have it filter them down)

I think that we've grown the props list by simply adding props to fix problems, and not thinking about fixing the problems and then adding props to help with that. I'm all aboard in terms of simplifying that list and making it easier to reason about and interact with for sure.

@carlpaten
Copy link

I just wanted to mention that, as a user of react-autocomplete, the issue description here has been incredibly useful!

@CMTegner CMTegner self-assigned this Jul 21, 2016
@hoodsy
Copy link

hoodsy commented Aug 2, 2016

@CMTegner maybe this should be in the README, I keep coming back here 😁

@pcalves
Copy link

pcalves commented Sep 1, 2016

What's the status of this issue? Any roadmap for the improvements detailed in the original message?

I'm currently developing an application and resorting to some extremely "hacky" workarounds because I can't set a custom onBlur as a prop. I'd be happy to help implement some of these changes as well — this component is already great in so many ways, it just needs that extra push. 😊

@whichsteveyp
Copy link
Contributor

Hey Paulo, thanks for the offer to help out - more than happy to review a PR. Would rather that than you having to write very hacky code in your code.

Currently looking to ship @CMTegner's other PR soon - not sure if that behavior would help your situation out. For onBlur, is it not possible to pass it via inputProps any more? Not in front of the latest API currently so I could be wrong.

Feel free to send code this way if you'd like and we can get the polish/improve items shipped with you!

Sent from my iPhone

On Sep 1, 2016, at 7:33 AM, Paulo Coelho Alves [email protected] wrote:

What's the status of this issue? Any roadmap for the improvements detailed in the original message?

I'm currently developing an application and resorting to some extremely "hacky" workarounds because I can't set a custom onBlur as a prop. I'd be happy to help implement some of these changes as well — this component is already great in so many ways, it just needs that extra push. 😊


You are receiving this because you were mentioned.
Reply to this email directly, view it on GitHub, or mute the thread.

@pcalves
Copy link

pcalves commented Sep 1, 2016

Thanks for the reply, @sprjr!

For onBlur, is it not possible to pass it via inputProps any more?

Looking at the current code, it doesn't seem possible:

<input
  {...this.props.inputProps}
  role="combobox"
  aria-autocomplete="list"
  autoComplete="off"
  ref="input"
  onFocus={this.handleInputFocus}
  onBlur={this.handleInputBlur}
  onChange={(event) => this.handleChange(event)}
  onKeyDown={(event) => this.handleKeyDown(event)}
  onKeyUp={(event) => this.handleKeyUp(event)}
  onClick={this.handleInputClick}
  value={this.props.value} />

If you pass onFocus, onBlur, onChange, etc. via inputProps they'll be ignored because the component is already setting those events, right? #54 already handled this issue as well as I can think of, but that PR has been closed because "it'll likely be superseded shortly by the work that will be spun off as a part of #138." So I'm guessing @CMTegner probably had some other ideas as for how to handle this issue?

@CMTegner
Copy link
Collaborator Author

CMTegner commented Sep 1, 2016

@pcalves I've just started work on implementing my ideas, hopefully the work will materialise as a PR within next week. It will of course be a breaking change, so we may want to plan a few other things before we do a major release.

I'll see if have time tomorrow to extract the code which prevents clobbering the event handlers passed via props.inputProps. This will allow passing any and all props and event handlers that are supported by the native <input> element.

In the meantime, have a look at #163 and see if it's at all relevant to your situation.

@whichsteveyp
Copy link
Contributor

@pcalves ah yea, forgot there were overrides below it for that prop. My mistake!

@CMTegner
Copy link
Collaborator Author

CMTegner commented Sep 2, 2016

#165 contains some of the work described here.

@CMTegner
Copy link
Collaborator Author

CMTegner commented Sep 3, 2016

I have a WIP branch here with some of the proposed changes.

The fact that we need to wrap the input in another element before returning it is an implementation detail (due to how React works) and should not be visible to the user. wrapperProps and wrapperStyle are the main culprits. There is a way to do away with the wrapper entirely - by rendering the menu as a new React tree - but that brings with it extra bookkeeping and logic that we may not want to have to deal with in the long run. It also prevents relative-positioning of the menu, which is only possible when the menu is a child of the same node as the .

I settled on props.wrapperComponent, like you suggested @sprjr. I first thought that dropping props.wrapperStyle and only keeping props.wrapperProps would be a good solution, but that surfaced a different problem: Overriding inline styles is really hard. Assuming the code looks like this:

<div style={{ display: 'inline-block' }} {...props.wrapperProps}>
  <input />
  <Menu />
</div>

There's no elegant way of removing display: inline-block without overriding styles in wrapperProps. If you want to inherit styles from your CSS framework (which is probably the common case), you need to do something along the lines of: wrapperProps: { style: {}, className: '...' }}. Not great.

props.wrapperComponent lets us have our own wrapper implementation internally, which is easily replaced by the user. It can be specified as a string (native DOM component), a function (stateless React component), or a React element (component class). Basically anything you can pass to React.createElement().

I chose span since its default display setting is inline, which is close to inline-block. It also saves having to actually implement a class internally.

The user should be able to think of Autocomplete is an augmented field, and use existing knowledge of the native input controls to quickly configure it. Passing value, onChange, className, placeholder, etc, should just work like it does on a element. Currently the native props are "hidden" behind inputProps, while only the Autocomplete-specific props are exposed as top-level props.

This was easy: just pass everything to <input>, minus the Autocomplete-specific props.

Similarly, the user should be able to expect Autocomplete to behave and react like a native element. Calling instance.focus() should focus the input, and instance.select() should select any text in the input. onFocus and onBlur should be triggered accordingly, as well as any other native event. Arrow keys should navigate text, and return should submit any surrounding form. The consumer shouldn't have to consider tradeoffs when choosing Autocomplete, it should be purely additive.

There's currently no elegant way of doing this. What I've implemented is a solution which proxies all (minus browser-specific ones) of HTMLInputElement's imperative APIs to the public instance returned from Autocomplete. This isn't great for several reasons: browser inconsistencies, future-proofness, custom methods on Input.prototype, etc..

What I actually want is facebook/react#4213. Having something like getPublicInstance() would enable us to simply return the <input> and have that be our public API.

You can mimic this behaviour by supporting a callback which receives the ref of the <input>. Sadly you can't actually use props.ref since it gets stripped away by React, but pretty much any other name would work (e.g. props.inputRef): <Autocomplete inputRef={el => this.input = el} />.

I'd like some input on this from anyone who has an opinion or experience with the matter.

That's as far as I've gotten. I intend to continue work and deal with point no. 4 in the coming days. All of this work will naturally lead to quite a few breaking changes, so I plan on overhauling the menu/item presentation API (which will likely also deal with the default menu styles) while I'm at it.

@davis
Copy link

davis commented Feb 1, 2017

is there a sane default for shouldItemRender? A basic searching algorithm?

@CMTegner
Copy link
Collaborator Author

CMTegner commented Feb 1, 2017

@davis shouldItemRender() defaults to returning true for everything, i.e. every item is rendered.

@davis
Copy link

davis commented Feb 2, 2017

@CMTegner i feel like a group of nicer defaults like "caseSensitiveMatch" "caseInsenstitiveMatch" etc could be nice. you could allow the user to just provide a function that 'gets' the value that should be compared (totally a feature request)

@CMTegner
Copy link
Collaborator Author

CMTegner commented Feb 5, 2017

@davis yes, that would be very nice. Having a set of curated, frequently used/desired behaviours that allow out-of-the-box use is great for users that have basic requirements. That being said, I don't necessarily think that it would belong in this module. What we're aiming for first and foremost is to create a set of solid APIs that allow others (the application developer or other module creators) to build atop of.

For example, it is fairly easy to create a component that wraps Autocomplete that could provide a set of implementations for shouldItemRender:

class MyAutocomplete extends Component {

  propTypes = {
    match: PropTypes.oneOf(['begin', 'any'])
  }

  startsWith (item, value) { return item.indexOf(value) === 0 }

  anywhere (item, value) { return item.indexOf(value) > -1 }

  render () {
    return (
      <Autocomplete
        items={this.props.items}
        shouldItemRender={this.props.match === 'begin' ? this.startWith : this.anywhere}
        ...
      />
    )
  }
}

Start adding other props like caseSensitive and regex and you suddenly have a advanced, customisable component that doesn't have to implement the basic functionality (like keyboard navigation, markup, a11y ,styling, etc).

@ahmadie
Copy link

ahmadie commented Aug 28, 2017

cannot figure out how to add className to <input/> as doing this

< ReactAutocomplete className="form-control"/>

is not working for me on current version

never mind this works → inputProps={{ className: "form-control"}}

@CMTegner
Copy link
Collaborator Author

Hi @ahmadie! You're probably looking for inputProps.className (if you want to set a class on the <input>) or wrapperProps.className (if you want to set a class on the element that wraps the input and menu). If you're still having trouble please feel free to open a new ticket so we can discuss it further :)

@thisisraghu1993
Copy link

can anyone please share the detailed code to increase the input box size

@gkostov
Copy link

gkostov commented Mar 24, 2018

I was wondering how the props.wrapperComponent idea was going? Ideally I would really love to be able to treat the autocomplete component as the bare minimum it could be - an attachment to a control that I specify. jQuery's UI library does it exactly like that and I find it extremely convenient - I have my user input field and wrapper controls and anything else, and the autocomplete plugin won't mess with those but will only attach event listeners, generate the dropdown DOM (where and how I specify) and provide logic for the autocomplete feature. I know React isn't as flexible but the closer this component can go the better ;-)

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

No branches or pull requests

9 participants