Angular2 ControlValueAccessor - binding

i'm trying to understand how ControlValueAccessor work precisely.
I have studied the behavior of it with two different control component:
The first is suppose to provide a primitive value: a single number.
The second provide a complex object.
So in short:
class FirstControlComponent implements ControlValueAccessor {
// ...
value:number = 10;
writeValue(value: number) {
this.value = value;
}
// ...
}
class SecondControlComponent implements ControlValueAccessor {
// ...
value:any = {};
writeValue(value: any) {
this.value = value;
}
// ...
}
The ControlValueAccessor interface only specify a 'setter': writeValue, but no 'getter'.
So when i bind a Control to SecondControlComponent, something like:
this.form = this.builder.group({
controlName: this.builder.control(this.theObject) });
and later in the template:
<second-component ngControl='controlName'> <second-component>
Everything works just fine, because writeValue is called on init with a reference to the existing theObject object, so the control modify the same instance of the object (hope i'm clear)
BUT: if i do exactly the same thing with FirstControlComponent, because the value is not passed as a reference (cause it's a primitive), and because ControlValueAccessor do not provide a 'setter' the value in my control and the value in my host component are NOT kept in sync ...
Does this mean that we HAVE to pass Object and not primitive to custom control implementing the ControlValueAccessor? I Guess no, so i guess i must be misunderstanding something .. :)
I'm using it the right way ?
Any hints are welcome !
Thanks !

It's not clear to me what you try to do here but ControlValueAccessor is an entity that you need to register for your element. Something like that:
const CUSTOM_VALUE_ACCESSOR = new Provider(
NG_VALUE_ACCESSOR, {useExisting: forwardRef(() => LabelsValueAccessor), multi: true});
#Directive({
(...)
providers: [CUSTOM_VALUE_ACCESSOR]
})
export class LabelsValueAccessor implements ControlValueAccessor {
(...)
}
It will then take part of the value updates (from component and from the template). When you set a value within the component on the input (for example), the writeValue method is called in your value accessor. If you want to update the input value from the value accessor, you need to leverage the registered onChange callback by Angular2
See this article (section "NgModel-compatible component") for more details:
http://restlet.com/blog/2016/02/17/implementing-angular2-forms-beyond-basics-part-2/

In Parent Component I want output of form like this {someOtherControlName: any, controlName: number} so output of Child form must be number.also in update form (example) parentform.patchValue({someOtherControlName: '', controlNmae: 3}) value of formControl must set properly
#Component({
selector: 'parent',
template: `
<div [formGroup]="form">
<second-component [formControl]='form.controlName'> <second-component>
<div formControlName="someOtherControlName"></div>
export class ParentComponent {
parentForm = new FormGroup({
controlName: new FormControl(), <<<<<======important
someOtherControlName: new FormControl()
})
}
In ControlValueAccessor:
#Component({
selector: 'counter',
template: `
<div class="number-input" [formGroup]="form">
<input type="text" class="form-control text-center" formControlName="counter" />
</div>
`,
styleUrls: ['counter.component.scss'],
providers: [
{
provide: NG_VALUE_ACCESSOR,
useExisting: CounterComponent,
multi: true,
},
],
})
export class CounterComponent implements ControlValueAccessor, OnDestroy, OnInit {
form = new FormGroup({
counter: new FormControl(0, Validators.required),
})
private onChange: (value: any) => void = value => {}
private onTouched: () => void = () => {}
myValue: number
private onDestroy$: Subject<void> = new Subject()
ngOnInit() {
this.form.valueChanges
.pipe(
tap(value => {
this.onChange(typeof value === 'number' ? value : value.counter) <<<< important
this.onTouched()
}),
takeUntil(this.onDestroy$)
)
.subscribe()
}
}
writeValue(value: number | { counter: number }) {
this.myValue = typeof value === 'number' ? value : value.counter
this.form.setValue({ counter: value > 0 ? value : 0 })
}
registerOnChange(fn: () => {}) {
this.onChange = fn
}
registerOnTouched(fn: () => {}) {
this.onTouched = fn
}
setDisabledState?(isDisabled: boolean): void {
// when the parent updates the
// state of the form control
if (isDisabled) {
this.form.disable()
} else {
this.form.enable()
}
}
ngOnDestroy(): void {
this.onDestroy$.next()
this.onDestroy$.complete()
}

Related

Set a form value using react-hook-form within React-Admin

In my React-Admin app, I'd like to leverage react-hook-form's useFormContext for various things, such as, for example, setting the default pre-selected choice in this custom input field:
...
import {
Create, SimpleForm, SelectInput
} from 'react-admin';
import { useFormContext } from 'react-hook-form';
const MyInput = () => {
const formContext = useFormContext();
formContext.setValue('category', 'tech');
return (
<SelectInput source="category" choices={[
{ id: 'tech', name: 'Tech' },
{ id: 'people', name: 'People' },
]}
/>
);
};
...
const ItemCreate = () => {
return (
<Create>
<SimpleForm>
<MyInput />
</SimpleForm>
</Create>
);
};
...
This sets the pre-selected value of the field, just as intended. But it throws a warning: Cannot update a component ("Form") while rendering a different component ("MyInput")...
Is there some way to achieve this without getting the warning?
Note: The only reason I'm using a custom input field here is because when I put useFormContext() directly into the component that contains SimpleForm it returns null (similarly described here).
The warning is related to the fact that the entire body of the MyInput() function is executed during each render, you need to call the setValue() function inside the useEffect hook.
Got this working by moving formContext.setValue into a useEffect hook:
...
import {
Create, SimpleForm, SelectInput
} from 'react-admin';
import { useFormContext } from 'react-hook-form';
const MyInput = () => {
const formContext = useFormContext();
// moved the setValue into a useEffect
useEffect(() => {
formContext.setValue('category', 'tech');
});
return (
<SelectInput source="category" choices={[
{ id: 'tech', name: 'Tech' },
{ id: 'people', name: 'People' },
]}
/>
);
};
...
const ItemCreate = () => {
return (
<Create>
<SimpleForm>
<MyInput />
</SimpleForm>
</Create>
);
};
...

how to retrieve form values and labels from react hook form and antd Select

I am using antd Select and react hook form via 'Controller'. I am populating the Select options from a fetched data with structure;
{
{
"id": "232342",
"term": "hello"
}
{
"id": "232342",
"term": "hello"
}
}
the Select component properly displays the term for selection. However, i want to retrieve both the 'id'and 'term' of the selected and use it to populate another json object.
getValues(" ") retrieves the 'id' only. How do i retrieve and access both the 'id' and 'term'.
Here is a portion of code:
import React from 'react'
import { useForm, Controller } from 'react-hook-form'
import { Select } from 'antd'
const { Option } = Select
export default function PatientRegistrationForm({ options }) {
const { register, handleSubmit, getValues, control, formState: { errors } } = useForm({
defaultValues: {
customer: "",
}
})
const children = []
for (const {id, pt: {term}} of options){
children.push(<Option key={id}>{term}</Option>)
}
// Define the retrieved values from the form
const retrievedID = getValues("customer")
// Use the retreived values to populate this object
const customer = {
customerId = retrievedID
customerName = "nothing happens here"
},
return (
<div className="">
<form onSubmit={handleSubmit(onSubmit)} className="">
<section>
<Controller
control={control}
name="customer"
render={({ field }) => (
<Select {...field} defaultValue=""
bordered={true}
filterOption={true}
className="form-control"
>
{ children }
</Select>
)}
/>
</section>
</form>
</div>
);
}
Thanks in advance for your help.
You have to retrieve the option manually using something like:
const retrievedID = getValues("customer")
const retrievedOption = options.find(option => option.id === retrievedID)
const customer = {
customerId: retrievedID,
customerName: retrievedOption.term
}
thank you #sgarcia.dev for your answer. I know its been a while but i want to put it out here incase it helps someone else. It turns out it had little to do with react hook form. Ant design select component has a prop 'labelInValue' which returns an object containing both the label and value.

How to use mobx observer with antd table columns render function

When wrapping an antd table column render function with observer, I get an error of render is not a function. I'm trying to avoid the need to every time create a separate observer wrapped functional component that I call from a column render function. Anybody had any luck or knows how to directly wrap a column render function with an observer (or an observer like implementation)?
Here is the typescript code:
import { observer } from "mobx-react-lite";
import { Button } from "antd";
import { ColumnsType } from "antd/lib/table";
import { TableDataSourceRecord } from "../models/TableDataSourceRecord";
const columns: ColumnsType<TableDataSourceRecord> = [
{
title: "View",
dataIndex: "url",
key: "url",
render: observer((url: any, _record: TableDataSourceRecord) => {
return (
<Button type="link" href={url.value}>
View
</Button>
);
}),
},
];
Thanks
Since render expects a function you can't pass React.Component to it, I guess (observer creates component).
Have you tried <Observer> wrapper component?
import { Observer } from "mobx-react-lite";
import { Button } from "antd";
import { ColumnsType } from "antd/lib/table";
import { TableDataSourceRecord } from "../models/TableDataSourceRecord";
const columns: ColumnsType<TableDataSourceRecord> = [
{
title: "View",
dataIndex: "url",
key: "url",
render: (url: string, _record: TableDataSourceRecord) => {
return (
<Observer>
{() => (
<Button type="link" href={url}>
View
</Button>
)}
</Observer>
);
},
},
];
Anyway, there is no point in having Observer in your example since you have no observable values there, url is just string primitive.

Relay Modern updater ConnectionHandler.getConnection() returns undefined when parent record is root

Debugging update:
So, we went a bit further in debugging this and it seems like 'client:root' cannot access the connection at all by itself.
To debug the complete store, we added this line in the updater function after exporting the store variable from the relay/environment.
console.log(relayEnvStore.getSource().toJSON())
If I use .get() with the specific string client:root:__ItemList_items_connection, I can access the records I have been looking for but it's definitely not pretty.
const testStore = store.get('client:root:__ItemList_items_connection')
console.log(testStore.getLinkedRecords('edges'))
Original:
I'm using Relay Modern and trying to update the cache after the updateItem mutation is completed with the updater. The call to ConnectionHandler.getConnection('client:root', 'ItemList_items') returns undefined.
I'm not sure if it's because I'm trying to use 'client:root' as my parent record or if there's a problem with my code. Has anyone found themselves with a similar issue?
Here's the paginationContainer:
const ItemListPaginationContainer = createPaginationContainer(
ItemList,
{
node: graphql`
fragment ItemList_node on Query
#argumentDefinitions(count: { type: "Int", defaultValue: 3 }, cursor: { type: "String" }) {
items(first: $count, after: $cursor) #connection(key: "ItemList_items") {
edges {
cursor
node {
id
name
}
}
pageInfo {
hasNextPage
hasPreviousPage
startCursor
endCursor
}
}
}
`
},
{
direction: 'forward',
getConnectionFromProps: props => props.node && props.node.items,
getVariables(props, { count, cursor }) {
return {
count,
cursor
}
},
query: graphql`
query ItemListQuery($count: Int!, $cursor: String) {
...ItemList_node #arguments(count: $count, cursor: $cursor)
}
`
}
)
Here's the mutation:
const mutation = graphql`
mutation UpdateItemMutation($id: ID!, $name: String) {
updateItem(id: $id, name: $name) {
id
name
}
}
`
Here's the updater:
updater: (store) => {
const root = store.getRoot()
const conn = ConnectionHandler.getConnection(
root, // parent record
'ItemList_items' // connection key
)
console.log(conn)
},
Turns out that I was setting my environment incorrectly. The store would reset itself every time I would make a query or a mutation, hence why I couldn't access any of the connections. I initially had the following:
export default server => {
return new Environment({
network: network(server),
store: new Store(new RecordSource())
})
}
All connections are accessible with this change:
const storeObject = new Store(new RecordSource())
export default server => {
return new Environment({
network: network(server),
store: storeObject
})
}

Define fragments based on fetched data

Can anyone explain how I can define fragments on Relay container based on data which currently fetched in component? Consider this example:
const TreeNodeContainer = Relay.createContainer(TreeNode, {
fragments: {
node: (variables) => Relay.QL`
fragment on TreeNode {
id,
expanded,
title,
children(first: 1000) #include(if: $expanded) {
edges {
node {
id,
${TreeNodeContainer.getFragment('node').if('how I can get component props or "expanded" value here?')}
}
}
}
}
`,
},
});
TreeNode component is simple enough - it has mutation which changed 'expanded' field on server.
class TreeNode extends React.Component {
handleClick() {
Relay.Store.update(new ExpandNodeMutation({node: this.props.node}));
}
render() {
var node = this.props.node;
var variables = this.props.relay.variables;
return (
<div className="node">
<div className="title" onClick={this.handleClick.bind(this)}> {node.title}</div>
<div className="children" style={{paddingLeft: 20}}>
{node.expanded && node.children && node.children.edges.map((edge)=> {
return <TreeNodeContainer node={edge.node} key={edge.node.id}/>
})}
</div>
</div>
)
}
}
Use .if(variables.expanded):
const TreeNodeContainer = Relay.createContainer(TreeNode, {
fragments: {
node: variables => Relay.QL`
fragment on TreeNode {
# ...
children(first: 1000) #include(if: $expanded) {
# ...
${TreeNodeContainer.getFragment('node').if(variables.expanded)}

Resources