Skip to content

Commit

Permalink
feat(react): adds generics for Ability and related components [skip ci]
Browse files Browse the repository at this point in the history
Prefixed private methods with _

Relates to #256 BREAKING CHANGES
  • Loading branch information
stalniy committed Feb 17, 2020
1 parent 8ac4ca1 commit 3102b6e
Show file tree
Hide file tree
Showing 6 changed files with 68 additions and 89 deletions.
46 changes: 1 addition & 45 deletions packages/casl-react/index.d.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1 @@
import { PureComponent, StatelessComponent } from 'react'
import { Ability } from '@casl/ability'

type BaseProps = {
do: string
on: any
} | {
I: string
a: string
} | {
I: string
an: string
} | {
I: string
of: any
} | {
I: string
this: any
};

type CanPropsStrict = BaseProps & {
ability: Ability
not?: boolean
passThrough?: boolean
}

type CanProps = BaseProps & {
ability?: Ability
not?: boolean
passThrough?: boolean
}

declare class CanComponent<T> extends PureComponent<T> {
allowed: boolean
}

export class Can extends CanComponent<CanPropsStrict> {
}

export class BoundCan extends CanComponent<CanProps> {
}

export function createCanBoundTo(ability: Ability): typeof BoundCan

export function createContextualCan(Consumer: any): StatelessComponent<CanProps>
export * from './dist/types';
2 changes: 1 addition & 1 deletion packages/casl-react/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
"scripts": {
"build": "rollup -c ../../tools/rollup.config.js -i src/index.ts -n casl.react -g react:React,prop-types:React.PropTypes,@casl/ability:casl",
"postbuild": "npm run build.types",
"build.types": "tsc",
"build.types": "rm -rf dist/types/* && tsc",
"lint": "eslint --ext .ts,.js src/ spec/",
"test": "NODE_ENV=test jest --config ../../tools/jest.config.js",
"prerelease": "npm test && NODE_ENV=production npm run build",
Expand Down
58 changes: 37 additions & 21 deletions packages/casl-react/src/Can.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import React, { PureComponent, Fragment, createElement } from 'react';
import PropTypes from 'prop-types';
import { Ability, Unsubscribe, AbilitySubject } from '@casl/ability';
import { Ability, Unsubscribe, Subject, SubjectType } from '@casl/ability';

const noop = () => {};
const renderChildren = Fragment
Expand All @@ -21,7 +21,8 @@ if (process.env.NODE_ENV !== 'production') {
.oneOfType([PropTypes.object, PropTypes.string])
.isRequired;

const alias = (names: string, validate: Function) => (props: any, ...args: unknown[]) => { // eslint-disable-line
// eslint-disable-next-line
const alias = (names: string, validate: Function) => (props: any, ...args: unknown[]) => {
if (!names.split(' ').some(name => props[name])) {
return validate(props, ...args);
}
Expand All @@ -42,35 +43,50 @@ if (process.env.NODE_ENV !== 'production') {
};
}

export type AbilityCanProps =
{ do: string, on: AbilitySubject } |
{ I: string, a: Exclude<AbilitySubject, object> } |
{ I: string, an: Exclude<AbilitySubject, object> } |
{ I: string, of: AbilitySubject } |
{ I: string, this: object };
export type AbilityCanProps<A extends string, S extends Subject> =
{ do: A | string, on: S } |
{ I: A | string, a: Extract<S, SubjectType> } |
{ I: A | string, an: Extract<S, SubjectType> } |
{ I: A | string, of: S } |
{ I: A | string, this: Exclude<S, SubjectType> };

export type CanExtraProps = {
export type CanExtraProps<A extends string, S extends Subject, C> = {
not?: boolean,
passThrough?: boolean,
ability: Ability
ability: Ability<A, S, C>
};

export type CanProps = AbilityCanProps & CanExtraProps;
export type CanProps<
A extends string = string,
S extends Subject = Subject,
C = object
> = AbilityCanProps<A, S> & CanExtraProps<A, S, C>;

export type BoundCanProps<
A extends string = string,
S extends Subject = Subject,
C = object
> = AbilityCanProps<A, S> & Omit<CanExtraProps<A, S, C>, 'ability'> & {
ability?: Ability<A, S, C>
};

export default class Can<T extends AbilityCanProps=CanProps> extends PureComponent<CanProps> {
export class Can<
A extends string = string,
S extends Subject = Subject,
C = object,
IsBound extends boolean = false
> extends PureComponent<true extends IsBound ? BoundCanProps<A, S, C> : CanProps<A, S, C>> {
static propTypes = propTypes;

private _isAllowed: boolean = false;

private _ability: Ability | null = null;

private _ability: Ability<A, S, C> | null = null;
private _unsubscribeFromAbility: Unsubscribe = noop;

componentWillUnmount() {
this._unsubscribeFromAbility();
}

connectToAbility(ability: Ability) {
private _connectToAbility(ability?: Ability<A, S, C>) {
if (ability === this._ability) {
return;
}
Expand All @@ -88,7 +104,7 @@ export default class Can<T extends AbilityCanProps=CanProps> extends PureCompone
return this._isAllowed;
}

isAllowed() {
private _canRender(): boolean {
const props: any = this.props;
const [action, field] = (props.I || props.do).split(/\s+/);
const subject = props.of || props.a || props.an || props.this || props.on;
Expand All @@ -98,12 +114,12 @@ export default class Can<T extends AbilityCanProps=CanProps> extends PureCompone
}

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

renderChildren() {
private _renderChildren() {
const { children, ability } = this.props;
const elements = typeof children === 'function'
? children(this._isAllowed, ability)
Expand Down
46 changes: 26 additions & 20 deletions packages/casl-react/src/factory.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,37 @@
import { createElement, StatelessComponent } from 'react';
import { createElement as h, StatelessComponent, ComponentClass } from 'react';
import PropTypes from 'prop-types';
import { Ability } from '@casl/ability';
import Can, { AbilityCanProps, CanExtraProps } from './Can';
import { Ability, Subject } from '@casl/ability';
import { Can, BoundCanProps } from './Can';

export type BoundCanProps =
AbilityCanProps &
Omit<CanExtraProps, 'ability'> &
{
ability?: Ability
};

class BoundCan extends Can<BoundCanProps> {
static propTypes = process.env.NODE_ENV === 'production'
? { ...Can.propTypes, ability: PropTypes.instanceOf(Ability) }
: {};
interface BoundCanClass<
A extends string,
S extends Subject,
C
> extends ComponentClass<BoundCanProps<A, S, C>> {
new (props: BoundCanProps<A, S, C>, context?: any): Can<A, S, C, true>
}

export function createCanBoundTo(ability: Ability): typeof BoundCan {
return class extends BoundCan {
export function createCanBoundTo<
A extends string,
S extends Subject,
C
>(ability: Ability<A, S, C>): BoundCanClass<A, S, C> {
return class extends Can<A, S, C, true> {
static defaultProps = { ability };
static propTypes = process.env.NODE_ENV === 'production'
? { ...Can.propTypes, ability: PropTypes.instanceOf(Ability) }
: {};
};
}

export function createContextualCan(Consumer: any): StatelessComponent<BoundCanProps> {
return function ContextualCan(props: BoundCanProps) {
return createElement(Consumer, null, (ability: Ability) => createElement(Can, {
ability: props.ability || ability,
export function createContextualCan<
A extends string,
S extends Subject,
C
>(Consumer: any): StatelessComponent<BoundCanProps<A, S, C>> {
return function ContextualCan(props: BoundCanProps<A, S, C>) {
return h(Consumer, null, (ability: Ability<A, S, C>) => h(Can as BoundCanClass<A, S, C>, {
ability,
...props,
}));
};
Expand Down
2 changes: 1 addition & 1 deletion packages/casl-react/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
export { default as Can } from './Can';
export * from './Can';
export * from './factory';
3 changes: 2 additions & 1 deletion packages/casl-react/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
"src/*"
],
"compilerOptions": {
"outDir": "dist/types"
"outDir": "dist/types",
"allowSyntheticDefaultImports": true
}
}

0 comments on commit 3102b6e

Please sign in to comment.