Home
NexoPOS

Custom Tab Component

NexoPOS provides a quick way to create a tabbed form with custom fields, but it's also possible to render a custom Vue component instead of tab content. In this scenario, you control how the fields are displayed, giving you greater flexibility to create a unique layout for your CRUD component.

To make your component work seamlessly with NexoPOS, you need to provide validation details for your custom tab component. Remember that the custom component should behave like a tab with fields. This will ensure that when the form is being submitted, it's checked for data safety.

Register Custom View Component

The first step is to register your component on the global component objects: nsExtraComponents. Refer to the documentation on injecting a Vue component.

Requirements

For the custom components to work, there are a few things that need to be provided and taken into consideration:

  • the component ns-crud-form.vue provides custom props to your component named "tab". This is the definition of the form as made in the CRUD PHP class (check the getForm method).
  • Your data must include an array of errors with an empty value as defined on your view component's data() method.
  • When the component is mounted, you should define whether it's valid or not. You'll then emit an event "invalid" with the array of errors.
  • using the "tab" props you can access the error stored on your tab component and display it (These are the errors emitted, but as it's not recommended to mutate props, we proceed with the event emittion).

Concrete Example

Here is how your custom component tab should look.

export default {
    props: [ 'tab' ],
    data() {
        return {
            errors: []
        }
    },
    template: `
    <div v-if="tab.instance.errors.length > 0" class="mb-2">
        <ns-notice v-for="error of tab.instance.errors">
            <template v-slot:title>{{ __m( 'Warning', 'YourModule' ) }}</template>
            <template v-slot:description>{{ error.message }}</template>
        </ns-notice>
    </div>
    `,
    mounted() {
        this.checkValidity();
    },
    methods: {
        checkValidity() {
            this.errors.push({
                identifier: 'invalid_email', // identifier of the error, this must be unique
                message: 'You must provide a valid email',
                name: 'email', // name of the field               
            });
          
            this.$emit( 'invalid', this.errors );
        }
    }
}

Emit Form Data To Parent Component

Now that the user has interacted with your custom tab component, you might want to send their data to the ns-crud-form component so it can send it to the server. The better approach here is to emit a "changed" event with the fields (an array of fields with a specific definition, we'll share that later). We'll do it as the ns-crud-form component is unable to require form data on demand. Your component is then responsible for providing this.

export default {
    props: [ 'tab' ],
    data() {
        return {
            errors: [],
            fields: [],
            validation: new FormValidation
        }
    },
    template: `
    <div>
      <div v-if="tab.instance.errors.length > 0" class="mb-2">
          <ns-notice v-for="error of tab.instance.errors">
              <template v-slot:title>{{ __m( 'Warning', 'YourModule' ) }}</template>
              <template v-slot:description>{{ error.message }}</template>
          </ns-notice>
      </div>
      <ns-field field="field" v-for="field of fields"/>
    </div>
    `,
    mounted() {
        // ...
    },
    watch: {
        // we carefully check fields change and emit event to the parent component
        fields: {
            deep: true,
            handle() {
                if ( this.validation.validateFields( this.fields ) ) {
                    this.$emit( 'changed', this.fields );
                }
            }
        }
    },
    methods: {
        // we're loading fields from our endpoint
        loadCustomFields() {
            nsHttpClient.get( '/api/mymodule/fields' ).subscribe({
                next: fields => {
                    this.fields   =   this.validation.createFields( fields );
                })
        },
    }
}

Let's explain what's happening here:

  1. We'll load fields from our endpoint (but it can be hardcoded as you want);
  2. We'll build the fields using the FormValidation class which is globally available on the dashboard.
  3. We'll watch changes to our fields and emit changed events to the parent component so it can merge your field data with the whole form. But do it if it's valid.

Field Structure

If you don't want to use the ns-field component but instead a custom element you created, you still need to follow the field structure if you want your form data to be submitted correctly. The form data of your component should be provided as an object of fields. Here is the structure of a field:

{
    type: 'text',
    name: 'email',
    description: 'field description',
    validation: 'required', 
    value: 'field value.'
}

The most important attributes here are "name" and "value".

Restoring State On Tab Change

A crud form usually consists of multiple tabs. At the current state, if you switch from one tab to another, your custom component will lose it's state. This is mainly because the component is mounted and unmounted on tab change, and then loses its state.

When you emit a "changed" with your component fields, these are made available on a property (tab) injected into your component.

export default {
    props: [ 'tab' ],
    data() {
        return {
            errors: [],
            fields: [],
            state: {},
            validation: new FormValidation
        }
    },
    template: `
    <div>
      <div v-if="tab.instance.errors.length > 0" class="mb-2">
          <ns-notice v-for="error of tab.instance.errors">
              <template v-slot:title>{{ __m( 'Warning', 'YourModule' ) }}</template>
              <template v-slot:description>{{ error.message }}</template>
          </ns-notice>
      </div>
      <ns-field field="field" v-for="field of fields"/>
    </div>
    `,
    mounted() {
        // if the tab has received our state
        if ( this.tab.fields.length > 0 ) {
            // in case you emit a "changed" event with only one field 
            // you can get the value of that field
            this.state =  this.tab.fields[0].value;
        }
    },
    watch: {
        // we carefully check fields change and emit event to the parent component
        fields: {
            deep: true,
            handle() {
                if ( this.validation.validateFields( this.fields ) ) {
                    this.$emit( 'changed', this.fields );
                }
            }
        }
    },
    methods: {
        // we're loading fields from our endpoint
        loadCustomFields() {
            nsHttpClient.get( '/api/mymodule/fields' ).subscribe({
                next: fields => {
                    this.fields   =   this.validation.createFields( fields );
                })
        },
    }
}