Proper custom component with complex data in it - ag-grid

I have following interface:
export interface Product {
name: string;
provider: {
name: string;
logo: string;
};
pricePerUnit: {
quantity: number;
currency: string;
};
}
And my rowData looks like this:
rowData = [
{
name: 'Fish',
provider: {
name: 'Amazon',
logo: 'url to amazon logo',
},
pricePerUnit: {
quantity: 5,
currency: 'USD',
},
},
]
So, as you can see i have at least 2 complex object here, and by design I should display provider as img + name and price as quantity + currency symbol.
I`m using custom angular components for that with styling.
Actual problem
In order to provide these object to my custom components, I set field property in colDefs as follow (example for price):
{
headerName: 'Price',
field: 'pricePerUnit',
cellRenderer: PriceCellRendererComponent,
},
And here is the catch, because I specified in field property complex object, I no longer able to visualize data using integrated charts, because for them to work I should specify in my field propery path to number itself, like so:
{
field: 'pricePerUnit.quantity',
}
But now I`ve broke my custom component because params.value now holds just a number and not my complex object. Same goes to provider.
And it`s also broke grouping, sorting, filtering.
html template for one of my custom component (provider) looks like so:
<div class="wrapper provider">
<tui-avatar [avatarUrl]="params.value.logo" class="provider__logo"></tui-avatar>
<div class="provider__name">{{params.value.name}}</div>
</div>
So the question is:
How to properly setup custom components, so they would work in grouping, sorting, filtering and also integrated charts would use just simple primitive like number to correctly display data?

Related

Ag-grid column definition and `__metadata__` property

I need to introduce object with my own properties in columnDefs object.
After, I did it, I see in dev console
So, here has been written, that I could mean __metadata__ property. Does this property suit to mine purpose?
I've not found any information in types and docs about this property
ColDef has no custom state property that you could use. Certainly do not touch __metadata__. The name sounds like some internal implementation detail.
You can store your metadata in a separate object, for example using colId as a key:
columnDefs: ColDef[] = [
{
colId: 'id',
field: 'id'
},
{
colId: 'name',
field: 'name'
}
];
columnMetadata: {
id: 'something custom',
name: 'custom data'
}
private getColumnMetadata(column: Column) {
return this.columnMetadata[column.getColId()];
}

How to get v-select items inside items?

I have a dropdown with v-select
<v-select
label="Select"
v-bind:items="companies"
v-model="e11"
item-text="employee.name"
item-value="name"
max-height="auto"
>
</v-select>
Data from API like
new Vue({
el: '#app',
data () {
return {
e11: [],
companies: [
{
companyName: 'Google',
employee: [{
name: 'Sandra Adams',
name: 'Ali Connors',
name: 'Trevor Hansen',
name: 'Tucker Smith',
}]
},
{
companyName: 'Facebook',
employee: [{
name: 'Sandra Adams',
name: 'Ali Connors',
name: 'Trevor Hansen',
name: 'Tucker Smith',
}]
},
{
companyName: 'Twitter',
employee: [{
name: 'Sandra Adams',
name: 'Ali Connors',
name: 'Trevor Hansen',
name: 'Tucker Smith',
}]
},
]
}
}
})
In dropdown item getting [object object] to fixed fixed it? I want to get dropdown list with grouped by company name.
Here is what I want to do
Here is the codepen link https://codepen.io/yurenlimbu/pen/bGVKGEa?editors=1011
I had a crack at it, as ive also had some issues in the past with vuetify dropdown, i managed to get it to work, you may need to update your current data objects to make the name key unique in the list.
Added computed property to map companies and employees and inject the employees company so we have one object with the data we need
mappedNames: function() {
return this.companies.map(function(person) {
person.employee.forEach(e => e.company = person.companyName)
return person.employee
})
}
Helper function to concat the arrays from the map
item-value prop update to access entire object, and linked to e11 v-model
Here is the codepen:
https://codepen.io/mcwalshh/pen/Vwvdvve
EDIT:
I've included the slot for item and selected item, so you can change how the data is displayed inside the select:
<template v-slot:selection="data">
{{ data.item.name }} - {{ data.item.company }}
</template>
ref: https://vuetifyjs.com/en/components/selects/
Here is the updated codepen:
https://codepen.io/mcwalshh/pen/rNOKqvd
EDIT #2:
Not really sure how to go about adding titles, i suggest reading more vuetify documentation, or perhaps in my example, use the helper in the text-name instead of the items to access companyName. But following from my previous posts, i think the potential way of doing it, is to inject/unshift an object into the employees with:
{ name: person.employee.companyName, type: "title" }
For now in my example i add it straight to the object, then use template v-if's to determine what is a title:
codepen: https://codepen.io/mcwalshh/pen/NWGzeoK
Now just a matter of targeting parent to match your gif, but im in no way saying this is the best approach, like i said before i had issues with vuetify in the past, so i hope this can help get you by until you work out the best method

Angular 2 - large scale application forms' handling

At the company I’m working for, we’re developing a large scale application with multiple forms, that the user needs to fill in in order to register for our program. When all questions have been answered, then the user reaches a section that sums up all their answers, highlights invalid answers and gives the user the chance to revisit any of the preceding form steps and revise their answers. This logic will be repeated across a range of top-level sections, each having multiple steps/pages and a summary page.
To accomplish this, we have created a component for each separate form step (they are categories like “Personal Details” or “Qualifications” etc.) along with their respective routes and a component for the Summary Page.
In order to keep it as DRY as possible, we started creating a “master” service which holds the information for all the different form steps (values, validity etc.).
import { Injectable } from '#angular/core';
import { Validators } from '#angular/forms';
import { ValidationService } from '../components/validation/index';
#Injectable()
export class FormControlsService {
static getFormControls() {
return [
{
name: 'personalDetailsForm$',
groups: {
name$: [
{
name: 'firstname$',
validations: [
Validators.required,
Validators.minLength(2)
]
},
{
name: 'lastname$',
validations: [
Validators.required,
Validators.minLength(2)
]
}
],
gender$: [
{
name: 'gender$',
validations: [
Validators.required
]
}
],
address$: [
{
name: 'streetaddress$',
validations: [
Validators.required
]
},
{
name: 'city$',
validations: [
Validators.required
]
},
{
name: 'state$',
validations: [
Validators.required
]
},
{
name: 'zip$',
validations: [
Validators.required
]
},
{
name: 'country$',
validations: [
Validators.required
]
}
],
phone$: [
{
name: 'phone$',
validations: [
Validators.required
]
},
{
name: 'countrycode$',
validations: [
Validators.required
]
}
],
}
},
{
name: 'parentForm$',
groups: {
all: [
{
name: 'parentName$',
validations: [
Validators.required
]
},
{
name: 'parentEmail$',
validations: [
ValidationService.emailValidator
]
},
{
name: 'parentOccupation$'
},
{
name: 'parentTelephone$'
}
]
}
},
{
name: 'responsibilitiesForm$',
groups: {
all: [
{
name: 'hasDrivingLicense$',
validations: [
Validators.required,
]
},
{
name: 'drivingMonth$',
validations: [
ValidationService.monthValidator
]
},
{
name: 'drivingYear$',
validations: [
ValidationService.yearValidator
]
},
{
name: 'driveTimesPerWeek$',
validations: [
Validators.required
]
},
]
}
}
];
}
}
That service is being used by all the components in order to set up the HTML form bindings for each, by accessing the corresponding object key and creating nested form groups, as well as by the Summary page, whose presentation layer is only 1way bound (Model -> View).
export class FormManagerService {
mainForm: FormGroup;
constructor(private fb: FormBuilder) {
}
setupFormControls() {
let allForms = {};
this.forms = FormControlsService.getFormControls();
for (let form of this.forms) {
let resultingForm = {};
Object.keys(form['groups']).forEach(group => {
let formGroup = {};
for (let field of form['groups'][group]) {
formGroup[field.name] = ['', this.getFieldValidators(field)];
}
resultingForm[group] = this.fb.group(formGroup);
});
allForms[form.name] = this.fb.group(resultingForm);
}
this.mainForm = this.fb.group(allForms);
}
getFieldValidators(field): Validators[] {
let result = [];
for (let validation of field.validations) {
result.push(validation);
}
return (result.length > 0) ? [Validators.compose(result)] : [];
}
}
After, we started using the following syntax in the components in order to reach the form controls specified in the master form service:
personalDetailsForm$: AbstractControl;
streetaddress$: AbstractControl;
constructor(private fm: FormManagerService) {
this.personalDetailsForm$ = this.fm.mainForm.controls['personalDetailsForm$'];
this.streetaddress$ = this.personalDetailsForm$['controls']['address$']['controls']['streetaddress$'];
}
which seems like a code smell in our inexperienced eyes. We have strong concerns how an application like this will scale, given the amount of sections we'll have in the end.
We’ve been discussing different solutions but we can’t come up with one that leverages Angular’s form engine, allows us to keep our validation hierarchy intact and is also simple.
Is there a better way to achieve what we’re trying to do?
I commented elsewhere about #ngrx/store, and while I still recommend it, I believe I was misunderstanding your problem slightly.
Anyway, your FormsControlService is basically a global const. Seriously, replace the export class FormControlService ... with
export const formControlsDefinitions = {
// ...
};
and what difference does it make? Instead of getting a service, you just import the object. And since we're now thinking of it as a typed const global, we can define the interfaces we use...
export interface ModelControl<T> {
name: string;
validators: ValidatorFn[];
}
export interface ModelGroup<T> {
name: string;
// Any subgroups of the group
groups?: ModelGroup<any>[];
// Any form controls of the group
controls?: ModelControl<any>[];
}
and since we've done that, we can move the definitions of the individual form groups out of the single monolithic module and define the form group where we define the model. Much cleaner.
// personal_details.ts
export interface PersonalDetails {
...
}
export const personalDetailsFormGroup: ModelGroup<PersonalDetails> = {
name: 'personalDetails$';
groups: [...]
}
But now we have all these individual form group definitions scattered throughout our modules and no way to collect them all :( We need some way to know all the form groups in our application.
But we don't know how many modules we'll have in future, and we might want to lazy load them, so their model groups might not be registered at application start.
Inversion of control to the rescue! Let's make a service, with a single injected dependency -- a multi-provider which can be injected with all our scattered form groups when we distribute them throughout our modules.
export const MODEL_GROUP = new OpaqueToken('my_model_group');
/**
* All the form controls for the application
*/
export class FormControlService {
constructor(
#Inject(MMODEL_GROUP) rootControls: ModelGroup<any>[]
) {}
getControl(name: string): AbstractControl { /etc. }
}
then create a manifest module somewhere (which is injected into the "core" app module), building your FormService
#NgModule({
providers : [
{provide: MODEL_GROUP, useValue: personalDetailsFormGroup, multi: true}
// and all your other form groups
// finally inject our service, which knows about all the form controls
// our app will ever use.
FormControlService
]
})
export class CoreFormControlsModule {}
We've now got a solution which is:
more local, the form controls are declared alongside the models
more scalable, just need to add a form control and then add it to the manifest module; and
less monolithic, no "god" config classes.
Your approach and Ovangle's one seem to be pretty good but even though this SO question is solved, I want to share my solution because it's a really different approach that I think you might like or might be useful to someone else.
what solutions there are for an app wide form where Components take care of different sub parts to the global form.
We've faced that exact same issue and after months of struggling with huge, nested and sometimes polymorphic forms, we've come up with a solution that pleases us, which is simple to use and which gives us "super powers" (like type safety within both TS and HTML), access to nested errors and others.
We've decided to extract that into a separated library and open source it.
Source code is available here: https://github.com/cloudnc/ngx-sub-form
And the npm package can be installed like that npm i ngx-sub-form
Behind the scenes, our library uses ControlValueAccessor and that allows us to use it on template forms AND reactive forms (you'll get the best out of it by using reactive forms though).
So what is it all about?
Before I start explaining, if you prefer to follow along with a proper editor I've made a Stackblitz example: https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Well an example is worth a 1000 words I guess so let's redo one part of your form (the hardest one with nested data): personalDetailsForm$
First thing to do is make sure everything is going to be type safe. Let's create the interfaces for that:
export enum Gender {
MALE = 'Male',
FEMALE = 'Female',
Other = 'Other',
}
export interface Name {
firstname: string;
lastname: string;
}
export interface Address {
streetaddress: string;
city: string;
state: string;
zip: string;
country: string;
}
export interface Phone {
phone: string;
countrycode: string;
}
export interface PersonalDetails {
name: Name;
gender: Gender;
address: Address;
phone: Phone;
}
export interface MainForm {
// this is one example out of what you posted
personalDetails: PersonalDetails;
// you'll probably want to add `parent` and `responsibilities` here too
// which I'm not going to do because `personalDetails` covers it all :)
}
Then, we can create a component that extends NgxSubFormComponent.
Let's call it personal-details-form.component.
#Component({
selector: 'app-personal-details-form',
templateUrl: './personal-details-form.component.html',
styleUrls: ['./personal-details-form.component.css'],
providers: subformComponentProviders(PersonalDetailsFormComponent)
})
export class PersonalDetailsFormComponent extends NgxSubFormComponent<PersonalDetails> {
protected getFormControls(): Controls<PersonalDetails> {
return {
name: new FormControl(null, { validators: [Validators.required] }),
gender: new FormControl(null, { validators: [Validators.required] }),
address: new FormControl(null, { validators: [Validators.required] }),
phone: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Few things to notice here:
NgxSubFormComponent<PersonalDetails> is going to give us type safety
We have to implements the getFormControls methods which expects a dictionary of the top level keys matching an abstract control (here name, gender, address, phone)
We keep full control over the options to create the formControl (validators, async validators etc)
providers: subformComponentProviders(PersonalDetailsFormComponent) is a small utility function to create the providers necessary to use a ControlValueAccessor (cf Angular doc), you just need to pass as argument the current component
Now, for every entry of name, gender, address, phone that is an object, we create a sub form for it (so in this case everything but gender).
Here's an example with phone:
#Component({
selector: 'app-phone-form',
templateUrl: './phone-form.component.html',
styleUrls: ['./phone-form.component.css'],
providers: subformComponentProviders(PhoneFormComponent)
})
export class PhoneFormComponent extends NgxSubFormComponent<Phone> {
protected getFormControls(): Controls<Phone> {
return {
phone: new FormControl(null, { validators: [Validators.required] }),
countrycode: new FormControl(null, { validators: [Validators.required] }),
};
}
}
Now, let's write the template for it:
<div [formGroup]="formGroup">
<input type="text" placeholder="Phone" [formControlName]="formControlNames.phone">
<input type="text" placeholder="Country code" [formControlName]="formControlNames.countrycode">
</div>
Notice that:
We define <div [formGroup]="formGroup">, the formGroup here is provided by NgxSubFormComponent you don't have to create it yourself
[formControlName]="formControlNames.phone" we use property binding to have a dynamic formControlName and then use formControlNames. This type safety mechanism is offered by NgxSubFormComponent too and if your interface changes at some point (we all know about refactors...), not only your TS will error for missing properties in the form but also the HTML (when you compile with AOT)!
Next step: Let's build the PersonalDetailsFormComponent template but first just add that line into the TS: public Gender: typeof Gender = Gender; so we can safely access the enum from the view
<div [formGroup]="formGroup">
<app-name-form [formControlName]="formControlNames.name"></app-name-form>
<select [formControlName]="formControlNames.gender">
<option *ngFor="let gender of Gender | keyvalue" [value]="gender.value">{{ gender.value }}</option>
</select>
<app-address-form [formControlName]="formControlNames.address"></app-address-form>
<app-phone-form [formControlName]="formControlNames.phone"></app-phone-form>
</div>
Notice how we delegate the responsibility to a sub component? <app-name-form [formControlName]="formControlNames.name"></app-name-form> that's the key point here!
Final step: built the top form component
Good news, we can also use NgxSubFormComponent to enjoy type safety!
#Component({
selector: 'my-app',
templateUrl: './app.component.html',
styleUrls: ['./app.component.css']
})
export class AppComponent extends NgxSubFormComponent<MainForm> {
protected getFormControls(): Controls<MainForm> {
return {
personalDetails: new FormControl(null, { validators: [Validators.required] }),
};
}
}
And the template:
<form [formGroup]="formGroup">
<app-personal-details-form [formControlName]="formControlNames.personalDetails"></app-personal-details-form>
</form>
<!-- let see how the form values looks like! -->
<h1>Values:</h1>
<pre>{{ formGroupValues | json }}</pre>
<!-- let see if there's any error (works with nested ones!) -->
<h1>Errors:</h1>
<pre>{{ formGroupErrors | json }}</pre>
Takeaway from all of that:
- Type safe forms
- Reusable! Needs to reuse the address one for the parents? Sure, no worries
- Nice utilities to build nested forms, access form control names, form values, form errors (+nested!)
- Have you noticed any complex logic at all? No observables, no service to inject... Just defining interfaces, extending a class, pass an object with the form controls and create the view. That's it
By the way, here's a live demo of everything I've been talking about:
https://stackblitz.com/edit/so-question-angular-2-large-scale-application-forms-handling
Also, it was not necessary in that case but for forms a little bit more complex, for example when you need to handle a polymorphic object like type Animal = Cat | Dog we've got another class for that which is NgxSubFormRemapComponent but you can read the README if you need more info.
Hope it helps you scale your forms!
Edit:
If you want to go further, I've just published a blog post to explain a lot of things about forms and ngx-sub-form here https://dev.to/maxime1992/building-scalable-robust-and-type-safe-forms-with-angular-3nf9
I did a similar application. The problem is that you are creating all your inputs at the same time, which is not likely scalable.
In my case, I did a FormManagerService who manages an array of FormGroup. Each step has a FormGroup that is initialized once in the execution on the ngOnInit of the step component by sending his FormGroup config to the FormManagerService. Something like that:
stepsForm: Array<FormGroup> = [];
getFormGroup(id:number, config: Object): FormGroup {
let formGroup: FormGroup;
if(this.stepsForm[id]){
formGroup = this.stepsForm[id];
} else {
formGroup = this.createForm(config); // call function to create FormGroup
this.stepsForm[id] = formGroup;
}
return formGroup;
}
You'll need an id to know which FormGroup corresponds to the step. But after that, you'll be able to split your Forms config in each step (so small config files that are easier for maintenance than a huge file). It will minimize the initial load time since the FormGroups are only create when needed.
Finally before submitting, you just need to map your FormGroup array and validate if they're all valid. Just make sure all the steps has been visited (otherwise some FormGroup won't be created).
This may not be the best solution but it was a good fit in my project since I'm forcing the user to follow my steps.
Give me your feedback. :)
Is it really necessary to keep the form controls in the service? Why not just leave the service as the keeper of data, and have the form controls in the components? You could use the CanDeactivate guard to prevent the user from navigating away from a component with invalid data.
https://angular.io/docs/ts/latest/api/router/index/CanDeactivate-interface.html

Using objects as options in Autoform

In my Stacks schema i have a dimensions property defined as such:
dimensions: {
type: [String],
autoform: {
options: function() {
return Dimensions.find().map(function(d) {
return { label: d.name, value: d._id };
});
}
}
}
This works really well, and using Mongol I'm able to see that an attempt to insert data through the form worked well (in this case I chose two dimensions to insert)
However what I really what is data that stores the actual dimension object rather than it's key. Something like this:
[
To try to achieve this I changed type:[String] to type:[DimensionSchema] and value: d._id to value: d. The thinking here that I'm telling the form that I am expecting an object and am now returning the object itself.
However when I run this I get the following error in my console.
Meteor does not currently support objects other than ObjectID as ids
Poking around a little bit and changing type:[DimensionSchema] to type: DimensionSchema I see some new errors in the console (presumably they get buried when the type is an array
So it appears that autoform is trying to take the value I want stored in the database and trying to use that as an id. Any thoughts on the best way to do this?.
For reference here is my DimensionSchema
export const DimensionSchema = new SimpleSchema({
name: {
type: String,
label: "Name"
},
value: {
type: Number,
decimal: true,
label: "Value",
min: 0
},
tol: {
type: Number,
decimal: true,
label: "Tolerance"
},
author: {
type: String,
label: "Author",
autoValue: function() {
return this.userId
},
autoform: {
type: "hidden"
}
},
createdAt: {
type: Date,
label: "Created At",
autoValue: function() {
return new Date()
},
autoform: {
type: "hidden"
}
}
})
According to my experience and aldeed himself in this issue, autoform is not very friendly to fields that are arrays of objects.
I would generally advise against embedding this data in such a way. It makes the data more difficult to maintain in case a dimension document is modified in the future.
alternatives
You can use a package like publish-composite to create a reactive-join in a publication, while only embedding the _ids in the stack documents.
You can use something like the PeerDB package to do the de-normalization for you, which will also update nested documents for you. Take into account that it comes with a learning curve.
Manually code the specific forms that cannot be easily created with AutoForm. This gives you maximum control and sometimes it is easier than all of the tinkering.
if you insist on using AutoForm
While it may be possible to create a custom input type (via AutoForm.addInputType()), I would not recommend it. It would require you to create a template and modify the data in its valueOut method and it would not be very easy to generate edit forms.
Since this is a specific use case, I believe that the best approach is to use a slightly modified schema and handle the data in a Meteor method.
Define a schema with an array of strings:
export const StacksSchemaSubset = new SimpleSchema({
desc: {
type: String
},
...
dimensions: {
type: [String],
autoform: {
options: function() {
return Dimensions.find().map(function(d) {
return { label: d.name, value: d._id };
});
}
}
}
});
Then, render a quickForm, specifying a schema and a method:
<template name="StacksForm">
{{> quickForm
schema=reducedSchema
id="createStack"
type="method"
meteormethod="createStack"
omitFields="createdAt"
}}
</template>
And define the appropriate helper to deliver the schema:
Template.StacksForm.helpers({
reducedSchema() {
return StacksSchemaSubset;
}
});
And on the server, define the method and mutate the data before inserting.
Meteor.methods({
createStack(data) {
// validate data
const dims = Dimensions.find({_id: {$in: data.dimensions}}).fetch(); // specify fields if needed
data.dimensions = dims;
Stacks.insert(data);
}
});
The only thing i can advise at this moment (if the values doesnt support object type), is to convert object into string(i.e. serialized string) and set that as the value for "dimensions" key (instead of object) and save that into DB.
And while getting back from db, just unserialize that value (string) into object again.

Posting limited fields with ExtJS 4.2.x REST on store update

We are trying to use ExtJS grid/forms and bind them with Store to perform REST operations.
Now as I was playing with extjs examples for restful api, I came across http://docs.sencha.com/extjs/4.2.1/#!/example/restful/restful.html and tried editing a model to add a new field 'phone' in the list like below:
Ext.define('Person', {
extend: 'Ext.data.Model',
fields: [{
name: 'id',
type: 'int',
useNull: true
}, 'email', 'first', 'last','phone'],
validations: [{
type: 'length',
field: 'email',
min: 1
}, {
type: 'length',
field: 'first',
min: 1
}, {
type: 'length',
field: 'last',
min: 1
}]
});
As you can see "phone" is a new field added in fields list of a model and after adding that field while I was trying to perform any of the rest operation (PUT/POST) it was posting that field along with rest of the visible fields in grid. This is something I have not expected.
Is there anyway by which I can just post dirty fields (which are modified) and not all those exist in model with default store/rest manipulation way that ExtJS has provided?
In the proxy writer definition you would want to use the writeAllFields param which will work for updates. New model instances will send all fields.
http://docs.sencha.com/extjs/4.2.1/#!/api/Ext.data.writer.Writer-cfg-writeAllFields