React-Final-Form -> Dynamically created from based on data from server - react-final-form

EDIT: THE IDEA BELLOW WORKS
I just noticed that the problem was in . notation of values I used. The path must be correctly converted to the same object structure as in case of tools changeValue).
Also, this code
...
const initialValues = form.getState().initialValues;
initialValues[fieldName] = fieldValue;
form.setConfig("initialValues", initialValues);
...
had to be changed to
...
const initialValues = form.getState().values;
initialValues[fieldName] = fieldValue;
form.setConfig("initialValues", initialValues);
...
as I wanted current values to be kept in the form instead of the form has to be "reinitialized".
I am using final-form for presenting a form partly generated dynamically based on the data templates and the data itself loaded from the server.
In order to generate the form and populate it with the values loaded from the server I am using custom mutator, which just sets the correct values once the form is generated and the data is loaded from the server.
Mutator works well, but I also need to set initialValues once the part of the form (based i.e. on initial value or user selected value of the combo box) is loaded and generated in order to prevent dirty state before the form is touched by the user.
Question: Is it possible to set initialValues dynamically, once the template/data is loaded in order to prevent pristine/dirty state, but also, without touching other form values and initialValues?
Please note, this is just example of children component used within the form declared in parent component (including the custom mutator). My original code is way too complex to pass it here. Maybe this code is not fully syntactically correct, maybe it would not work with checkbox, don't care too much about it - it is just an example and the question is about something else. Mutator works actually well, so please focus on initalValues.
interface Props {
fieldName: string;
fieldType: "checkbox" | "text";
fieldValue: string | boolean;
}
function DynamicFormField({
fieldName,
fieldType,
fieldValue
}: Props) {
const form = useForm();
useEffect(
() => {
// *** this is what I tried, but does not work ***
const initialValues = form.getState().initialValues;
initialValues[fieldName] = fieldValue;
form.setConfig("initialValues", initialValues);
// ***
// *** also tried ***
const initialValues = { ...form.getState().initialValues };
initialValues[fieldName] = fieldValue;
form.setConfig("initialValues", initialValues);
// ***
form.mutators.setValue(fieldName, fieldValue);
},
[fieldName, fieldValue]
)
}
return (
<Field name={fieldName}>
{({ input }) => (
{
fieldType === "checkbox" ?
(<input {...input} type="checkbox")
:
(<input {...input} type="text")
}
)}
</Field>
)

It works, please see edit.
The initialValues object must just have same structure as the values object.

Related

Display nested object dynamically to fill its inputs with data

I am using angular 7 as a front end for my project and want to display input fields for user to fill data using reactive forms.
The problem is that the object i am trying to display is having multiple layers,so the first layer can have input which can be dynamically added (so that i use fb.array when building the form) and object which can be added dynamically with all things it contains which can have the same or even a different structure and so on .
i have achieved that but through static way that includes the following:
-determine how many inputs and objects can be added dynamically (i can know that through an attribute in the object and the inputs which have same structures but different types ,input,list,object....),
-add nested form for each object with its children.
which means i have to add some code when ever the object getting deeper.
How can i achieve that dynamically so that i pass the object regardless of how much layers is has , and display it as a nested forms and can that be done with reactive form or i have to change the entire method?
This is what i have already tried :
this.check = this.objects.filter(e=>e.Max>1);
if(this.check.length === 1){
this.parentA=this.check[0];
this.childrenA=this.parentA[0].filter(e=>e.type != "object");
this.form = this.fb.group({
'A': this.fb.array([
this.initA(),
])
});
}else if(this.check.length === 2){
.
.
this.form = this.fb.group({
'A': this.fb.array([
this.initB()
])
});
}
initA() {
const elementFormGroup = this.fb.group({});
this.childrenA.forEach(e => {
eFormGroup.addControl(e.Name,
this.fb.control([], Validators.required));
}
}
initY() {debugger
const elementFormGroup = this.fb.group({});
this.childrenB.forEach(e => {
elementFormGroup.addControl(e.Name,
this.fb.control([], Validators.required));
});
elementFormGroup.addControl('A', this.fb.array([
this.initA()
]))
return elementFormGroup;
}
.
.
.
.
I would appreciate any help because i am really stuck at this way.
Thank you.

How do you add an initial selection for Angular Material Table SelectionModel?

The Angular Material documentation gives a nice example for how to add selection to a table (Table Selection docs). They even provide a Stackblitz to try it out.
I found in the code for the SelectionModel constructor that the first argument is whether there can be multiple selections made (true) or not (false). The second argument is an array of initially selected values.
In the demo, they don't have any initially selected values, so the second argument in their constructor (line 36) is an empty array ([]).
I want to change it so that there is an initially selected value, so I changed line 36 to:
selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);
This changes the checkbox in the header to an indeterminate state (as expected), but does not cause the row in the table to be selected. Am I setting the initial value incorrectly, or what am I missing here? How can I set an initially selected value?
Tricky one. You need to initialize the selection by extracting that particular PeriodicElement object from your dataSource input, and passing it to the constructor.
In this particular case, you could code
selection = new SelectionModel<PeriodicElement>(true, [this.dataSource.data[1]);
It's because of the way SelectionModel checks for active selections.
In your table markup you have
<mat-checkbox ... [checked]="selection.isSelected(row)"></mat-checkbox>
You expect this binding to mark the corresponding row as checked. But the method isSelected(row) won't recognize the object passed in here as being selected, because this is not the object your selection received in its constructor.
"row" points to an object from the actual MatTableDataSource input:
dataSource = new MatTableDataSource<PeriodicElement>(ELEMENT_DATA);
But the selection initialization:
selection = new SelectionModel<PeriodicElement>(true, [{position: 2, name: 'Helium', weight: 4.0026, symbol: 'He'}]);
happens with a new object you create on the fly. Your selection remembers THIS object as a selected one.
When angular evaluates the bindings in the markup, SelectionModel internally checks for object identity. It's going to look for the object that "row" points to in the internal set of selected objects.
Compare to lines 99-101 and 16 from the SelectionModel source code:
isSelected(value: T): boolean {
return this._selection.has(value);
}
and
private _selection = new Set<T>();
I was facing the same issue, I used dataSource to set the initial value manually in ngOnInit()
ngOnInit() {
this.dataSource.data.forEach(row => {
if (row.symbol == "H") this.selection.select(row);
});
}
If you do the following, it works too
selection = new SelectionModel<PeriodicElement>(true, [ELEMENT_DATA[1]])
To select all you can do
selection = new SelectionModel<PeriodicElement>(true, [...ELEMENT_DATA])
I hope the answer is helpful
Or more dynamically if you have a set of values and you want to filter them before:
selection = new SelectionModel<PeriodicElement>(true, [
...this.dataSource.data.filter(row => row.weight >= 4.0026)
]);
This gets more tricky if you have data loading asynchronously from an api. Here is how I did it:
Firstly I have implemented the DataSource from "#angular/cdk/table". I also have an RxJS Subject that fires whenever data is loaded (first time or when user changes page in the pagination section)
export abstract class BaseTableDataSource<T> implements DataSource<T>{
private dataSubject = new BehaviorSubject<T[]>([]);
private loadingSubject = new BehaviorSubject<boolean>(false);
private totalRecordsSubject = new BehaviorSubject<number>(null);
public loading$ = this.loadingSubject.asObservable();
public dataLoaded$ = this.dataSubject.asObservable();
public totalRecords$ = this.totalRecordsSubject.asObservable().pipe(filter(v => v != null));
constructor(){}
connect(collectionViewer: CollectionViewer): Observable<T[]>{
return this.dataSubject.asObservable();
}
disconnect(collectionViewer: CollectionViewer): void {
this.dataSubject.complete();
this.loadingSubject.complete();
this.totalRecordsSubject.complete();
}
abstract fetchData(pageIndex, pageSize, ...params:any[]) : Observable<TableData<T>>;
abstract columnMetadata(): {[colName: string]: ColMetadataDescriptor };
loadData(pageIndex, pageSize, params?:any[]): void{
this.loadingSubject.next(true);
this.fetchData(pageIndex, pageSize, params).pipe(
finalize(() => this.loadingSubject.next(false))
)
.subscribe(data => {
this.totalRecordsSubject.next(data.totalNumberOfRecords);
this.dataSubject.next(data.records)
});
}
}
Now when I want to pre-select a row, I can write a function like this in my component which hosts a table that uses an implementation of the above mentioned data source
selectRow(rowSelectionFn: (key: string) => boolean){
this.dataSource.dataLoaded$.pipe(takeUntil(this.destroyed$))
.subscribe(data => {
const foundRecord = data.filter(rec => rowSelectionFn(rec));
if(foundRecord && foundRecord.length >= 0){
this.selection.toggle(foundRecord[0]);
}
});
}

Breeze import with entity extended property of ko.observableArray() throwing error

Running into an error during import for entities extended with a ko.observableArray() property, vs. being extended as a simple array [] type in the constructor.
var customerCtor = function () {
this.extendedProp = ko.observable(true);
//this.extendedArray = ko.observableArray(); // causes error: Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
this.extendedArray = []; // this works just fine
};
I created a test along-side the Breeze v1.3.6 DocCode: exportImportTests.js "stash entire cache locally and restore" as my starting point, and here is the new test:
test("w/extended Customer, stash entire cache locally and restore", 3, function () {
var em1 = newEm();
var store = em1.metadataStore;
// extend Customer with observables
var customerCtor = function () {
this.extendedProp = ko.observable(true);
this.extendedArray = ko.observableArray(); // causes error: Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
//this.extendedArray = []; // but this will work just fine?
};
store.registerEntityTypeCtor("Customer", customerCtor);
var expected = testData.primeTheCache(em1);
// grab first Customer, push value onto extendedArray prop
var custEntity = em1.getEntities(expected.customerType)[0];
custEntity.extendedArray().push('some-value'); // even when defined as [], Breeze re-writes field as ko.observable
var exportData = em1.exportEntities();
var stashName = "stash_everything";
window.localStorage.setItem(stashName, exportData);
var importData = window.localStorage.getItem(stashName);
var em2 = new EntityManager(); // virginal - so register ctor on this instance
var store2 = em2.metadataStore;
store2.registerEntityTypeCtor("Customer", customerCtor);
em2.importEntities(importData);
var entitiesInCache = em2.getEntities();
var restoreCount = entitiesInCache.length;
equal(restoreCount, expected.entityCount,
"should have restored expected number of all entities");
var restoredCustomer = em2.getEntities(expected.customerType)[0];
ok(restoredCustomer.extendedProp(), 'extended property present');
ok(restoredCustomer.extendedArray().length > 0, 'extended Array present and has data');
});
An em2.importEntities(importData); throws the error:
Error: Cannot write a value to a ko.computed unless you specify a 'write' option. If you wish to read the current value, don't pass any parameters.
at Error (<anonymous>)
at h [as extendedArray] (http://localhost:47595/Scripts/knockout-2.2.1.js:44:167)
at ctor.initializeEntityPrototype.proto.setProperty (http://localhost:47595/Scripts/breeze.debug.js:14634:31)
at updateTargetPropertyFromRaw (http://localhost:47595/Scripts/breeze.debug.js:13062:24)
at aspectName (http://localhost:47595/Scripts/breeze.debug.js:13025:13)
at Array.forEach (native)
at updateTargetFromRaw (http://localhost:47595/Scripts/breeze.debug.js:13023:19)
at em._inKeyFixup (http://localhost:47595/Scripts/breeze.debug.js:12601:17)
at Array.forEach (native)
at importEntityGroup (http://localhost:47595/Scripts/breeze.debug.js:12568:28)
As Breeze always rewrites constructor fields (in my case for KO), defining as [] works. But not sure why this would happen when the property is pre-defined?
Anyone run into this, or have I missed a doc note somewhere?
We'll look at it.
Yes, Breeze assumes every property added in a constructor is supposed to be rewritten per the prevailing "model library" which, in your case, is KO. Therefore, no surprise that the array becomes a ko.observableArray.
Moreover, because such a property is presumed to be under Breeze's jurisdiction, we have to tie it into Breeze observability and serialization mechanisms which means we re-write it as a Breeze-flavored observable array. Such an array is a computed.
Evidently there is some problem with the way we're doing this for a property which is "unmapped". We'll look at that.
N.B.: I am assuming (and your code confirms) that the array property, extendedArray, is an "unmapped property" in the Breeze sense. That should be fine.
You should not mention mapped collection navigation properties in your constructor. There is no valid reason to do so that I can think of. The main reason to mention a mapped property in the constructor is (a) to give it a default value or (b) make it available to a custom (unmapped) computed property. There is no reasonable alternative default value for a collection navigation property (empty is the default) and it would be rare/avoidable to include it in a computed.

How to display previous value on Min Miles text field

I want to display a previous value on Min Miles and that should not be editable. I want like
Default value of Min Miles is 0.
When I click on Add More Range then In the new form - Min Value should be Max Value of Previous Form.
I am using semantic form for. Please Help Me. How can I do this...
Regarding your second question, and assuming that the new form appears through javascript, without page reloading, you can grab the
field value with javascript and use it as the default value for the
new field. The "add new range"
Something Like
function getvalue(){
var inputTypes_max = [],inputTypes_min = [],inputTypes_amount = [];
$('input[id$="max_miles"]').each(function(){
inputTypes_max.push($(this).prop('value'));
});
$('input[id$="amount"]').each(function(){
inputTypes_amount.push($(this).prop('value'));
});
var max_value_of_last_partition = inputTypes_max[inputTypes_max.length - 2]
var amount_of_last_partition = inputTypes_amount[inputTypes_amount.length - 2]
if (max_value_of_last_partition == "" || amount_of_last_partition == "" ){
alert("Please Fill Above Details First");
}else{
$("#add_more_range_link").click();
$('input[id$="min_miles"]').each(function(){
inputTypes_min.push($(this).prop('id'));
});
var min_id_of_last_partition=inputTypes_min[inputTypes_min.length - 2]
$("#"+min_id_of_last_partition).attr("disabled", true);
$("#"+min_id_of_last_partition).val(parseInt(max_value_of_last_partition) + 1)
}
}
I have Used Jquery's End Selector In a loop to get all value of max and amount field as per your form and get the ids of your min_miles field and then setting that value of your min_miles as per max_miles
It worked For me hope It works For You.
Default value of a field can just be passed in the form builder as a second parameter:
...
f.input :min_miles, "My default value"
Of course I do not know your model structure but you get the idea.
Regarding your second question, and assuming that the new form appears through javascript, without page reloading, you can grab the field value with javascript and use it as the default value for the new field. The "add new range" click will be the triggerer for the value capture.
Something like (with jQuery):
var temp_value = '';
$('#add_more_range').click(function(){
temp_value = $('#my_form1 #min_miles').value();
$('#my_form2 #max_miles').value(temp_value);
});
Again I am just guessing the name of the selectors, but the overall approach should work.
If you are also adding dinamically to the page the "Add new range" buttons/links, then you should delegate the function in order to be inherited also for the so new added buttons:
$('body').on('click', '#add_more_range', function(){...});

Processing multiple select controls within jquery mobile form

I am trying to process multiple input selects in a form each one has a unique name and id.
here is my first try, this is broken when y = value.val(); executes
var selects = $("#pmWorkOrderProcedureStepsForm").find('select');
$.each(selects,
function(index, value)
{
y = value.val();
});
I can see in chrome debug that value has a reference to something that looks like
HTMLSelectElement#select-choice-400139826
Where select-choice-400139826 is my first select input name.
How do I get just the name and the selected value of the input from here.
New to jquery mobile!
You can use the following code snippet:
var selects = $("#pmWorkOrderProcedureStepsForm").find("select");
$.each(selects,function(){
name = $(this).attr('name');
value = $(this).val();
});
A demo here - http://jsfiddle.net/5xg6F/
Let me know if that helps.

Resources