-
Notifications
You must be signed in to change notification settings - Fork 2.7k
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
Conversation
* 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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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
.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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.)
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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...
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
…hanges in text next to toggle.
@@ -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 { |
There was a problem hiding this comment.
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?
…a part of options along with controlled props.
…nentRef functionality is available in components using Foundation.
*/ | ||
export class BaseComponentMin<TComponentProps extends IBaseProps = {}, TViewProps = {}, TState = {}> extends React.Component< | ||
IStateComponentProps<TComponentProps, TViewProps>, | ||
TState |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 }; |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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' |
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>; |
There was a problem hiding this comment.
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.
There was a problem hiding this 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
🎉 Handy Links: |
* 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.
Pull request checklist
$ npm run change
Description of changes
Converting existing Toggle component to use Foundation.
Focus areas to test
(optional)
Microsoft Reviewers: Open in CodeFlow