@vendasta/vform

Vendasta's Angular vForm

Usage no npm install needed!

<script type="module">
  import vendastaVform from 'https://cdn.skypack.dev/@vendasta/vform';
</script>

README

VForm Angular

import { VFormModule } from '@vendasta/vform';

What is this?

This is an Angular component and service which allows us to convert existing VForm-backed Knockout frontends to Angular.

The core functionality of the form generation is handled mostly in the VFormStoreService, while the translation to and from the VForm backend form definitions and data is handled by the VFormTranslationService. Fortunately, most developers won't have to worry about these.

How do I use this?

Converting an existing VForm-backed form:

Converting an existing VForm is as simple as including the VFormModule from @vendasta/vform in your module, then using the VFormComponent and passing in a FormOptions, like so:

<v-form [formOptions]="formOptions"></v-form>

Generally this will be placed inside a <mat-card>, but you can put it anywhere. VFormComponent is a fluid in that it fills the width of its container.

FormOptions

FormOptions is structured like this:

| Property | Type | Required | Notes |--|:--:|:--:|--| | formUrl | string | true | The VForm backend URL, sans any query params. Params will be added automatically according to the action and the context. | leftCustomButtons | ButtonOptions[] | false | Defaults to a cancel and a submit button with default actions. According to Material guidelines, this should be a maximum of 2 buttons. | rightCustomButtons | ButtonOptions[] | false | Defaults to [] (no buttons). These will be displayed in an overflow menu on the right side of the form. According to Material guidelines, any actions beyond the 2 left buttons should go in here. | cancelUrl | string | false | ButtonAction.CANCEL redirects to this URL. | successUrl | string | false | ButtonAction.SUBMIT redirects to this URL on a successful submit if this property is set. | context | string | false | Should be encoded via encodeURIComponent, defaults to the form page URL.

Context

The context property is the context that gets passed to the VForm backend. For some forms, this may include necessary context such as AGID and PID. If you are passing in a context, ensure that it has been encoded using the Javascript built-in function encodeURIComponent.

As you can see, buttons are a list of ButtonOptions interfaces.

ButtonOptions

ButtonOptions is structured like this:

| Property | Type | Required | Notes | |--|:--:|:--:|--| | label | string | true | The button's display text. | action | ButtonAction or Function | true | Can be passed ButtonAction.SUBMIT, ButtonAction.CLEAR, ButtonAction.CANCEL, the default supported actions. Alternatively, can be passed a custom function to run instead of a default action. | color | string | false | One of 'primary', 'accent', or 'warn'. Will make this button a coloured material raised button.

Custom input width

The VForm component takes an inputWidth input to change the width of the inputs. This should be passed a string with either pixel/em size or a percentage. For example, <v-form [formOptions]="formOptions" [inputWidth]="85%"></v-form>.

Example FormOptions

Simple:

this.formOptions = {formUrl: '/form/v1/the-vform-url/'}

Robust:

this.formOptions = {
  formUrl: '/form/v1/the-vform-url/',
  leftCustomButtons: [
    {label: 'Create Admin', action: ButtonAction.SUBMIT, color: 'primary'},
    {label: 'Cancel', action: ButtonAction.CANCEL},
  ],
  rightCustomButtons: [
    {label: 'Clear', action: ButtonAction.CLEAR},
    {label: 'Custom Button Action', action: () => {
      console.log('💩');
    }},
  ],
  successUrl: '/success-url/',
  cancelUrl: '/cancel-url/'
}

VForm Outputs

The v-form tag provides a few outputs you can/must use depending on the situation. If you don't provide a successUrl in your form options, it is assumed that you will be using one of the form response related outputs to redirect your user or do something else after the form is submitted. Here's a table of the outputs:

| Output | Type | Notes | |--|:--:|--| | formSubmitSuccess | boolean | When the form is submitted and it gets a successful response from the vform api, this output will emit true | | formSubmitResult | FormResult | Same situation as formSubmitSuccess, except this output will emit the response from the vform api. FormResult is documented below this table. | | hasError | boolean | Will emit true when there is an error in the form. | | errorMessage | string | Will contain the relevant error message if hasError emitted true. | | formLoadSuccess | FormState | Emits a FormState every time the state changes. If you want to know if the form is successfully loaded, check the formReady attribute on FormState (documented below). |

// Vapi/Vform responses come in this format.
export interface FormResult {
  // The specified version of the api, e.g. "1.0"
  version: string;
  // Data returned from the endpoint. Can be basically anything, but strings and form errors are common.
  data: any | string | FormErrorResponse;
  // Request id that was generated to serve this request. Can be used to find log messages.
  requestId: string;
  // How long it took the api to return.
  responseTime: number;
  // Status code of the response, e.g. 200, 400, 500, etc.
  statusCode: number;
}

export interface FormState {
  formGroup: FormGroup;
  formReady: boolean;
  sections: Section[];
  isLoading: boolean;
  isSubmitting: boolean;
  formSubmitUrl: string;
  onSubmitSuccess: boolean;
}

Creating a new Angular form without a VForm backend

This is not currently supported, but may be in a future release.

I need help!

Contact Wisakejak.

Contributing

Not all form components have been created. To add custom components to VForm Angular:

  1. First, add the control type into the ControlType enum in vform.models.ts. This should be identical to the control name returned from the vForm backend, but in upper case.

  2. In vform.component.html, add the following to the ngSwitch:

    <ng-container *ngSwitchCase="controlType.YOURCONTROLTYPE">
      <your-control [placeholder]="field.label" [required]="field.required" [control]="f.controls[field.name]"></your-control>
    </ng-container>
    

    You can put anything inside the ng-container, but keep the following in mind:

    • Stick to Material guidelines as much as possible
    • The control should take Angular's FormControl as an input so that the VFormStoreService can control its state.
    • If possible, try to use Angular Material components for the new control.
  3. Set the field value in the setFieldData method in fields/field-base.ts according to the data type of the field.

  4. If your new control needs custom behaviour when the form is altered, add this behaviour to the formAlteringElementChanged method in vform.store.service.ts.

  5. Update CHANGELOG.md and README.md.

  6. Merge dat boi to master! after PR of course

Currently supported controls

The following fields are currently supported:

  • Text/textbox
  • Password
  • Textarea
  • Multitext
  • Toggle/switch
  • Radio button group
  • Checkbox
  • Dropdown
  • CountryState
  • PhotoUpload
  • Multiselect (select2 support coming)
  • Hidden
  • Text-Separator
  • Checkboxes
  • Vsource
  • Tag

Developing

Developing vform locally can be easy. Add these paths to your tsconfig.app.json:

{ 
    "compilerOptions": {
        "paths": {
            "@vendasta/vform": ["../../frontend/angular/projects/vform/src/lib/"],
            "@vendasta/vform/*": ["../../frontend/angular/projects/vform/src/lib/*"],
            "@vendasta/*": ["../node_modules/@vendasta/*"],
            "@agm/core": ["../node_modules/@agm/core"],
            "@agm/core/*": ["../node_modules/@agm/core/*"],
            "@ngx-translate": ["../node_modules/@ngx-translate"],
            "@ngx-translate/*": ["../node_modules/@ngx-translate/*"],
            "ngx-markdown": ["../node_modules/ngx-markdown"],
            "ngx-markdown/*": ["../node_modules/ngx-markdown/*"],
            "@angular/*": ["../node_modules/@angular/*"],
            "core-js": ["../node_modules/core-js"],
            "core-js/*": ["../node_modules/core-js/*"],
            "rxjs": ["../node_modules/rxjs/"],
            "rxjs/*": ["../node_modules/rxjs/*"],
            "zone.js": ["../node_modules/zone.js/"],
            "zone.js/*": ["../node_modules/zone.js/*"]
        }
    }
}

This pathing assumes you have the frontend repo and the project you're interested in cloned in the same folder. If you don't, just have the @vendasta/vform pathing go to wherever your frontend repo is located. The pathing on everything else assumes that your tsconfig.app.json is a folder in from your node_modules. Fix this as required!

Once these paths are in place, you should be able to make changes to vform in frontend and have your dev server reload when there are changes.

Vfilter

A close cousin of Vform is Vfilter, which is used to create views of filterable data. Consider a list of salespeople statistics that you'd like to filter and search on. Vfilter makes that easier. Here's an example of a Vfilter in action:

<!-- handleFeedData($event) simply does a this.feedData = [...$event]; to force that to update -->
<v-filter formUrl="/your-form-url" (feedData)="handleFeedData($event)">
    <div *ngFor="let data of feedData" class="human">
        <div class="human__first-name">{{ data.firstName }}</div>
        <div class="human__last-name">{{ data.lastName }}</div>
    </div>
</v-filter>

As you can see, VFilter relies on content projection to allow the VFilter implementer to display their data their way. Rather than building multiple different use-cases for displaying data into VFilter we can pass the decision of how to render the data back to the implementer. This is how the original Vfilter could work as well, we're just making that the only way this one works!

v-filter inputs

| Input | Type | Required | Notes | |--|:--:|--|--| | formUrl | string | true | The form (filter) url that you are interested in. Should point to a derivative of a vform.VFilterHandler. | | context | string \| object | false | If you need to pass a context other than the current page url, pass one here. | | ignoredQueryParams | string[] | false | If you need to ignore certain query parameters because they interfere with your form you can use this input to do so. An example is marketId in the Manage Salespeople handler of Partner Center. We don't want to use that query parameter to update our form so we mark it as ignored for that form. | | infiniteScroll | boolean | false | Whether or not you want the form to update via infinite scroll or not. |

v-filter outputs

| Output | Type | Required | Notes | |--|:--:|--|--| | feedData | any | true | This output must be used by the implementor to show the feed data. VFilter doesn't show any feed data on its own. This output will always emit the full set of data that should be shown, so there's no need to aggregate this data on the implementing side. E.g. in the case of an infinite scrolling handler, there could be the temptation to always add whatever data comes from this output onto what you already have. Don't do that! Just use exactly what this output provides and render that in the outlet. | | formSelectionChanges | any | false | This will be emitted whenever the form fields change. |