Home
NexoPOS

Add Create Button on search-select Field

A "search-select" is a component that helps to filter a list of options and ease the selection. By default, the component doesn't allow creating new entries, that are later added to the list of options. With a few configurations, you can add support for creating entries via a popup and on successful submission, have it added to the options.

This feature works out of the box with internal crud and settings components without any further modification of the code. This means that if you have, for example, a crud component that allows selecting products, you just have to define the support on the search-select field, that's all. However, if you want to have deeper control or a better understanding of how things are done, you might proceed with reading this.

Working Principle

One important thing to note is that your implementations must load fields asynchronously. This ensures that the fields can be reloaded anytime without reloading the page (that's how the crud component fields are loaded on NexoPOS).

Now, when you define your field structures, you'll define it on the backend side. From there, you'll set which component it should load when the "+" button is clicked and the configurations that are passed as properties (it's a Vue 3 component).

Field Configuration

All crud components provide their configuration as an array using the method getFormConfig. When we create our field, we must provide two extra entries: "component" and "props". The component can be any component you want.

If you want to use your component, you must register it first. Furthermore, that component resolves the response from a server (which is useful to select the entry that was created automatically). However, in our example, we'll use "nsCrudForm, " a component built on NexoPOS to render a crud form.

Here is how you'll then define the configuration of your search-select field.

<?php
use App\Crud\ProductCategoryCrud;

$fields = [
    [
        'type' => 'search-select',
        'name' => 'category_id',
        'label' => 'Assigned Category',
        'description' => 'any description you want goes here',
        //Here is how to enable support for a create button
        'component' => 'nsCrudForm',
        'props' => ProductCategoryCrud::getFormConfig()
    ]
];

You'll note that we use the ProductCategoryCrud class to retrieve its configuration. From NexoPOS 5, that method is provided to all available CRUD components. The variable $fields must then be returned as a response on an API call.

Settings Up The Vue Component

Suppose you're not using our nsSettings or nsCrudForm component. In that case, you'll have to provide a communication bridge between your Vue component that renders the form, the search-select field, and the component used to create an entry. As you might guess, we have 3 Vue components in this scenario. We'll start with the component that renders the form.

<template>
  <div>
    <form>
      <ns-field v-for="field in fields"/>
    </form>
  </div>
</template>

<script lang="ts">
declare const FormValidation;
export default {
  data() {
    return {
      fields: [],
      formValidation: new FormValidation,
    };
  },
  methods; {
    loadFields() {
      nsHttpClient('/api/path/to/the/fields')
        .subscribe({
          next: fields => {
            this.fields = this.formValidataion.createForm(fields);
          },
          error: error => {
            // handle error here
          });
    }
  }
  created() {
    this.loadFields();
  },
};
</script>

As mentioned above, we're loading fields asynchronously. Now, to make sure to populate the options, every time the component that creates the entry resolves the response from the server, we need to add a callback to the <ns-field> element like this:

<template>
  <div>
    <form>
      <ns-field @saved="handleSaveEvent( $event, field )" v-for="field in fields"/>
    </form>
  </div>
</template>

<script lang="ts">
declare const FormValidation;
export default {
  data() {
    return {
      fields: [],
      formValidation: new FormValidation,
    };
  },
  methods: {
    async handleSaveEvent( serverResponse, field ) {
      try {
        field.options.push({
            label: serverResponse.data.entry[ field.props.optionAttributes.label ],
            value: serverResponse.data.entry[ field.props.optionAttributes.value ]
        });
        field.value     =   serverResponse.data.entry[ field.props.optionAttributes.value ];
      } catch ( exception ) {
        // something went wrong
      }
      
    },
    loadFields() {
      return new Promise( ( resolve, reject ) => {
        nsHttpClient('/api/path/to/the/fields')
          .subscribe({
            next: fields => {
              resolve( fields );
              this.fields = this.formValidataion.createForm(fields);
            },
            error: error => {
              reject( error )
            });
      });
    }
  }
  created() {
    this.loadFields();
  },
};
</script>

We're using field.props to get the actual configuration of the Crud component that is being loaded. On that object, we have "optionsAttributes" which define what property should be considered as a label or what should be used as a value. Note that those two properties are required to be used as an option. By default, NexoPOS assumes you're using "id" as a value and "name" as a label. You can change that behavior by editing the "optionsAttributes" on the crud component.

If you're not using nsCrudForm to create an entry, we'll show you how to create your custom component that will be displayed on a pop-up. When a component is opened as a pop-up, NexoPOS provides as an additional prop the variable "popup", this can then be used to detect whether it's displaying on a pop-up or not. Here is how we'll define our component:

<template>
  <div>
    <form>
      <ns-field v-for="field in fields"/>
      <button @click="createEntry()">Save</button>
    </form>
  </div>
</template>

<script lang="ts">
declare const FormValidation;
export default {
  props: [ 'popup' ],
  data() {
    return {
      fields: [],
      formValidation: new FormValidation,
    };
  },
  methods: {
    createEntry() {
      if ( this.formValidation.validateFields( this.fields ) ) {
          const form = this.formValidation.extractFields( this.fields );
          nsHttpClient.post( 'api/to/create/entry', form )
            .subscribe({
                next: response => {
                    this.popup.params.resolve( response );
                },
                error: error => {
                    this.popup.params.reject( error );
                }
            });
         return; // stop it here
      }
      
      // form is not valid
    },
    loadFields() {
      nsHttpClient('/api/path/to/the/fields')
        .subscribe({
          next: fields => {
            this.fields = this.formValidataion.createForm(fields);
          },
          error: error => {
            reject( error )
          });
    }
  }
  created() {
    this.loadFields();
  },
};
</script>

Using the property "popup" we have access to various params provided on our component. Two of those params are "resolve" and "reject". We make sure of both to return the response of the server.