1

The React component, which can be considered as third-party component, looks as following:

import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';

export const Tag = (props: React.ElementConfig): React$Node =>{
    const{
        classNames,
        props:
        {
            children,
            className,
            ...restProps
        },
    } = extractCommonClassNames(props);

    const combinedClassNames = classnames(
        'tag',
        className,
        ...classNames,
    );

    return (
        <span
          className={combinedClassNames}
          {...restProps}
        >
          {children}
          <i className="sbicon-times txt-gray" />
        </span>
    );
};

The component where I use the component above looks as following:

import React from 'react';
import * as L from '@korus/leda';
import type { KendoEvent } from '../../../types/general';

type Props = {
  visible: boolean
};

type State = {
  dropDownSelectData: Array<string>,
  dropDownSelectFilter: string
}

export class ApplicationSearch extends React.Component<Props, State> {
  constructor(props) {
    super(props);
    this.state = {
      dropDownSelectData: ['Имя', 'Фамилия', 'Машина'],
      dropDownSelectFilter: '',
    };
    this.onDropDownSelectFilterChange = this.onDropDownSelectFilterChange.bind(this);
  }

  componentDidMount() {
    document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray');
  }

  onDropDownSelectFilterChange(event: KendoEvent) {
    const data = this.state.dropDownSelectData;
    const filter = event.filter.value;
    this.setState({
      dropDownSelectData: this.filterDropDownSelectData(data, filter),
      dropDownSelectFilter: filter,
    });
  }

  // eslint-disable-next-line class-methods-use-this
  filterDropDownSelectData(data, filter) {
    // eslint-disable-next-line func-names
    return data.filter(element => element.toLowerCase().indexOf(filter.toLowerCase()) > -1);
  }

  render() {
    const {
      visible,
    } = this.props;

    const {
      dropDownSelectData,
      dropDownSelectFilter,
    } = this.state;

    return (
      <React.Fragment>
        {
          visible && (
            <React.Fragment>
              <L.Block search active inner>
                <L.Block inner>
                  <L.Block tags>
                    <L.Tag>
                      option 1
                    </L.Tag>
                    <L.Tag>
                      option 2
                    </L.Tag>
                    <L.Tag>
                      ...
                    </L.Tag>
                  </L.Block>
                </L.Block>
              </React.Fragment>
            )}
      </React.Fragment>
    );
  }
}

Is it possible to remove "txt-gray" from the component from outside and if so, how?

tesicg
  • 3,971
  • 16
  • 62
  • 121

3 Answers3

2

Remove the class from where you're using the Tag component:

componentDidMount() {
  document.querySelector('.sbicon-times.txt-gray').classList.remove('txt-gray')
}

Or more specific:

.querySelector('span i.sbicon-times.txt-gray')

As per your comment,

I have multiple components with "txt-gray", but when I use your code, "txt-gray" has been removed from first component only. How to remove it from all components?

I will suggest you to use the code to remove the class in the parent component of using the Tag component. And also use querySelectorAll as in this post.

Bhojendra Rauniyar
  • 83,432
  • 35
  • 168
  • 231
  • 2
    Beware, this will remove the txt-grey class from all the Tag components (and from any other component that has both sbicon-times and txt-grey css classes) – Keilath Nov 15 '18 at 09:39
  • @estus There's no anyway to change without touching the component. So, the selector should choose specific element to remove class from it. – Bhojendra Rauniyar Nov 15 '18 at 09:39
  • There are ways but they aren't pretty. I guess I'll post an answer for completeness. – Estus Flask Nov 15 '18 at 09:41
  • I have multiple components with "txt-gray", but when I use your code, "txt-gray" has been removed from first component only. How to remove it from all components? – tesicg Nov 15 '18 at 09:48
  • `.querySelector('.txt-gray')`?? – Bhojendra Rauniyar Nov 15 '18 at 09:50
  • 1
    @tesicg just use `.querySelectorAll` with same arguments, then loop on the return value and remove the class using `item.classList.remove('txt-gray')` – Keilath Nov 15 '18 at 10:04
  • This one: document.querySelectorAll('span i.sbicon-times.txt-gray'); doesn't work at all, which means all is gray. – tesicg Nov 15 '18 at 10:11
  • 1
    @tesicg `for (let item of document.querySelectorAll('span i.sbicon-times.txt-gray')) item.classList.remove('txt-gray')` should actually work – Keilath Nov 15 '18 at 10:21
1

Refactoring

A clean way is to modify the component to allow it to conditionally add txt-gray through a prop:

<i className={classnames('sbicon-times', { 'txt-gray': props.gray })} />

If the component cannot be modified because it belongs to third-party library, this involves forking a library or replacing third-party component with its modified copy.

Direct DOM access with findDOMNode

A workaround is to access DOM directly in parent component:

class TagWithoutGray extends React.Component {
  componentDidMount() {
    ReactDOM.findDOMNode(this).querySelector('i.sbicon-times.txt-gray')
    .classList.remove('txt-gray');
  }

  // unnecessary for this particular component
  componentDidUpdate = componentDidMount; 

  render() {
    return <Tag {...this.props}/>;
  }
}

The use of findDOMNode is generally discouraged because direct DOM access is not idiomatic to React, it has performance issues and isn't compatible with server-side rendering.

Component patching with cloneElement

Another workaround is to patch a component. Since Tag is function component, it can be called directly to access and modify its children:

const TagWithoutGray = props => {
  const span = Tag(props);
  const spanChildren = [...span.props.children];
  const i = spanChildren.pop();

  return React.cloneElement(span, {
    ...props,
    children: [
      ...spanChildren,
      React.cloneElement(i, {
        ...i.props,
        className: i.props.className.replace('txt-gray', '')
      })
    ]
  });
}

This is considered a hack because wrapper component should be aware of patched component implementation, it may break if the implementation changes.

Estus Flask
  • 206,104
  • 70
  • 425
  • 565
0

No, it is not possible

The only way is to change your Tag component

import * as React from 'react';
import classnames from 'classnames';
import { extractCommonClassNames } from '../../utils';

export const Tag = (props: React.ElementConfig): React$Node =>{
    const{
        classNames,
        props:
        {
            children,
            className,
            ...restProps
        },
    } = extractCommonClassNames(props);

    const combinedClassNames = classnames(
        'tag',
        className,
        ...classNames,
    );

    const grayClass = this.props.disableGray ? 'sbicon-times' : 'sbicon-times txt-gray';

    return (
        <span
          className={combinedClassNames}
          {...restProps}
        >
          {children}
          <i className={grayClass} />
        </span>
    );
};

Now, if you pass disableGray={true} it will suppress the gray class, otherwise of you pass false or avoid passing that prop at all it will use the gray class. It is a small change in the component, but it allows you not to change all the points in your code where you use this component (and you are happy with grey text)

Keilath
  • 436
  • 4
  • 10