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

Check if OUIA is set after mounting #2478

Merged
merged 11 commits into from
Jul 24, 2019

Conversation

jschuler
Copy link
Collaborator

@jschuler jschuler commented Jul 8, 2019

Closes: #2477

Adding OUIA capabilities to library components

  1. Import: import { InjectedOuiaProps, withOuiaContext } from '../withOuia';
  2. For TS combine the props with the InjectedOuiaProps class Switch extends React.Component<SwitchProps & InjectedOuiaProps>
  3. Wrap the component in the withOuiaContext higher-order-component
const  SwitchWithOuiaContext = withOuiaContext(Switch);
export { SwitchWithOuiaContext as Switch };
  1. OUIA props are in this.props.ouiaContext
const { ouiaContext, ouiaId } = this.props;
<label
	className=""
	htmlFor=""
	{...ouiaContext.isOuia && {
	'data-ouia-component-type':  'Switch',
	'data-ouia-component-id':  ouiaId || ouiaContext.ouiaId
	}}
>my label</label>

Consumer usage

Case 1: non-ouia users

<Switch  />

No re-render, does not render ouia attributes

Case 2: enable ouia through local storage

in local storage ouia: true
<Switch  />

render's ouia attribute data-ouia-component-type="Switch"

Case 3: enable ouia through local storage and generate id

in local storage ouia: true
in local storage ouia-generate-id: true
<Switch  />

render's ouia attributes data-ouia-component-type="Switch" data-ouia-component-id="0"

Case 4: enable ouia through local storage and provide id

in local storage ouia: true
<Switch ouiaId="my_switch_id" />

render's ouia attributes data-ouia-component-type="Switch" data-ouia-component-id="my_switch_id"

Case 5: enable ouia through context and provide id

Note: If context provided isOuia is true and local storage provided isOuia is false, context will win out. Context will also win if its isOuia is false and local storage's is true. Context > local storage
import { OuiaContext } from  '@patternfly/react-core';
<OuiaContext.Provider value={{ isOuia: true }}>
	<Switch ouiaId="my_switch_id" />
</OuiaContext.Provider>

render's ouia attributes data-ouia-component-type="Switch" data-ouia-component-id="my_switch_id"

@jschuler jschuler requested review from tlabaj and redallen July 8, 2019 20:16
@patternfly-build
Copy link
Contributor

PatternFly-React preview: https://patternfly-react-pr-2478.surge.sh

@jschuler jschuler requested a review from dgutride July 8, 2019 20:29
@jschuler
Copy link
Collaborator Author

jschuler commented Jul 8, 2019

See description on how to use in the library

Copy link
Contributor

@karelhala karelhala left a comment

Choose a reason for hiding this comment

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

I wouldn't really change props of children, it is unsafe and if there is going to be an array of children it will break. Same for children wrapped with React.Fragment.

This is really perfect example for context. We can create context with default value and reuse it everywhere we want to access it. We can also export this context so consumers can use this as well, another benefit is that we could control the value by ourselves in future instead of using localStorage anybody else can use Provider and set the value there.

const OUIAContext = React.createContext({
  isOuia: isOUIAEnvironment()
});
<OUIAContext.Consumer>
  {({ isOuia }) => (
    <label
      //...
  </label>
  )}
</OUIAContext.Consumer>

@jschuler
Copy link
Collaborator Author

@karelhala Context uses render props. Usage would be the same, as it can be inserted at any level. Just like context, it needs a single child.
The advantage here is that the render props class packages additional functionality that makes it easy to reuse code and less boilerplate. For example it checks the localStorage after component mount as it wouldn't otherwise detect changes for statically built sites.

@karelhala
Copy link
Contributor

@karelhala Context uses render props. Usage would be the same, as it can be inserted at any level. Just like context, it needs a single child.
The advantage here is that the render props class packages additional functionality that makes it easy to reuse code and less boilerplate. For example it checks the localStorage after component mount as it wouldn't otherwise detect changes for statically built sites.

I don't really like this approach of checking component render, setting state and rendering again because what you are essentially doing is rendering twice same component. And as we all know rendering is the most expensive operation. If it were used only on staticly rendered pages, I would be fine with that. But this is going to be used on live sites (with JS and window and such) and twice rendering the same elements will take a LOT of time.

How about we use Context as I proposed and add decorator as well that would set provider's value after mounting. We'd specifically say that this decorator is mostly for staticly rendered pages.

const withOUIA = WrappedComponent => {
   class ComponentWithOUIA extends React.Component {
      state: WithOUIAState = {
        renderWithOUIA: false,
        ouiaId: getOUIAUniqueId()
      };

      componentDidMount() {
        const { renderWithOUIA } = this.state;
        const isOuia = isOUIAEnvironment();
        if (isOuia !== renderWithOUIA) {
          this.setState({ renderWithOUIA: isOuia });
        }
      }

      render() {
        const { renderWithOUIA, ouiaId } = this.state;
        return <OUIAContext.Provider value={{ isOuia: renderWithOUIA, ouiaId }}>
          <WrappedComponent {...this.props} />
         </OUIAContext.Provider>
      }
  }
  return ComponentWithOUIA;
};

@Hyperkid123
Copy link
Contributor

@jschuler I agree with @karelhala.

Basically what i see here is a mix of of Context with some additional logic of component decorators (like withRouter for instance).

The problem of mixing these two is that you are forcing additional render after initial render (state or props mutation usually seen in decorators). This might be seen as a nit pick but i think we should try to be as efficient as possible. If you are leveraging client side rendering (and most of PF4 users are) you are left with subpar solution.

I would prefer having decorator for your static pages and context for client rendering. This would not even mean having duplicate code because you could use the context inside your decorator. And using the React Context comes with all the checks, optimizations etc.

@jschuler
Copy link
Collaborator Author

@karelhala @Hyperkid123 Can you explain what you mean by decorators? Also i am trying to understand how the context version differs all that much. Looks like it's a HOC instead of a render props component. About rendering twice, that would only happen if the local storage key 'ouia' is true, so for normal consumers this wouldn't change how often it gets rendered

@redallen
Copy link
Contributor

@jschuler It doesn't force a rerender unless the prop is true, but React does have to crawl the tree again and stateless components with OUIA will have to become stateful, so this is a performance hit (although minor).

@Hyperkid123 Your suggested decorator is really just setting a flag when we build the docs, which is in #2487 and an alternative to this PR.

@tlabaj tlabaj added PF4 React issues for the PF4 core effort chore 🏠 labels Jul 10, 2019
@tlabaj tlabaj requested a review from dlabaj July 10, 2019 17:22
@@ -0,0 +1,40 @@
import * as React from 'react';
import { isOUIAEnvironment, getUniqueId as getOUIAUniqueId } from '../helpers/ouia';

Choose a reason for hiding this comment

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

an absolutely critical note - the ouia id should identify an actual object via its uuid or pkey - having a incremented global as data source there is a massive problem

Copy link
Contributor

Choose a reason for hiding this comment

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

Copy link
Contributor

Choose a reason for hiding this comment

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

@RonnyPfannschmidt Why is it a massive problem? It provides unique IDs every page load per-instance of the component that are consistent every page reload. If we were to generate a UUID instead with something like this solution, how could we make sure they're unique without a global data source that keeps a record of them all, is inconsistent between page loads, and has the same problem?

Choose a reason for hiding this comment

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

@redallen i believe you completely misunderstood the use for those ids - they are for identifying application object across potentially different pages, different views and different collections - you cannot generate them randomly or from a counter, you have to take database ids or primary keys

Choose a reason for hiding this comment

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

we are now iterating on the ouia spec as we noted that the component id specification is lacking a description of the intent to make this absolutely clear

Copy link
Contributor

@redallen redallen Jul 11, 2019

Choose a reason for hiding this comment

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

@RonnyPfannschmidt We are a component library, have no database IDs or primary keys to map to components we create on the page. How do we comply to the spec *for our docs page at http://patternfly-react.surge.sh/?

Choose a reason for hiding this comment

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

@redallen thats why it is critical, a user can pass it in, the user will have the database keys,

for documentation pages/examples, exemplary ids are suitable

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

what's the format of these? haven't seen the spec yet. @redallen maybe something that can be passed in as a json string into another local var?

Choose a reason for hiding this comment

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

the id should be pretty much expressable as pain string

possible examples are 00000000-0000-0000-0000-000000000000, flammable, state-deffered, john@example.com

Copy link
Contributor

Choose a reason for hiding this comment

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

I wasn't sure about the auto generated IDs at first and given the spec we can't really generate them. If I understand correctly the spec these IDs are kinda like helpers to identify what is being rendered. So for instance if you have a list of cards on screen each card would have data-ouia-component-type="card" set, but the data-ouia-component-id would be probably UUID from database, or whatever to identify which data are actually rendered there.

I think that best way here would be to add uoiaId prop to every component that is marked by ouia spec. Consumers can then pass this prop to component and if ouia is enabled we will add it to root element. To ilustrate this for set of cards I will have a list of UUIDs from server and render user data in them

import React from 'react';
import { Card, CardBody } from '@patternfly/react-core';

const data = [{
  uuid: '00000000-0000-0000-0000-000000000001',
  name: 'Foo'
}, {
  uuid: '00000000-0000-0000-0000-000000000002',
  name: 'Bar'
}];

export default () => (<React.Fragment>
  {data.map((user) => <Card key={user.id} ouiaId={user.id}>
    <CardBody>{user.name}</CardBody>
  </Card>
)}
</React.Fragment>)

The resulting HTML would look like

<div class="pf-c-card" data-ouia-component-id="00000000-0000-0000-0000-000000000001" data-ouia-component-type="Card">
  <div class="pf-c-card__body">
    Foo
  </div>
</div>
<div class="pf-c-card" data-ouia-component-id="00000000-0000-0000-0000-000000000002" data-ouia-component-type="Card">
  <div class="pf-c-card__body">
    Bar
  </div>
</div>

@RonnyPfannschmidt
Copy link

Thanks for this excellent detail description

I believe it's often likely that render key and ouia ID can be the same, that may be a chance for convenience

@jschuler
Copy link
Collaborator Author

Ok, how about the new commit? I don't think we want to really advertise any OUIA props in our consumer docs but we can still allow the passing of a data-ouia-component-id prop for supported components. So for instance

<Switch
        id="simple-switch"
        label={isChecked ? 'Message when on' : 'Message when off'}
        isChecked={isChecked}
        onChange={this.handleChange}
        aria-label="Message when on"
        data-ouia-component-id="my-unique-data-id"
      />

Reading the spec, these ids are optional and can be left off unless passed in. If you wish to always add them (for example for the patternfly-react.surge.sh site) can add another local variable ouia-generate-id and set it to true.

redallen
redallen previously approved these changes Jul 16, 2019
Copy link
Contributor

@redallen redallen left a comment

Choose a reason for hiding this comment

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

This satisfies all the constraints.

render() {
const { renderWithOUIA, ouiaId } = this.state;
const { children } = this.props;
return children({
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there any reason why are you using children instead of React context? I think that it is much easier approach and it allows for further improvements.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Okay I'll update to use context :)

Copy link
Contributor

@redallen redallen left a comment

Choose a reason for hiding this comment

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

Nitpicking. Nice work that should satisfy everyone and statically render correctly.

ouiaId?: number | string;
}

type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>;
Copy link
Contributor

Choose a reason for hiding this comment

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

How about instead import { Omit } from '../../helpers/typeUtils';?


const SwitchWithOuiaContext = withOuiaContext(Switch);

export { SwitchWithOuiaContext as Switch };
Copy link
Contributor

Choose a reason for hiding this comment

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

React-docgen is still unphased when grabbing the props for Switch. This is good.

redallen
redallen previously approved these changes Jul 22, 2019
@karelhala karelhala self-requested a review July 22, 2019 15:29
Copy link
Contributor

@karelhala karelhala left a comment

Choose a reason for hiding this comment

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

I really like the sage of context! just one correction PF shouldn't really take care of OUIA IDs because these will be bound to each application and each screen. Plus consumers should be able to have no ID if there is one component of same type present.

The simplest usage can be <Switch /> there can't be any ID because it's just one switch on screen.

PF should be aware of data-ouia-component-id and it should be part of rendered DOM only if ouia is enabled (either with localStorage or with context provider) however calculating it is not good. It is possible to pass it from context provider, however the final code will be janky and not good to read.

export const generateOUIAId = (): boolean => typeof window !== 'undefined' && window.localStorage['ouia-generate-id'] && window.localStorage['ouia-generate-id'].toLowerCase() === 'true' || false;

let id = 0;
export const getUniqueId = (): number => id++;
Copy link
Contributor

Choose a reason for hiding this comment

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

Patternfly shouldn't really provide these IDs because they will be bound to specific screen inside specific product. On one place the ID will represent inventory ID and on other it can represent for instance ordered products. And if there is going to be just one component of one type we can't have any ID. Plus autogenerated ID would break some UI automated tests where we compare rendered HTML.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

id is only provided if ouia-generate-id in local storage is true. The main use case i see for this is the PatternFly docs, since our examples won't have manual ouia ids assigned to them. Otherwise no id is added to the component

'data-ouia-component-type': 'Switch',
'data-ouia-component-id': this.ouiaId
'data-ouia-component-id': ouiaContext.ouiaId
Copy link
Contributor

Choose a reason for hiding this comment

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

Can we remove ouiaId from context and consume it from props instead? It would be kinda repetetive to add id to multiple items trough context provider, plus not all items will have data-ouia-component-id set. Only if there will be multiple items of same component we will use ouiaId

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

ok, i'll make it so it can be consumed from props

Copy link
Contributor

@tlabaj tlabaj left a comment

Choose a reason for hiding this comment

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

LGTM

@tlabaj tlabaj dismissed karelhala’s stale review July 24, 2019 20:55

Comments addressed

@jschuler
Copy link
Collaborator Author

@karelhala I believe i've addressed your comments. If there is anything else I don't mind following up

@tlabaj tlabaj merged commit 8d396e7 into patternfly:master Jul 24, 2019
@patternfly-build
Copy link
Contributor

Your changes have been released in:

  • @patternfly/react-core@3.75.3
  • @patternfly/react-docs@4.9.3
  • @patternfly/react-inline-edit-extension@2.9.50
  • demo-app-ts@2.12.12
  • @patternfly/react-integration@2.12.1
  • @patternfly/react-table@2.14.24
  • @patternfly/react-topology@2.6.21
  • @patternfly/react-virtualized-extension@1.1.83

Thanks for your contribution! 🎉

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
chore 🏠 PF4 React issues for the PF4 core effort
Projects
None yet
Development

Successfully merging this pull request may close these issues.

PF4: OUIA attrbiutes are not rendered if window.localStorage.ouia is true
8 participants