Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Converting existing Toggle component to use Foundation (in experiments) #6959

Merged
merged 19 commits into from
Nov 7, 2018
Merged

Conversation

khmakoto
Copy link
Member

@khmakoto khmakoto commented Nov 2, 2018

Pull request checklist

  • Addresses an existing issue: Fixes #0000
  • Include a change request file using $ npm run change

Description of changes

Converting existing Toggle component to use Foundation.

Focus areas to test

(optional)

Microsoft Reviewers: Open in CodeFlow

* Optional callback to access the IToggleComponent interface. Use this instead of ref for accessing
* the public methods and properties of the component.
*/
componentRef?: IRefObject<IToggleComponent>;
Copy link
Member

@JasonGore JasonGore Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BaseComponent manages componentRef for existing styled components. Since Toggle is extending BaseState and not BaseComponent, this currently won't be managed properly.

@dzearing I know we've talked about BaseComponent being bulky, but what do you think about having a newer, more minimal BaseComponent that'll handle componentRef? componentRef seems fairly universal for our components.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm thinking BaseState could expand or contain this more minimal BaseComponent functionality.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So, should I just take out componentRef? I did have to change one test because componentRef was working weird, basically checking the same thing without using it.

Originally, the test was:

it('doesn't trigger onSubmit when placed inside a form', () => {
    let component: any;
    const onSubmit = sinon.spy();

    const wrapper = mount(
      <form
        action="#"
        // tslint:disable-next-line:jsx-no-lambda
        onSubmit={e => {
          onSubmit();
          e.preventDefault();
        }}
      >
        <Toggle
          // tslint:disable-next-line:jsx-no-lambda
          componentRef={ref => (component = ref)}
          label="Label"
        />
      </form>
    );
    const button: any = wrapper.find('button');
    // simulate to change toggle state
    button.simulate('click');
    expect((component as React.Component<any, any>).state.checked).toEqual(true);
    expect(onSubmit.called).toEqual(false);
  });

And I had to change it to:

it('doesn't trigger onSubmit when placed inside a form', () => {
    const onSubmit = sinon.spy();

    const wrapper = mount(
      <form
        action="#"
        // tslint:disable-next-line:jsx-no-lambda
        onSubmit={e => {
          onSubmit();
          e.preventDefault();
        }}
      >
        <Toggle
          // tslint:disable-next-line:jsx-no-lambda
          label="Label"
        />
      </form>
    );
    const button: any = wrapper.find('button');

    // simulate to change toggle state
    button.simulate('click');

    expect(
      button
        .first()
        .getDOMNode()
        .getAttribute('aria-checked')
    ).toEqual('true');
    expect(onSubmit.called).toEqual(false);
  });

Just wanted to really double check if it is still doing its intended purpose.

Copy link
Member

@JasonGore JasonGore Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

componentRef is supposed to be set to point to the component's instance, but BaseComponent handles that. Right now, nothing is actually setting that value in the new Toggle, so it wouldn't be usable.

I would revert the test and comment it out until we resolve how we deal with componentRef. Also consider temporarily commenting out componentRef prop unless we can resolve it in this PR.

Did you have to change any other tests?

Copy link
Member Author

@khmakoto khmakoto Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good, will revert and comment the test for the moment.

Thankfully, this was the only test I had to change.

Copy link
Member

@JasonGore JasonGore Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unless @dzearing objects, I think we should copy over BaseComponent to where BaseState resides in experiments and minify it just so that it contains the componentRef logic as part of this PR and then have BaseState extend it. I'm still thinking of names (BaseComponentMin? something better?) but once we can promote it we can have the existing BaseComponent extend it.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also invalid type similar to Button/CS (sorry for not noticing the wrong type before.)


export type IToggleViewProps = Pick<
IToggleProps,
'as' | 'componentRef' | 'label' | 'onText' | 'offText' | 'ariaLabel' | 'checked' | 'disabled' | 'onChange' | 'keytipProps'
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not 100% sure if I'm making the proper separation between state and view here. Would like your input on that.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are these all used by the view? I would only include props actually used by the view.

One consideration is just to have the view simply take in text prop and have state concerned with whether that is onText, offText, or something else. View's props should be very simple and not require much logic, ideally.

As you have it here, some props such as defaultChecked should never present as they are an initializer prop for the state component and do not concern the view. (They do affect checked, but state is responsible for determining the value for view.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I actually use all of them in Toggle.view.tsx. Well, except for componentRef, which you commented about not working correctly with BaseState.

Copy link
Member

@JasonGore JasonGore Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the state prop onText/offText -> view text prop is worthy of changing for simplicity's sake. It's a good demonstration on how view's interface should be really simple while state is concerned with determining view's properties.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should be done, please take a look at it if you want.


export type IToggleViewProps = Pick<
IToggleProps,
'as' /*| 'componentRef'*/ | 'label' | 'ariaLabel' | 'disabled' | 'onChange' | 'keytipProps'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually as I think of this view shouldn't ever need componentRef prop as the state component will have the interface implementation (and will therefore be the actual component reference.)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I removed the prop from the ViewProps list 👍.


if (!disabled) {
// Only update the state if the user hasn't provided it.
if (checkedProp === undefined) {
Copy link
Member

@JasonGore JasonGore Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually you don't have to do this explicit check. The BaseState constructor takes a second argument which is a list of controlled props. In this case, it should be { controlledProps: ['checked'] }. Button does something similar. This allows you to set checked via setState without fear because BaseState will ensure the proper value is passed to the view.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Although it does raise an interesting question with text.. what if the state's value disagrees with the prop? Then we'll show the wrong text? Hmmm...

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an interesting question for sure. Should we leave this check? Should we leave the check but only for text and let BaseState handle checked?

I just went ahead and tried deleting the condition and the text was indeed changing although the pill wasn't being toggled.

Copy link
Member Author

@khmakoto khmakoto Nov 2, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Another thing we could do is retrieve the state of checked once again after calling this.setState() and use that value to set text. Let me know which one is the option you prefer.

@@ -31,12 +31,19 @@ export class BaseState<TComponentProps, TViewProps, TState> extends React.Compon
}
}

// Unless overwritten by the component, the props stay the same.
public transformDerivedProps(newProps: TViewProps): TViewProps {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think this would be better as part of options along with controlled props.

Also, the typing seems to provide clarity on the name. I wonder if transformViewProps is a better name?

*/
export class BaseComponentMin<TComponentProps extends IBaseProps = {}, TViewProps = {}, TState = {}> extends React.Component<
IStateComponentProps<TComponentProps, TViewProps>,
TState
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JasonGore I have some doubts about a change I made here to make it work with the separation between state and view but I don't know if it was the correct thing to do.

Originally, this was:

export class BaseComponent<TProps extends IBaseProps = {}, TState = {}> extends React.Component<TProps, TState> {

What do you think?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is at a lower level so BaseComponentMin should not have to know anything about state components or state vs. view classes, and shouldn't be using anything from IStateComponentProps. The only thing BaseComponentMin should be concerned about is that props has componentRef, which is covered by extends IBaseProps. Line 21 should be able to be just TComponentProps.


// tslint:disable:variable-name
private __disposables: IDisposable[] | null;
private __resolves: { [name: string]: (ref: React.ReactNode) => React.ReactNode };
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need either of these for supporting componentRef?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can safely remove __resolves, but we need __disposables for componentWillUnmount().

'componentWillReceiveProps',
'render',
'componentDidUpdate',
'componentWillUnmount'
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this seems to need trimming from original use cases here and in BaseComponent. At least for here, only keep the ones we actually use for now.

…nentRef to missing components in experiments.
@@ -13,6 +13,7 @@ export type IButtonSlots = 'root' | 'stack' | 'text' | 'icon' | 'menuIcon';

export interface IButtonProps extends IStyleableComponentProps<IButtonProps, IButtonStyles> {
as?: keyof JSX.IntrinsicElements;
componentRef?: IRefObject<IButtonComponent>;
Copy link
Member

@JasonGore JasonGore Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this isn't correct, but I can see how the naming is confusing. IButtonComponent should probably be called IButtonComponentTypes (and IComponent be called IComponentTypes) as they are defining types, not actual methods on the class. componentRef must be provided an interface implemented by the component. In this case, Button will eventually need to implement something like IButton but can be {} for now.

Copy link
Member

@JasonGore JasonGore Nov 6, 2018

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or possibly better yet for each case (changing naming appropriate), define an interface IButton = {} then pass IButton to componentRef.

* Optional callback to access the ICollapsibleSectionComponent interface. Use this instead of ref for accessing
* the public methods and properties of the component.
*/
componentRef?: IRefObject<ICollapsibleSectionComponent>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as button, should be {} for now.

Copy link
Member

@JasonGore JasonGore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great! Thanks for dealing with the churn

@JasonGore JasonGore merged commit d6be126 into microsoft:master Nov 7, 2018
@msft-github-bot
Copy link
Contributor

🎉@uifabric/experiments@v6.41.0 has been released which incorporates this pull request.:tada:

Handy Links:

@khmakoto khmakoto deleted the toggle branch November 8, 2018 19:43
stues pushed a commit to stues/office-ui-fabric-react that referenced this pull request Nov 12, 2018
* Add styling api change.

* Adding finished Toggle transition to use Foundation. Missing transitioning of tests.

* Adding tests for Toggle component using Foundation.

* Adding change file.

* Deleting unimportant auto-generated comments.

* Modifying state test file.

* Commenting out 'componentRef' and relegating 'offText' and 'onText' as state-only props.

* Removed 'componentRef' from view props.

* Adding derived props transformation to BaseState.tsx to account for changes in text next to toggle.

* Renaming transformDerivedProps into transformViewProps and making it a part of options along with controlled props.

* Using new transformViewProps api in Toggle component.

* Creating BaseComponentMin and extending it to BaseState so that componentRef functionality is available in components using Foundation.

* Correcting typing errors when using BaseComponentMin and adding componentRef to missing components in experiments.

* Using exported interface in componentRef.

* Changing componentRef from using IToggleComponent to using IToogle.

* Getting rid of disposables.
@micahgodbolt micahgodbolt changed the title Converting existing Toggle component to use Foundation Converting existing Toggle component to use Foundation (in experiments) Nov 14, 2018
@microsoft microsoft locked as resolved and limited conversation to collaborators Aug 30, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

4 participants