Hi
I have 2 collections: Tags and Post. A post can have 1 tag.
I would like post.tag to be recalculated (in the Autoform) if a new entry is added (or modified or removed) to the Tags collection (ie be reactive to the Tags collection).
This is the Post schema:
PostSchemaDefinition = {
...
tags: {
type: String,
uniforms: {
options: () => Tags.find().fetch().map(tag => ({ label: tag.nombre, value: tag._id })),
},
// I also tried with allowedValues + transform
},
but in the Post create Autoform, the list of Tags is not updated when a new Tag is added to the Tags collection.
Based on https://github.com/vazco/uniforms/issues/151 I tried to
let PostSchema = new SimpleSchema(PostSchemaDefinition);
if (Meteor.isClient) {
Tracker.autorun(() => {
Meteor.subscribe('Tags.list');
PostSchema = new SimpleSchema(PostSchemaDefinition);
});
}
Posts.attachSchema(PostSchema);
but still is not updated.
How can I get dynamic allowedValues/options with Uniforms?
Hi @rafahoro. I see two kinds of options here:
withTracker, useTracker, custom solution, etc.). Downside is that it will be less performant as schema creation and almost complete rerender (no way other than deep comparity check that new schema is the same) is rather heavy.options (or allowedValues + transform) in props. Something like this:tsx
function MyForm() {
const options = useTracker(/* ... */);
return (
<AutoForm schema={schema}>
<SelectField name="tags" options={options} />
</AutoForm>
);
}
Hi @radekmie
Thank you for your answer.
I need/want to do that on the schema definition (so the .tsx file is unchanged, and actually unaware of exact fields).
Do you have an example of Tracker.autorun on Schema definition?
BTW: I cant use withTracker/useTracker as my Schemas are not in a component (because they are used also in backend), and hooks can only be called inside of the body of a function component.
Therefore you won鈥檛 be able to make it reactive easily. uniforms are not integrated with Meteor reactivity in any way.
Other thing that you could do would be to create a custom field component that will reactively fetch options and pass it to a standard SelectField.
Hi @radekmie:
Thank you for your help. I've created a simple SelectFieldReactive field based on your comment.
Pasting it here in case it helps someone else.
import { Mongo } from 'meteor/mongo';
import { useTracker } from 'meteor/react-meteor-data';
import React from 'react';
import { BaseField } from 'uniforms';
import { SelectField } from 'uniforms-bootstrap4';
import { useSubscription } from '/imports/ui/Hooks/useSubscription';
interface ISelectFieldReactiveProps {
name: string;
}
// tslint:disable-next-line: no-any
const _SelectFieldReactive = (props: ISelectFieldReactiveProps, uProps: any) => {
const fieldName = props.name;
// next line is ugly
const { subscriptions, component, collections, ...rest }
= uProps.uniforms.schema.schema._schema[fieldName].uniforms;
let loading = true;
subscriptions.forEach((s: string) => {
loading = loading && useSubscription(s);
});
const [colecs] = useTracker(() =>
collections.map(
(C: Mongo.Collection<unknown>) => C.find().fetch())
, [loading]);
if (loading) {
return null;
}
return (
<SelectField {...props} {...rest} data-force-render={colecs} />
);
};
_SelectFieldReactive.contextTypes = BaseField.contextTypes;
export const SelectFieldReactive = _SelectFieldReactive;
Then you can use it in schema definition as
export const SpeakersSchemaDefinition = {
...
country: {
type: String,
allowedValues: () => Countries.find().fetch().map(p => p._id),
uniforms: {
transform: (id: string) => Countries.findOne({ _id: id })?.name || `Error ${id}`,
component: SelectFieldReactive,
subscriptions: ['Countries.list'],
collections: [Countries],
},
},
...
}
The above code works, and I'm able to continue with my development. So Thank you very much for your time and your help.
If OK with you, I would like to ask for your advice an a couple of improvements I would like to implement.
1- Im accessing the uniforms field in the schema with uProps.uniforms.schema.schema._schema[fieldName].uniforms;, and needless to say its ugly :). What is the proper way to do so?
2- Probably related to '1', my code works OK with the example above, but it doesn't work as part of an array:
// THIS DOENST WORK
speakers: {
type: Array,
optional: true,
uniforms: {
component: ListField,
},
},
'speakers.$': {
type: String,
allowedValues: () => Speakers.find().map((d: ISpeaker) => d._id),
uniforms: {
transform: (id: string) => Speakers.findOne({ _id: id }).name,
component: SelectFieldReactive,
subscriptions: ['Speakers.list'],
collections: [Speakers],
},
},
Regards,
Rafa
connectField to wire this component with uniforms. It'll provide you all guaranteed props and field (schema field definition) is one of them.1. You could've used the schema directly, but it'll change in v3 (1. will work).Your component breaks the rules of hooks. I think you could fix and generalize it at once. Something like this:
// Component:
function Reactive({ field, name }) {
const props = useTracker(field.autorun, field.autorunDeps ?? []);
// This condition and "loading component" can be different.
return props.loading ? null : <AutoField {...props} name="" />;
}
const ReactiveField = connectField(Reactive);
// Schema:
export const SchemaDefinition = {
country: {
type: String,
uniforms: {
// Such functions can be extracted and shared with other `useTracker`,
// and `withTracker` calls. It makes them testable as well. Also, most of
// them will follow the same subscribe -> fetch pattern, therefore create
// a helper for that and you are set.
autorun() {
const subscriptions = [Meteor.subscribe('Countries.list')];
if (subscriptions.some(subscription => !subscription.ready())) {
return { loading: true };
}
const countries = Countries.find().fetch();
return { options: countries.map(country => ({ label: country.name, value: country._id })) };
},
// Use this one to specify hook dependencies.
autorunDeps: [],
},
};
I think the problem is already solved. If there will be something else - comment anyway!
Hi @radekmie ,
It took me some time, but I got it working: thank you!!
For completeness, will write here my solution based on yours.
import { useTracker } from 'meteor/react-meteor-data';
import React from 'react';
import { connectField } from 'uniforms';
interface IAutorunRes {
loading: boolean;
}
interface IReactiveFieldProps {
uniforms: {
autorun: () => IAutorunRes,
autorundeps: unknown[],
fieldcomponent: React.ComponentClass<{ name: string}, unknown>,
};
}
function Reactive({ field }: { field: IReactiveFieldProps }) {
const { autorun, autorundeps, fieldcomponent: Field } = field.uniforms;
const { loading, ...props } = useTracker(autorun, autorundeps ?? []);
return loading ? null : <Field {...props} name="" />;
}
filterDOMProps.register('autorun', 'autorundeps', 'fieldcomponent'); // this is necessary to avoid the warning mentioned in PS1 below
export const ReactiveField = connectField(Reactive);
And the schema:
export const SchemaDefinition = {
...
country: {
type: String,
allowedValues: () => Countries.find().fetch().map(p => p._id),
uniforms: {
component: ReactiveField,
autorun() {
const subscriptions = [Meteor.subscribe('Countries.list')];
if (subscriptions.some(subscription => !subscription.ready())) {
return { loading: true };
}
const countries = Countries.find().fetch();
return {
loading: false,
options: countries.map(country => ({ label: country.name, value: country._id })),
};
},
autorundeps: [],
fieldcomponent: SelectField,
},
},
Note that I needed to set the uniforms.compoment field in the schema, else was not using the ReactField at all. And related to that, need to define also fieldcomponent else (with AutoField) was entering an infinite/recursive loop.
Thank you so much for your help.
Rafa
PS: With my changes, React warns
react_devtools_backend.js:6 Warning: Invalid values for propsautorun,fieldcomponenton <div> tag. Either remove them from the element, or pass a string or number value to keep them in the DOM. For details, see https://fb.me/react-attribute-behavior . If I find how to solve this, will post it here too.
PS2: And this is the schema for arrays:
speakersId: {
type: Array,
optional: true,
uniforms: {
component: ListField,
removeIcon: DefaultRemoveIcon,
addIcon: DefaultAddIcon,
initialCount: 3,
},
label: 'disertantes',
},
'speakersId.$': {
type: String,
allowedValues: () => Speakers.find().map((d: ISpeaker) => d._id),
uniforms: {
component: ReactiveField,
autorun() {
const subscriptions = [Meteor.subscribe('Countries.list')];
if (subscriptions.some(subscription => !subscription.ready())) {
return { loading: true };
}
const items = Speakers.find().fetch();
return {
loading: false,
options: items.map(d => ({ label: `${d.lastName}, ${d.name}`, value: d._id })),
label: false,
style: { width: '80%' },
};
},
autorundeps: [],
fieldcomponent: SelectField,
},
},
About that warning - you can register your custom props with filterDOMProps.register.
Most helpful comment
About that warning - you can register your custom props with
filterDOMProps.register.