Skip to content

Commit

Permalink
Foundation: Evolve API (#9072)
Browse files Browse the repository at this point in the history
* Evolve create component API to separate out view and make options bag optional.

* Change files.

* Only use options displayName for Customizations.

* Remove unnecessary check.

* Another unnecessary check! :/
  • Loading branch information
JasonGore committed May 13, 2019
1 parent c4507e4 commit 20b09cc
Show file tree
Hide file tree
Showing 23 changed files with 117 additions and 83 deletions.
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/experiments",
"comment": "Support changes to createComponent API.",
"type": "patch"
}
],
"packageName": "@uifabric/experiments",
"email": "jagore@microsoft.com"
}
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/foundation",
"comment": "Evolve create component API to separate out view and make options bag optional.",
"type": "minor"
}
],
"packageName": "@uifabric/foundation",
"email": "jagore@microsoft.com"
}
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "@uifabric/react-cards",
"comment": "Support changes to createComponent API.",
"type": "patch"
}
],
"packageName": "@uifabric/react-cards",
"email": "jagore@microsoft.com"
}
@@ -0,0 +1,11 @@
{
"changes": [
{
"packageName": "office-ui-fabric-react",
"comment": "Support changes to createComponent API.",
"type": "patch"
}
],
"packageName": "office-ui-fabric-react",
"email": "jagore@microsoft.com"
}
5 changes: 2 additions & 3 deletions packages/experiments/src/components/Accordion/Accordion.tsx
Expand Up @@ -8,7 +8,7 @@ import { styles } from './Accordion.styles';

const AccordionItemType = (<CollapsibleSection /> as React.ReactElement<ICollapsibleSectionProps>).type;

const view: IAccordionComponent['view'] = props => {
const AccordionView: IAccordionComponent['view'] = props => {
const { collapseItems } = props;

const children: React.ReactChild[] = React.Children.map(
Expand Down Expand Up @@ -43,10 +43,9 @@ const AccordionStatics = {

export const Accordion: React.StatelessComponent<IAccordionProps> & {
Item: React.StatelessComponent<ICollapsibleSectionProps>;
} = createComponent({
} = createComponent(AccordionView, {
displayName: 'Accordion',
styles,
view,
statics: AccordionStatics
});

Expand Down
7 changes: 3 additions & 4 deletions packages/experiments/src/components/Button/Button.tsx
Expand Up @@ -2,14 +2,13 @@ import { createComponent } from '../../Foundation';
import { useButtonState as state } from './Button.state';
import { ButtonStyles as styles, ButtonTokens as tokens } from './Button.styles';
import { IButtonProps } from './Button.types';
import { ButtonView as view } from './Button.view';
import { ButtonView } from './Button.view';

export const Button: React.StatelessComponent<IButtonProps> = createComponent({
export const Button: React.StatelessComponent<IButtonProps> = createComponent(ButtonView, {
displayName: 'Button',
state,
styles,
tokens,
view
tokens
});

export default Button;
Expand Up @@ -2,14 +2,13 @@ import { createComponent } from '../../../Foundation';
import { useMenuButtonState as state } from './MenuButton.state';
import { MenuButtonStyles as styles, MenuButtonTokens as tokens } from './MenuButton.styles';
import { IMenuButtonProps } from './MenuButton.types';
import { MenuButtonView as view } from './MenuButton.view';
import { MenuButtonView } from './MenuButton.view';

export const MenuButton: React.StatelessComponent<IMenuButtonProps> = createComponent({
export const MenuButton: React.StatelessComponent<IMenuButtonProps> = createComponent(MenuButtonView, {
displayName: 'MenuButton',
state,
styles,
tokens,
view
tokens
});

export default MenuButton;
Expand Up @@ -2,13 +2,12 @@ import * as React from 'react';
import { createComponent } from '../../../Foundation';
import { SplitButtonStyles as styles, SplitButtonTokens as tokens } from './SplitButton.styles';
import { ISplitButtonProps } from './SplitButton.types';
import { SplitButtonView as view } from './SplitButton.view';
import { SplitButtonView } from './SplitButton.view';

export const SplitButton: React.StatelessComponent<ISplitButtonProps> = createComponent({
export const SplitButton: React.StatelessComponent<ISplitButtonProps> = createComponent(SplitButtonView, {
displayName: 'SplitButton',
styles,
tokens,
view
tokens
});

export default SplitButton;
Expand Up @@ -4,16 +4,14 @@ import { collapsibleSectionStyles } from './CollapsibleSection.styles';
import { ICollapsibleSectionProps } from './CollapsibleSection.types';
import { createComponent } from '../../Foundation';

export const CollapsibleSection: React.StatelessComponent<ICollapsibleSectionProps> = createComponent({
export const CollapsibleSection: React.StatelessComponent<ICollapsibleSectionProps> = createComponent(CollapsibleSectionView, {
displayName: 'CollapsibleSection',
view: CollapsibleSectionView,
state: useCollapsibleSectionState,
styles: collapsibleSectionStyles
});

// TODO: This is only here for testing createComponent and should be removed before promoting to production
export const CollapsibleSectionStateless: React.StatelessComponent<ICollapsibleSectionProps> = createComponent({
export const CollapsibleSectionStateless: React.StatelessComponent<ICollapsibleSectionProps> = createComponent(CollapsibleSectionView, {
displayName: 'CollapsibleSection',
view: CollapsibleSectionView,
styles: collapsibleSectionStyles
});
@@ -1,13 +1,15 @@
import { createComponent } from '../../Foundation';
import { CollapsibleSectionTitleView as view } from './CollapsibleSectionTitle.view';
import { CollapsibleSectionTitleView } from './CollapsibleSectionTitle.view';
import { getStyles as styles } from './CollapsibleSectionTitle.styles';
import { ICollapsibleSectionTitleProps } from './CollapsibleSectionTitle.types';

export const CollapsibleSectionTitle: React.FunctionComponent<ICollapsibleSectionTitleProps> = createComponent({
displayName: 'CollapsibleSectionTitle',
view,
styles,
factoryOptions: {
defaultProp: 'text'
export const CollapsibleSectionTitle: React.FunctionComponent<ICollapsibleSectionTitleProps> = createComponent(
CollapsibleSectionTitleView,
{
displayName: 'CollapsibleSectionTitle',
styles,
factoryOptions: {
defaultProp: 'text'
}
}
});
);
Expand Up @@ -3,9 +3,8 @@ import { VerticalPersonaStyles, VerticalPersonaTokens } from './VerticalPersona.
import { IVerticalPersonaProps } from './VerticalPersona.types';
import { createComponent } from '../../../Foundation';

export const VerticalPersona: React.StatelessComponent<IVerticalPersonaProps> = createComponent({
export const VerticalPersona: React.StatelessComponent<IVerticalPersonaProps> = createComponent(VerticalPersonaView, {
displayName: 'VerticalPersona',
view: VerticalPersonaView,
styles: VerticalPersonaStyles,
tokens: VerticalPersonaTokens
});
Expand Up @@ -4,9 +4,8 @@ import { PersonaCoinStyles } from './PersonaCoin.styles';
import { IPersonaCoinProps } from './PersonaCoin.types';
import { PersonaCoinView } from './PersonaCoin.view';

export const PersonaCoin: React.StatelessComponent<IPersonaCoinProps> = createComponent({
export const PersonaCoin: React.StatelessComponent<IPersonaCoinProps> = createComponent(PersonaCoinView, {
displayName: 'PersonaCoin',
view: PersonaCoinView,
styles: PersonaCoinStyles,
state: usePersonaCoinState
});
Expand Up @@ -41,8 +41,7 @@ const PersonaCoinImageView = (props: IPersonaCoinImageProps): JSX.Element | null
);
};

export const PersonaCoinImage: React.StatelessComponent<IPersonaCoinImageProps> = createComponent({
export const PersonaCoinImage: React.StatelessComponent<IPersonaCoinImageProps> = createComponent(PersonaCoinImageView, {
displayName: 'PersonaCoinImage',
view: PersonaCoinImageView,
styles: personaCoinImageStyles
});
5 changes: 2 additions & 3 deletions packages/experiments/src/components/Toggle/Toggle.ts
@@ -1,12 +1,11 @@
import { ToggleView as view } from './Toggle.view';
import { ToggleView } from './Toggle.view';
import { ToggleStyles as styles, ToggleTokens as tokens } from './Toggle.styles';
import { useToggleState as state } from './Toggle.state';
import { IToggleProps } from './Toggle.types';
import { createComponent } from '../../Foundation';

export const Toggle: React.StatelessComponent<IToggleProps> = createComponent({
export const Toggle: React.StatelessComponent<IToggleProps> = createComponent(ToggleView, {
displayName: 'Toggle',
view,
state,
styles,
tokens
Expand Down
9 changes: 5 additions & 4 deletions packages/foundation/etc/foundation.api.md
Expand Up @@ -14,7 +14,7 @@ import * as PropTypes from 'prop-types';
import * as React_2 from 'react';

// @public
export function createComponent<TComponentProps extends ValidProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>, TViewProps extends TComponentProps = TComponentProps, TStatics = {}>(component: IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>): React_2.FunctionComponent<TComponentProps> & TStatics;
export function createComponent<TComponentProps extends ValidProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>, TViewProps extends TComponentProps = TComponentProps, TStatics = {}>(view: IViewComponent<TViewProps>, options?: IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>): React_2.FunctionComponent<TComponentProps> & TStatics;

// @public
export function createFactory<TProps extends ValidProps, TShorthandProp extends ValidShorthand = never>(DefaultComponent: React_2.ComponentType<TProps>, options?: IFactoryOptions<TProps>): ISlotFactory<TProps, TShorthandProp>;
Expand All @@ -32,18 +32,19 @@ export function getControlledDerivedProps<TProps, TProp extends keyof TProps>(pr
export function getSlots<TComponentProps extends ISlottableProps<TComponentSlots>, TComponentSlots>(userProps: TComponentProps, slots: ISlotDefinition<Required<TComponentSlots>>): ISlots<Required<TComponentSlots>>;

// @public
export type IComponent<TComponentProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>, TViewProps = TComponentProps, TStatics = {}> = Required<IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>>;
export type IComponent<TComponentProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>, TViewProps = TComponentProps, TStatics = {}> = Required<IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>> & {
view: IViewComponent<TViewProps>;
};

// @public
export interface IComponentOptions<TComponentProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>, TViewProps = TComponentProps, TStatics = {}> {
displayName: string;
displayName?: string;
factoryOptions?: IFactoryOptions<TComponentProps>;
fields?: string[];
state?: IStateComponentType<TComponentProps, TViewProps>;
statics?: TStatics;
styles?: IStylesFunctionOrObject<TViewProps, TTokens, TStyleSet>;
tokens?: ITokenFunctionOrObject<TViewProps, TTokens>;
view: IViewComponent<TViewProps>;
}

// @public
Expand Down
15 changes: 8 additions & 7 deletions packages/foundation/src/IComponent.ts
Expand Up @@ -103,9 +103,9 @@ export interface IComponentOptions<
TStatics = {}
> {
/**
* Display name to identify component in React hierarchy.
* Display name to identify component in React hierarchy. This parameter is required for targeted component styling via theming.
*/
displayName: string;
displayName?: string;
/**
* List of fields which can be customized.
*/
Expand All @@ -114,10 +114,6 @@ export interface IComponentOptions<
* Styles prop to pass into component.
*/
styles?: IStylesFunctionOrObject<TViewProps, TTokens, TStyleSet>;
/**
* React view component.
*/
view: IViewComponent<TViewProps>;
/**
* Optional state component that processes TComponentProps into TViewProps.
*/
Expand Down Expand Up @@ -145,7 +141,12 @@ export type IComponent<
TStyleSet extends IStyleSet<TStyleSet>,
TViewProps = TComponentProps,
TStatics = {}
> = Required<IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>>;
> = Required<IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>> & {
/**
* Component that generates view output.
*/
view: IViewComponent<TViewProps>;
};

/**
* Factory options for creating component.
Expand Down
30 changes: 16 additions & 14 deletions packages/foundation/src/createComponent.tsx
Expand Up @@ -10,7 +10,8 @@ import {
IStyleableComponentProps,
IStylesFunctionOrObject,
IToken,
ITokenFunction
ITokenFunction,
IViewComponent
} from './IComponent';
import { IDefaultSlotProps, ISlotCreator, ValidProps } from './ISlots';

Expand All @@ -27,7 +28,7 @@ import { IDefaultSlotProps, ISlotCreator, ValidProps } from './ISlots';
* Views should simply be stateless pure functions that receive all props needed for rendering their output.
* State component is optional. If state is not provided, created component is essentially a functional stateless component.
*
* @param component - component Component options. See IComponentOptions for more detail.
* @param options - component Component options. See IComponentOptions for more detail.
*/
export function createComponent<
TComponentProps extends ValidProps,
Expand All @@ -36,21 +37,22 @@ export function createComponent<
TViewProps extends TComponentProps = TComponentProps,
TStatics = {}
>(
component: IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics>
view: IViewComponent<TViewProps>,
options: IComponentOptions<TComponentProps, TTokens, TStyleSet, TViewProps, TStatics> = {}
): React.FunctionComponent<TComponentProps> & TStatics {
const { factoryOptions = {} } = component;
const { factoryOptions = {} } = options;
const { defaultProp } = factoryOptions;

const result: React.FunctionComponent<TComponentProps> = (
componentProps: TComponentProps & IStyleableComponentProps<TViewProps, TTokens, TStyleSet>
) => {
const settings: ICustomizationProps<TViewProps, TTokens, TStyleSet> = _getCustomizations(
component.displayName,
options.displayName,
React.useContext(CustomizerContext),
component.fields
options.fields
);

const useState = component.state;
const useState = options.state;

if (useState) {
// Don't assume state will return all props, so spread useState result over component props.
Expand All @@ -62,8 +64,8 @@ export function createComponent<

const theme = componentProps.theme || settings.theme;

const tokens = _resolveTokens(componentProps, theme, component.tokens, settings.tokens, componentProps.tokens);
const styles = _resolveStyles(componentProps, theme, tokens, component.styles, settings.styles, componentProps.styles);
const tokens = _resolveTokens(componentProps, theme, options.tokens, settings.tokens, componentProps.tokens);
const styles = _resolveStyles(componentProps, theme, tokens, options.styles, settings.styles, componentProps.styles);

const viewProps = {
...componentProps,
Expand All @@ -72,19 +74,19 @@ export function createComponent<
_defaultStyles: styles
} as TViewProps & IDefaultSlotProps<any>;

return component.view(viewProps);
return view(viewProps);
};

result.displayName = component.displayName;
result.displayName = options.displayName || view.name;

// If a shorthand prop is defined, create a factory for the component.
// TODO: This shouldn't be a concern of createComponent.. factoryOptions should just be forwarded.
// Need to weigh creating default factories on component creation vs. memozing them on use in slots.tsx.
// Need to weigh creating default factories on component creation vs. memoizing them on use in slots.tsx.
if (defaultProp) {
(result as ISlotCreator<TComponentProps, any>).create = createFactory(result, { defaultProp });
}

assign(result, component.statics);
assign(result, options.statics);

// Later versions of TypeSript should allow us to merge objects in a type safe way and avoid this cast.
return result as React.FunctionComponent<TComponentProps> & TStatics;
Expand Down Expand Up @@ -141,7 +143,7 @@ function _resolveTokens<TViewProps, TTokens>(
* @param fields Optional list of properties to grab from global store and context.
*/
function _getCustomizations<TViewProps, TTokens, TStyleSet extends IStyleSet<TStyleSet>>(
displayName: string,
displayName: string | undefined,
context: ICustomizerContext,
fields?: string[]
): ICustomizationProps<TViewProps, TTokens, TStyleSet> {
Expand Down
Expand Up @@ -9,7 +9,7 @@ import { IStackComponent, IStackProps, IStackSlots } from './Stack.types';

const StackItemType = (<StackItem /> as React.ReactElement<IStackItemProps>).type;

const view: IStackComponent['view'] = props => {
const StackView: IStackComponent['view'] = props => {
const { as: RootType = 'div', disableShrink, wrap, ...rest } = props;

warnDeprecations('Stack', props, {
Expand Down Expand Up @@ -65,10 +65,9 @@ const StackStatics = {

export const Stack: React.StatelessComponent<IStackProps> & {
Item: React.StatelessComponent<IStackItemProps>;
} = createComponent({
} = createComponent(StackView, {
displayName: 'Stack',
styles,
view,
statics: StackStatics
});

Expand Down

0 comments on commit 20b09cc

Please sign in to comment.