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

@casl/react Can fallback #980

Open
dennemark opened this issue Oct 11, 2024 · 0 comments
Open

@casl/react Can fallback #980

dennemark opened this issue Oct 11, 2024 · 0 comments

Comments

@dennemark
Copy link

Is your feature request related to a problem? Please describe.
I thought it might be nice to have a fallback option for Can component instead of using passThrough that works similiar to react Suspense.

Describe the solution you'd like
<Can I="save" a="Post" fallback={<h1>Not authorized</h1>} ><button>Save</button></Can>

Unfortunately I could not run the repo via contribution guidelines.
 ERR_PNPM_RECURSIVE_RUN_FIRST_FAIL  @casl/[email protected] prebuild: rm -rf dist/* && npm run build.types Exit status 2

So I am pasting the untested code here - I added some documentation to Can component. Maybe helpful:

import { PureComponent, ReactNode } from 'react';
import {
  Unsubscribe,
  AbilityTuple,
  SubjectType,
  AnyAbility,
  Generics,
  Abilities,
  IfString,
} from '@casl/ability';

const noop = () => {};

type AbilityCanProps<
  T extends Abilities,
  Else = IfString<T, { do: T } | { I: T }>
> = T extends AbilityTuple
  ? { do: T[0], on: T[1], field?: string } |
  { I: T[0], a: Extract<T[1], SubjectType>, field?: string } |
  { I: T[0], an: Extract<T[1], SubjectType>, field?: string } |
  { I: T[0], this: Exclude<T[1], SubjectType>, field?: string }
  : Else;

interface ExtraProps {
  not?: boolean
  passThrough?: boolean
  fallback?: ReactNode
}

interface CanExtraProps<T extends AnyAbility> extends ExtraProps {
  ability: T
  children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode)
}

interface BoundCanExtraProps<T extends AnyAbility> extends ExtraProps {
  ability?: T
  children: ReactNode | ((isAllowed: boolean, ability: T) => ReactNode)
}

export type CanProps<T extends AnyAbility> =
  AbilityCanProps<Generics<T>['abilities']> & CanExtraProps<T>;
export type BoundCanProps<T extends AnyAbility> =
  AbilityCanProps<Generics<T>['abilities']> & BoundCanExtraProps<T>;


/**
 *
 * https://casl.js.org/v6/en/package/casl-react
 *
 * checking abilities on model
 *
 * ```ts
 * <Can I='create' a='Post' >{children}</Can>
 * ```
 *
 * checking abilities on instances
 *
 * ```ts
 * <Can I='read' this={post} >{children}</Can>
 * ```
 *
 * To show content conditionally either use passThrough or fallback.
 * passThrough has priority over fallback.
 * 
 * passThrough:
 * ```ts
 *    <Can I="create" a="Post" passThrough>
 *     {allowed => <button disabled={!allowed}>Save</button>}
 *    </Can>
 * ```
 * 
 * fallback:
 * ```ts
 *    <Can I="create" a="Post" fallback={<p>Not authorized to save </p>}>
 *     <button>Save</button>
 *    </Can>
 * ```
 * 
 */
export class Can<
  T extends AnyAbility,
  IsBound extends boolean = false
> extends PureComponent<IsBound extends true ? BoundCanProps<T> : CanProps<T>> {
  private _isAllowed: boolean = false;
  private _ability: T | null = null;
  private _unsubscribeFromAbility: Unsubscribe = noop;

  componentWillUnmount() {
    this._unsubscribeFromAbility();
  }

  private _connectToAbility(ability?: T) {
    if (ability === this._ability) {
      return;
    }

    this._unsubscribeFromAbility();
    this._ability = null;

    if (ability) {
      this._ability = ability;
      this._unsubscribeFromAbility = ability.on('updated', () => this.forceUpdate());
    }
  }

  get allowed() {
    return this._isAllowed;
  }

  private _canRender(): boolean {
    const props: any = this.props;
    const subject = props.of || props.a || props.an || props.this || props.on;
    const can = props.not ? 'cannot' : 'can';

    return props.ability[can](props.I || props.do, subject, props.field);
  }

  render() {
    this._connectToAbility(this.props.ability);
    this._isAllowed = this._canRender();
    return this.props.passThrough || this._isAllowed 
    ? this._renderChildren() 
    : this.props.fallback || null;
  }

  private _renderChildren() {
    const { children, ability } = this.props;
    const elements = typeof children === 'function'
      ? children(this._isAllowed, ability as any)
      : children;

    return elements as ReactNode;
  }
}

Not sure if this test is enough for Can.spec.js:

    it('renders fallback children if `fallback` prop is assigned', () => {
      const component = renderer.create(
        e(Can, { I: 'delete', a: 'Post', fallback: e('h1', null, 'not authorized'), ability }, child)
      )

      expect(ability.can('delete', 'Post')).to.be.false
      expect(component.toJSON().children).to.deep.equal([child.props.children])
    })
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant