npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

ngx-power-forms

v0.0.5

Published

A library designed to abstract away the repetitive elements of building a form. Takes away a small amount of the flexibility provided by Angular forms, but saves you time in the process. The library is built on top of Angular reactive forms and makes exte

Downloads

4

Readme

Ngx Power Forms

A library designed to abstract away the repetitive elements of building a form. Takes away a small amount of the flexibility provided by Angular forms, but saves you time in the process. The library is built on top of Angular reactive forms and makes extensive use of observables, to allow for onPush change detection.

Demo is available here.

Installation and setup

To install the library, use

npm i ngx-power-forms

Once installed, you should add the NgxPowerFormsModule using forRoot to a core module. Within forRoot you can provide an object to further customise things such as form validation messages, custom components and miscellanious settings.

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    NgxPowerFormsModule.forRoot({
      customErrors: {
        required: () => 'Please ensure you fill in this field'
      },
      customOptions: {
        showRequiredSymbol: false
      }
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

To access the styles for the project, you should install the stylesheet in to your angular.json file.

"architect": {
  "build": {
    "options": {
      "styles": [
        "node_modules/ngx-power-forms/src/lib/styles/index.scss",
      ]
    }
  }
}

Creating a form

To create your first form, you can use the FormFactory service.

Below you can find an extensive example of a range of inputs that are available using the service.

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent implements OnInit {
  public form: FormContainer;
  public fields: Record<string, FormInputField>;

  constructor(private formFactory: FormFactory){}

  ngOnInit(): void {
    this.form = this.formFactory.create({
      fields: [
        {
          name: 'textField',
          label: 'Text field',
          type: FormInputType.TEXT,
          value: 'Starting value',
          placeholder: 'Placeholder text',
          validators: [Validators.required],
          disabled: true
        },
        {
          name: 'selectField',
          label: 'Select field with placeholder',
          type: FormInputType.SELECT,
          placeholder: 'Select your option',
          value: 1,
          customConfig: {
            options: [
              { label: 'Option 1', value: 1 },
              { label: 'Option 2', value: 2 }
            ]
          }
        },
        {
          name: 'selectReactiveField',
          label: 'Reactive select field',
          type: FormInputType.SELECT,
          customConfig: {
            options: of([
              { label: 'Option 1', value: 1 },
              { label: 'Option 2', value: 2 }
            ])
          }
        },
        {
          name: 'numberField',
          label: 'Number field',
          type: FormInputType.NUMBER,
          validators: [Validators.min(5)]
        },
        {
          name: 'checkboxField',
          label: 'Checkbox field',
          type: FormInputType.CHECKBOX,
          customConfig: {}
        },
        {
          name: 'radioField',
          label: 'Radio field',
          type: FormInputType.RADIO,
          customConfig: {
            options: [
              { label: 'Option 1', value: 1 },
              { label: 'Option 2', value: 2 }
            ]
          }
        },
        {
          name: 'textareaField',
          label: 'Text area field',
          type: FormInputType.TEXTAREA,
          validators: [Validators.minLength(20), Validators.maxLength(100)]
        },
        {
          name: 'multiSelectField',
          label: 'Multi select field',
          type: FormInputType.MULTI_SELECT,
          placeholder: 'Select your items',
          value: [1],
          customConfig: {
            options: [
              { label: 'Option 1', value: 1 },
              { label: 'Option 2', value: 2 }
            ]
          }
        },
        {
          name: 'dateField',
          label: 'Date field',
          type: FormInputType.DATETIME
        },
                {
          name: 'customSelect',
          label: 'Custom select',
          type: FormInputType.CUSTOM_SELECT,
          value: 1,
          customConfig: {
            options: [{ label: 'Hello', value: 1 }, { label: 'Yo!', value: 2 }],
            component: CustomSelectOptionComponent
          }
        },
        {
          name: 'customMultiSelect',
          label: 'Custom multi select',
          type: FormInputType.CUSTOM_MULTI_SELECT,
          value: [],
          customConfig: {
            options: [
              { label: 'Option 1', value: 1 },
              { label: 'Option 2', value: 2 },
              { label: 'Option 3', value: 3 }
            ]
          }
        },
        {
          name: 'resultTextField',
          label: 'This is disabled when the previous checkbox is not checked',
          type: FormInputType.TEXT,
          hooks: {
            onInit: (field) => {
              const decidingCheckbox = this.form.getField('checkboxField');

              if (decidingCheckbox) {
                decidingCheckbox.value$.pipe(startWith(decidingCheckbox.value)).subscribe((val) => {
                  field.setDisabled(!val);
                });
              }
            }
          }
        }
      ],
      onSave: (formValue) => console.log(formValue)
    });

    this.fields = this.form.getFieldsObject();
  }
}

To then use these fields in your template, it looks as simple as this:

<form [formGroup]="form.formGroup" (ngSubmit)="form.save()">
  <pow-form-input [formInputField]="fields.textField"></pow-form-input>
  <pow-form-input [formInputField]="fields.dateField"></pow-form-input>
  <pow-form-input-select [formInputField]="fields.selectField"></pow-form-input-select>
  <pow-form-input-select [formInputField]="fields.selectReactiveField"></pow-form-input-select>
  <pow-form-input-multi-select [formInputField]="fields.multiSelectField"></pow-form-input-multi-select>
  <pow-form-input-number [formInputField]="fields.numberField"></pow-form-input-number>
  <pow-form-input-checkbox [formInputField]="fields.checkboxField"></pow-form-input-checkbox>
  <pow-form-input-radio-set [formInputField]="fields.radioField"></pow-form-input-radio-set>
  <pow-form-input-textarea [formInputField]="fields.textareaField"></pow-form-input-textarea>
</form>

Alternatively, if you don't need anything in between the form inputs, you can use pow-form-input-item to loop through your fields in the order that they were added to the array.

<form [formGroup]="form.formGroup" (ngSubmit)="form.save()">
  <pow-form-input-item *ngFor="let field of form.fields" [formInputField]="field"></pow-form-input-item>
</form>

To simplify this even more, we've also added a pow-form component, where you simply pass in the formContainer instance as an input and then embed your action buttons inside the element.

If one of your buttons is of type 'submit', the submission of the form will be automatic and the function that to passed to the onSave field of the config object will be called.

<pow-form [formContainer]="form">
  <input type="submit" value="Submit form">
</pow-form>

Typings & interfaces

Typings vary between each form input type. By assigning a value to the type field using the FormInputType enum, TypeScript is able to determine the intellisense needed, for example requiring options in the customConfig for select and radio fields.

| Field | Description | |----------------- |------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | name | The key used for the form group. When getting the value of a containing form, this is the key that the value will be assigned to. | | label | The label that will be displayed with your form input. | | type | The type of the form input. Used to determine which component to display when using a dynamic form, or passed straight in to the input type value (e.g. input[type=email]). Can be configured with custom types. | | placeholder | Where applicable, the placeholder text to display. | | disabled | Whether the field is disabled on initialisation | | value | The starting value of the field. | | validators | Array of validators - the same format as passing it in to a FormControl. | | asyncValidators | Array of async validators - same as passing in to a FormControl. | | customConfig | Varies from input to input. For example, select and radio fields require a field defining options in the custom config object. | | hooks | Hooks to be ran in sync with the Angular lifecycle hooks of the component generated. Can pass in an onInit hook to configure things like a disabled state of the field depending on the value of other inputs. |

Customisability

As well as being able to override the classes yourself, the project extensively uses CSS variables. This aims to make the forms as customisable as possible without having to manually override CSS classes, with !important sprinkled everywhere.

  /* Core */
  --form-input-feature-color: dodgerblue;
  --form-input-error-color: red;

  /* Form input label */
  --form-input-label-font-size: 14px;
  --form-input-label-margin-bottom: 8px;
  --form-input-label-font-weight: 500;
  --form-input-label-letter-spacing: 0.5px;
  --form-input-label-color: grey;

  /* Form input label error state */
  --form-input-label-color--error: var(--form-input-error-color);

  /* Form input field */
  --form-input-background-color: white;
  --form-input-vertical-padding: 12px;
  --form-input-horizontal-padding: 12px;
  --form-input-padding: var(--form-input-vertical-padding) var(--form-input-horizontal-padding);
  --form-input-border-color: grey;
  --form-input-border: 1px solid var(--form-input-border-color);
  --form-input-border-radius: 5px;
  --form-input-font-size: 16px;
  --form-input-font-color: black;
  --form-input-placeholder-color: grey;

  /* Form input field disabled states */
  --form-input-border-style--disabled: dashed;
  --form-input-opacity--disabled: 0.5;

  /* Form input field focus states */
  --form-input-outline-width--focus: 1px;
  --form-input-outline-color--focus: var(--form-input-feature-color);

  /* Form input field error states */
  --form-input-border-color--error: var(--form-input-error-color);
  --form-input-outline-color--error: var(--form-input-error-color);

  /* Form input error text */
  --form-input-error-font-weight: 400;
  --form-input-error-font-size: 14px;
  --form-input-error-spacing-top: 4px;

  /* Form input host */
  --form-input-spacing: 24px;

  /* -- Custom fields -- */

  /* Select */
  --form-input-select-icon-color: var(--form-input-border-color);
  --form-input-select-icon-spacing: var(--form-input-horizontal-padding);
  
  /* Checkbox */
  --form-input-checkbox-size: 24px;
  --form-input-checkbox-spacing: 10px;
  --form-input-checkbox-label-font-size: var(--form-input-label-font-size);
  --form-input-checkbox-check-color: var(--form-input-feature-color);
  --form-input-checkbox-inner-size: 12px;
  --form-input-checkbox-label-font-size: 12px;
  --form-input-checkbox-inner-radius: 3px;
  
  /* Radio */
  --form-input-radio-size: 22px;
  --form-input-radio-selected-size: 11px;
  --form-input-radio-spacing: 10px;
  --form-input-radio-label-spacing: 10px;
  --form-input-radio-border: 1px solid var(--form-input-border-color);
  --form-input-radio-label-font-size: var(--form-input-checkbox-label-font-size);
  --form-input-radio-check-color: var(--form-input-checkbox-check-color);

  /* Textarea */
  --form-input-textarea-line-height: 1.4;

  /* Multi select */
  --form-input-multi-select-option-background-color: var(--form-input-feature-color);
  --form-input-multi-select-option-text-color: white;
  --form-input-multi-select-option-font-size: 14px;
  --form-input-multi-select-option-vertical-padding: 9px;
  --form-input-multi-select-option-horizontal-padding: 8px;
  --form-input-multi-select-option-padding: var(--form-input-multi-select-option-vertical-padding) var(--form-input-multi-select-option-horizontal-padding);
  --form-input-multi-select-option-radius: var(--form-input-border-radius);
  --form-input-multi-select-dropdown-border: 1px solid #ddd;
  --form-input-multi-select-focus-background: #{rgba(black, 0.05)};
  --form-input-multi-select-dropdown-font-size: 16px;
  --form-input-multi-select-dropdown-option-padding: 12px;

The form styles have been designed specifically to try and reduce the amount of customisation that you will have to do. They have also been created with accessibility in mind. You can overwrite the variables by using a :root tag in your global stylesheet.

Each form input container will by default have 3 CSS classes assigned to it, form-input-host, form-input-host-${formInputType}, and form-input-host-${formInputName} to allow for easy external styling across the whole app. An example of this is the --form-input-spacing variable which dictates the default spacing between form input components when displayed next to each other.

Custom option components

Some components, such as FormInputCustomSelect and FormInputCustomMultiSelect can have a component passed in to the custom config when creating the input. This component must extend CustomSelectOptionComponent, and then you are free to create a new template and styles to make the custom select option appear however you want.

Create a clickable element that calls the onSelect function in the callback, and use the isSelected input to style your custom component when it has been selected.

If you want to customise the container of the custom select options, simply target form-input-custom-select, or form-input-custom-multi-select respectively.

Creating custom form components

To integrate your own component in to the framework, there are a few things that you need to set up. The first thing is to extend the libraries types to allow you to define a custom config interface for your component.

For this example, we will be adding our own 'range' component.

It is recommended to create a new directory, whether it be in your shared folder or as a new module, named forms.

Extending the types

First, lets create a new enum to hold our new form input type.

export enum FormInputCustomType {
  RANGE = 'range'
};

Then, create a new interface file. We've called ours form-input-range-config.interface.ts.

In this file, we want to create two interfaces.

export interface FormInputRangeConfig extends FormInputBaseConfig {
  type: FormInputCustomType.RANGE;
  customConfig: FormInputRangeCustomConfig;
  value: number;
}

export interface FormInputRangeCustomConfig {
  min: number;
  max: number;
  step: number;
}

The first interface defines the object that we will need to pass in to the arguments to our form input creation method. In here you should extend FormInputBaseConfig, and override the type with the value of the enum that we have just created. We've also overriden the value field, as we want to make it require a number.

Finally, if our new component will have any extra config that is not part of a standard input, we want to create a new interface below it and assign that interface to the customConfig field on the primary config interface.

For this example we will require a min, max and step fields in order to configure our slider.

Finally, we need to integrate this interface with the library. Create a new types file in this directory called power-forms.d.ts, and paste the following in to the file.

declare module 'ngx-power-forms' {
  interface FormInputConfigMap {
    [FormInputCustomType.RANGE]: FormInputRangeConfig;
  }
}

Once this is done, when you are creating a new input using the FormInputFactory or FormFactory, if you try to create an input with a type of FormInputCustomField.RANGE, the intellisense will match the interface to the one you previously created, and typescript will make sure that you provide the correct parameters.

Creating the component

To create a custom component that can be integrated in to the framework, first generate an Angular component using the CLI, and extend this component from FormInputComponent. You should pass your customConfig interface to the extended FormInputComponent as a generic, if your component has a customConfig object.

@Component({
  selector: 'app-form-input-range',
  templateUrl: './form-input-range.component.html',
  styleUrls: ['./form-input-range.component.scss'],
  changeDetection: ChangeDetectionStrategy.OnPush
})
export class FormInputRangeComponent extends FormInputComponent<FormInputRangeCustomConfig> {}

You are free to build the template however you wish, however it is recommended to try to stick to the following structure for consistency. The example below is an implementation of our custom built slider.

<div class="form-input-container">
  <pow-form-input-label [formInputField]="formInputField"></pow-form-input-label>

  <input class="form-input-range"
    type="range"
    [name]="formInputField.name"
    [id]="formInputField.name"
    [formControl]="formInputField.control"
    [min]="formInputField.customConfig.min"
    [max]="formInputField.customConfig.max"
    [step]="formInputField.customConfig.step">

  <pow-form-input-error [errors]="formInputField.errors$ | async"></pow-form-input-error>
</div>

As you can see in this example, we have referenced fields inside customConfig, and the Angular language service knows that min, max and step are all a part of the customConfig object for this component because of the generic that we passed through to the base class.

If your component follows the normal style of an input (i.e. looks like a text box), you should apply the form-input class to the input. This will ensure that the styles are consistent with the rest of the inputs. In this case however, the slider field does not follow the usual structure, so we have omitted that CSS class.

Integrating with the dynamic form

Finally, if we want our custom component to be able to be integrated with the pow-form or pow-form-input-item components, we need to pass our component in to our library using the forRoot method on the library module.

@NgModule({
  declarations: [
    AppComponent,
    FormInputRangeComponent
  ],
  imports: [
    BrowserModule,
    NgxPowerFormsModule.forRoot({
      customInputComponents: {
        [FormInputCustomType.RANGE]: FormInputRangeComponent
      }
    })
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }