react-querybuilder/react-querybuilder

Conditionally show/hide fields

fridaystreet opened this issue · 13 comments

Hi,

Great library thanks for the time and effort you have put into it. Totally appreciate it's such a chicken and egg complicated type problem so well done.

I've got quite far with it and during all the reading could have sworn I read somewhere about conditionally hiding fields, but can't seem to find reference to it now, so I could have just mistaken it for conditionally altering some other aspect of the rules.

What I am trying to do is to allow users to add output channels for their notification rules. Myt approach was going to be have a top level group with a 'channel' field allowing the selection of eg 'email, web, push' etc

When selecting the channel, then display the different fields related to that particular channel.

Any hints on how to do this (or maybe a better approach) would be greatly appreciated.

Cheers
Paul

Could you provide some mockups showing the desired rendering? It sounds like something I've suggested a solution to before but I'm not certain.

Thanks for the quick response. Not sure if this helps, this is as far as I've got.

Basically I've got one buider that allows user to cretae rules around what triggers the notifcation and that is all pretty good (although I actually need this hide functionaluity there too, as the trigger condition fields shouldn't display 'event' or 'action')

So in the output section, what I want is to be able to select the type of output, and then for the appropriate fields for that output type to show up.

What I've done in the trigger section is essentially use fixed groups, which I was wondering if that's something I could do here and just have group buttons for each output type, then have the different outputs as predefined group configurations with fixed fields (ie no +rule button) same as I have done for trigger . So 'add email' 'add sms' etc rather than selecting the output type in a field.

I know I can use the fieldSelector function and just change the options in there, and it becomes a bit complicated then trying to determine what field you are in and just feels a bit smelly, I guess I could do it that way. I've found a couple of references in other issues about uniqueness as well, which is another problem that needs solving if I don't use fixed set of fields and allow users to add more fields/rules themselves, I don't want them adding another output field of the same type in some instances.

I guess what I really want to be able to do is add a group with a single field which only has 'output channel'. Once they select the channel (somehow detect that) and expand the fields in the group with the extra fields for that type of output. What would be really nice would be if you could override the field lists at the group level and the list of fields was actually more relative to where it is used and not global.

Sorry if that's clear as mud, still trying to get my head around how the library hangs together to explain the problem.
Screenshot 2024-03-04 at 11 24 30 am

This is what I mean regarding using group buttons and a set of fixed fields. Depending which one you pick it just adds a group with the correct fields. The field selectors are disabled and you can't add any rules

Screenshot 2024-03-04 at 7 08 38 pm

It's not very scalable. We have many different types of outputs and the list will include integrations etc. I'm now thinking that if the 'channel' field could be in a first level generic 'channel' group and when you select the desired output channel value (email, sms etc), it will add a subgroup with the fixed fields for that channel similar to above. In fact that would be best in terms of keeping the code a bit friendlier.

Any ideas how to do that would be good thanks

Let me know if this sandbox does what you want: https://codesandbox.io/p/devbox/x86zgp

Screenshot:

image

Here's the code for reference:

styles.scss

This right-justifies the "Remove group" button, which wasn't in your requirement but I thought it looked nicer.

.ruleGroup .ruleGroup .ruleGroup-header {
  justify-content: space-between;
}

App.tsx

import { teal } from '@mui/material/colors';
import { createTheme, ThemeProvider } from '@mui/material/styles';
import {
  MaterialActionElement,
  MaterialValueSelector,
  QueryBuilderMaterial,
} from '@react-querybuilder/material';
import { useState } from 'react';
import type {
  ActionWithRulesAndAddersProps,
  Field,
  Path,
  RuleGroupType,
  VersatileSelectorProps,
} from 'react-querybuilder';
import { formatQuery, QueryBuilder } from 'react-querybuilder';
import './styles.scss';
import { ChannelRuleGroup } from './ChannelRuleGroup.js';

const muiTheme = createTheme({
  palette: {
    secondary: {
      main: teal[500],
    },
  },
});

const fields: Field[] = [
  { name: 'recipients', label: 'Recipients' },
  { name: 'recipientsList', label: 'Recipients List' },
  { name: 'recipientsListKey', label: 'Recipients List Key' },
  { name: 'subject', label: 'Subject' },
  { name: 'htmlTemplate', label: 'HTML Template' },
];

const initialQuery: RuleGroupType = {
  combinator: 'and',
  rules: [],
};

/**
 * Return a group with the default rules and a `channel` property
 * equivalent to the `context` passed by the action element.
 */
const onAddGroup = (
  rg: RuleGroupType,
  _path: Path,
  _query: RuleGroupType,
  context: any
) => {
  console.log(context);
  return {
    ...rg,
    channel: context,
    rules: [
      { field: 'recipients', operator: '=', value: '' },
      { field: 'recipientsList', operator: '=', value: '' },
      { field: 'recipientsListKey', operator: '=', value: '' },
      { field: 'subject', operator: '=', value: '' },
      { field: 'htmlTemplate', operator: '=', value: '' },
    ],
  };
};

/**
 * Render four buttons instead of one, each passing a different context.
 */
const AddGroup = (props: ActionWithRulesAndAddersProps) =>
  props.path.length === 0 ? (
    <>
      <MaterialActionElement
        {...props}
        label="Email"
        handleOnClick={e => props.handleOnClick(e!, 'Email')}
      />
      <MaterialActionElement
        {...props}
        label="Notification"
        handleOnClick={e => props.handleOnClick(e!, 'Notification')}
      />
      <MaterialActionElement
        {...props}
        label="Desktop Alert"
        handleOnClick={e => props.handleOnClick(e!, 'Desktop Alert')}
      />
      <MaterialActionElement
        {...props}
        label="Push Notification"
        handleOnClick={e => props.handleOnClick(e!, 'Push Notification')}
      />
    </>
  ) : null;

/** Used to hide any elements */
const HiddenElement = () => null;

/** Used to disabled any selectors */
const DisabledSelector = (props: VersatileSelectorProps) => (
  <MaterialValueSelector {...props} disabled />
);

export const App = () => {
  const [query, setQuery] = useState(initialQuery);

  return (
    <div>
      <ThemeProvider theme={muiTheme}>
        <QueryBuilderMaterial>
          <QueryBuilder
            fields={fields}
            query={query}
            onQueryChange={setQuery}
            controlClassnames={{ queryBuilder: 'queryBuilder-branches' }}
            controlElements={{
              addGroupAction: AddGroup,
              addRuleAction: HiddenElement,
              removeRuleAction: HiddenElement,
              combinatorSelector: HiddenElement,
              fieldSelector: DisabledSelector,
              operatorSelector: DisabledSelector,
              ruleGroup: ChannelRuleGroup,
            }}
            onAddGroup={onAddGroup}
          />
        </QueryBuilderMaterial>
      </ThemeProvider>
      <h4>Query</h4>
      <pre>
        <code>{formatQuery(query, 'json')}</code>
      </pre>
    </div>
  );
};

ChannelRuleGroup.tsx

This is a very slightly modified version of the standard RuleGroup component.

import Typography from '@mui/material/Typography';
import * as React from 'react';
import type { RuleGroupProps } from 'react-querybuilder';
import {
  TestID,
  useRuleGroup,
  useStopEventPropagation,
  RuleGroupHeaderComponents,
  RuleGroupBodyComponents,
} from 'react-querybuilder';

export const ChannelRuleGroup = React.memo((props: RuleGroupProps) => {
  const rg = useRuleGroup(props);

  rg.addRule = useStopEventPropagation(rg.addRule);
  rg.addGroup = useStopEventPropagation(rg.addGroup);
  rg.cloneGroup = useStopEventPropagation(rg.cloneGroup);
  rg.toggleLockGroup = useStopEventPropagation(rg.toggleLockGroup);
  rg.removeGroup = useStopEventPropagation(rg.removeGroup);
  rg.shiftGroupUp = useStopEventPropagation(rg.shiftGroupUp);
  rg.shiftGroupDown = useStopEventPropagation(rg.shiftGroupDown);

  return (
    <div
      ref={rg.previewRef}
      title={rg.accessibleDescription}
      className={rg.outerClassName}
      data-testid={TestID.ruleGroup}
      data-dragmonitorid={rg.dragMonitorId}
      data-dropmonitorid={rg.dropMonitorId}
      data-rule-group-id={rg.id}
      data-level={rg.path.length}
      data-path={JSON.stringify(rg.path)}>
      <div ref={rg.dropRef} className={rg.classNames.header}>
        {rg.path.length > 0 && (
          <Typography variant="body1">
            Output Channel:{' '}
            <strong>{(rg.ruleGroup as any).channel ?? null}</strong>
          </Typography>
        )}
        <RuleGroupHeaderComponents
          {...(rg as Parameters<typeof RuleGroupHeaderComponents>[0])}
        />
      </div>
      <div className={rg.classNames.body}>
        <RuleGroupBodyComponents
          {...(rg as Parameters<typeof RuleGroupBodyComponents>[0])}
        />
      </div>
    </div>
  );
});

Hi Jake,

Really appreciate you taking the time to post this up. The overall idea is what I have just without the custom ruleGroup which seems like a more rubust way to go and I was looking at how to do that so could add a title as you have so thanks. Yes the delete button is better right aligned, hadn't quite got that far so thanks for the extra attention to detail.

I can't quite seem to get it to work inside our app though. Firstly not using typescript so in converting it I've probably missed something.

These 2 functions don't appear on rg so these lines are thowing cannot convert undefined to null

no probelm we're not using them so I've commented them out, but would be good to know what I'm missing

rg.shiftGroupUp = useStopEventPropagation(rg.shiftGroupUp);
 rg.shiftGroupDown = useStopEventPropagation(rg.shiftGroupDown);

rg.path doesn't seem to be defined either so .lemngth is failing. Again I have added rg.path?.length and that's ok

data-level={rg.path.length}

{rg.path.length > 0 && (

Now my main problem is these 2 lines

<RuleGroupHeaderComponents
          {...(rg as Parameters<typeof RuleGroupHeaderComponents>[0])}
        />

 <RuleGroupBodyComponents
          {...(rg as Parameters<typeof RuleGroupBodyComponents>[0])}
        />

I have tried to just spread rg as the props,

{...rg}

but this then complains that schema isn't present. And when I debug rg inside the component, indeed, there is no schema property. The schema property does exist on the 'props' being passed in beofre you passed it to useRuleGroup

so instead I have tried

 <RuleGroupBodyComponents
         
         {...props}
 {...rg}
        />

Which then loads the component and the buttons. But when I click a button I get props.handleOnClick is not a function.

Would be great to get this custom group working as it looks much better, but I think the main issue is how to add this group from it being selected as a value rather than having to have a button for each type of output. Is this something that would be possible through fieldSelector some how?

my channelgroup.js

import Typography from '@mui/material/Typography';
import * as React from 'react';
import {
  TestID,
  useRuleGroup,
  useStopEventPropagation,
  RuleGroupHeaderComponents,
  RuleGroupBodyComponents,
} from 'react-querybuilder';

export const ChannelRuleGroup = React.memo((props) => {
  const rg = useRuleGroup(props);

  rg.addRule = useStopEventPropagation(rg.addRule);
  rg.addGroup = useStopEventPropagation(rg.addGroup);
  rg.cloneGroup = useStopEventPropagation(rg.cloneGroup);
  rg.toggleLockGroup = useStopEventPropagation(rg.toggleLockGroup);
  rg.removeGroup = useStopEventPropagation(rg.removeGroup);
  // rg.shiftGroupUp = useStopEventPropagation(rg.shiftGroupUp); //cannot convert undefined to null
  // rg.shiftGroupDown = useStopEventPropagation(rg.shiftGroupDown);

  return (
    <div
      ref={rg.previewRef}
      title={rg.accessibleDescription}
      className={rg.outerClassName}
      data-testid={TestID.ruleGroup}
      data-dragmonitorid={rg.dragMonitorId}
      data-dropmonitorid={rg.dropMonitorId}
      data-rule-group-id={rg.id}
      data-level={rg.path?.length} //cannot read property of undefined reading length
      data-path={JSON.stringify(rg.path)}>
      <div ref={rg.dropRef} className={rg.classNames.header}>
        {rg.path?.length > 0 && (
          <Typography variant="body1">
            Output Channel:{' '}
            <strong>{rg.ruleGroup.channel ?? null}</strong>
          </Typography>
        )}
        <RuleGroupHeaderComponents
          {...props}
          {...rg}
        />
      </div>
      <div className={rg.classNames.body}>
        <RuleGroupBodyComponents
          {...props}
          {...rg}
        />
      </div>
    </div>
  );
});

Oh, I'm sorry about that. I was using the v7 release candidate template. If you upgrade to version 7.0.0-rc.1, those issues should be resolved. 7.0.0 final will be released within the next few days.

If you can't upgrade to the v7 RC or wait on the proper v7, I can modify the example to use v6.

oh no that's no problem, thanks, I can just go to 7. Only just started using the library so nothing in production yet.

Sorry just to confirm though, spreading rg[0] is correct for js implementation?

 <RuleGroupHeaderComponents
          {...rg[0]}
        />
        

{...rg[0]} shouldn't be right, whether JS or TS. useRuleGroup returns an object whose keys are the props for RuleGroupHeaderComponents and RuleGroupBodyComponents, so it should just be {...rg} as in the TS version, just without the as [...blah blah blah].

yeah no worries. Sorry might need that v6 example. Just realised can only use v7 with react 18 and we haven't quite got there yet. Next on the list, but we need to to the muiv5 upgrade first :-(

I'll see if I can convert the example from v7 to v6 (and from TS to JS) tonight.

thanks really appreciate your time

Updated sandbox: https://codesandbox.io/p/devbox/xq7lpp?file=%2Fsrc%2FChannelRuleGroup.jsx%3A7%2C29

JS code for the custom rule group, based on RuleGroup.tsx from v6.5.5:

import Typography from '@mui/material/Typography';
import * as React from 'react';
import {
  TestID,
  useRuleGroup,
  useStopEventPropagation,
  RuleGroupHeaderComponents,
  RuleGroupBodyComponents,
} from 'react-querybuilder';

export const ChannelRuleGroup = props => {
  const rg = { ...props, ...useRuleGroup(props) };

  const { addRule, addGroup, cloneGroup, toggleLockGroup, removeGroup } = rg;
  const methodsWithoutEventPropagation = useStopEventPropagation({
    addRule,
    addGroup,
    cloneGroup,
    toggleLockGroup,
    removeGroup,
  });

  const subComponentProps = { ...rg, ...methodsWithoutEventPropagation };

  return (
    <div
      ref={rg.previewRef}
      className={rg.outerClassName}
      data-testid={TestID.ruleGroup}
      data-dragmonitorid={rg.dragMonitorId}
      data-dropmonitorid={rg.dropMonitorId}
      data-rule-group-id={rg.id}
      data-level={rg.path.length}
      data-path={JSON.stringify(rg.path)}>
      <div ref={rg.dropRef} className={rg.classNames.header}>
        {rg.path.length > 0 && (
          <Typography variant="body1">
            Output Channel: <strong>{rg.ruleGroup.channel ?? null}</strong>
          </Typography>
        )}
        <RuleGroupHeaderComponents {...subComponentProps} />
      </div>
      <div className={rg.classNames.body}>
        <RuleGroupBodyComponents {...subComponentProps} />
      </div>
    </div>
  );
};

Refer to my previous comment for the RQB v7 code when you get around to upgrading.