2.12.23-alpha • Published 10 months ago

@ampath-kenya/ngx-openmrs-formentry v2.12.23-alpha

Weekly downloads
1
License
ISC
Repository
-
Last release
10 months ago

AMPATH POC Formentry

Ampath forms is a forms engine that is inspired and built to work with OpenMRS and its encounter/obs model. That being said it tries not to assume that it will be used with an OpenMRS context and does not take responsibility for fetching any dynamic data from OpenMRS that responsibility should be handled by the consuming application by providing data sources to the engine

The form engine is built on top Angular forms and provides extensions to support our specific data collection requirements. A good place to start for you to understand Ampath forms is the Angular tutorial on building dynamic forms (We highly recommend that you work through the tutorial)

Main Concepts

Question Model

The question model is lifted from the Angular tutorial and adapted to support our form schema.

Datasources

The concept of datasources is an attempt at eliminating need for the library to know about the OpenMRS backend for cases where you need to ;

  • Resolve uuid to labels by hitting an OpenMRS endpoint (Mostly used for values which provide concept uuids and allows us fetch the label from a remote endpoint)

  • Fetch options for a select drop down by searching via rest (Used for concepts and drugs)

  • Upload documents and relate them to an encounter(Basically allows us to upload images and then set the url as an obs value which may not be ideal)

The engine does not care about where the data sources get their data, only that they return observables which the engine can subscribe to for the data it needs. Which means you can provide dummy observables to the engine and it will happily consume them (That is how the example consumer app in the repository works without having an OpenMRS backend)

Development

Build the library by running:

$ npm run build:lib

Then run the demo with:

$ cd dist/ngx-formentry $ npm link $ cd ../ $ npm link ngx-formentry $ npm start

Usage

app.component.ts

import { Component, OnInit } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { FormGroup } from '@angular/forms';

import { Subscriber } from 'rxjs';
import { Observable, Subject, of } from 'rxjs';

import {
  QuestionFactory,
  Form,
  FormFactory,
  ObsValueAdapter,
  OrderValueAdapter,
  EncounterAdapter,
  DataSources,
  FormErrorsService,
  EncounterPdfViewerService
} from '../../dist/ngx-formentry';
import { MockObs } from './mock/mock-obs';

const adultForm = require('./adult-1.4.json');
const adultFormObs = require('./mock/obs.json');
const formOrdersPayload = require('./mock/orders.json');
@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  data: any;
  schema: any;
  sections: {} = {};
  formGroup: FormGroup;
  activeTab = 0;
  form: Form;
  stack = [];
  encounterObject = adultFormObs;
  showingEncounterViewer = false;
  public header = 'UMD Demo';

  constructor(
    private questionFactory: QuestionFactory,
    private formFactory: FormFactory,
    private obsValueAdapater: ObsValueAdapter,
    private orderAdaptor: OrderValueAdapter,
    private encAdapter: EncounterAdapter,
    private dataSources: DataSources,
    private encounterPdfViewerService: EncounterPdfViewerService,
    private formErrorsService: FormErrorsService,
    private http: HttpClient
  ) {
    this.schema = adultForm;
  }

  ngOnInit() {
    this.dataSources.registerDataSource('drug', {
      searchOptions: this.sampleSearch,
      resolveSelectedValue: this.sampleResolve
    });
    this.dataSources.registerDataSource('personAttribute', {
      searchOptions: this.sampleSearch,
      resolveSelectedValue: this.sampleResolve
    });
    this.dataSources.registerDataSource('problem', {
      searchOptions: this.sampleSearch,
      resolveSelectedValue: this.sampleResolve
    });
    this.dataSources.registerDataSource('location', {
      searchOptions: this.sampleSearch,
      resolveSelectedValue: this.sampleResolve
    });
    this.dataSources.registerDataSource('provider', {
      searchOptions: this.sampleSearch,
      resolveSelectedValue: this.sampleResolve
    });

    const ds = {
      dataSourceOptions: { concept: undefined },
      searchOptions: (text?: string) => {
        if (ds.dataSourceOptions && ds.dataSourceOptions.concept) {
          const items: Array<any> = [
            { id: 1, text: 'Stage 1 Symptom' },
            { id: 2, text: 'Stage 2 Symptom' }
          ];
          return Observable.create((observer: Subject<any>) => {
            setTimeout(() => {
              observer.next(items);
            }, 1000);
          });
        }
      },

      resolveSelectedValue: (key: string) => {
        if (ds.dataSourceOptions && ds.dataSourceOptions.concept) {
          const item = { id: 1, text: 'Stage 1 Symptom' };
          return Observable.create((observer: Subject<any>) => {
            setTimeout(() => {
              observer.next(item);
            }, 1000);
          });
        }
      }
    };

    this.dataSources.registerDataSource('conceptAnswers', ds);

    const obs = new MockObs();
    this.dataSources.registerDataSource('rawPrevEnc', obs.getObs());

    this.dataSources.registerDataSource('patient', { sex: 'M' }, true);

    this.dataSources.registerDataSource('patientInfo', {
      name: 'Test Patient',
      age: '37',
      birthdate: '7/7/1982',
      mui: '447062073-5',
      nid: '1234567'
    });

    this.dataSources.registerDataSource('file', {
      fileUpload: (data) => {
        return of({ image: 'https://unsplash.it/1040/720' });
      },
      fetchFile: (url) => {
        return new Observable((observer: Subscriber<any>) => {
          let objectUrl: string = null;
          const headers = new HttpHeaders({
            Accept: 'image/png,image/jpeg,image/gif,application/pdf'
          });
          this.http
            .get('https://unsplash.it/1040/720', {
              headers,
              responseType: 'json'
            })
            .subscribe((res: any) => {
              const blob = new Blob(res.body);
              objectUrl = URL.createObjectURL(blob);
              observer.next(objectUrl);
            });

          return () => {
            if (objectUrl) {
              URL.revokeObjectURL(objectUrl);
              objectUrl = null;
            }
          };
        });
      }
    });

    // Create form
    this.createForm();

    // Set encounter, obs, orders
    adultFormObs.orders = formOrdersPayload.orders;
    this.encAdapter.populateForm(this.form, adultFormObs);

    this.setUpCascadeSelectForWHOStaging();

    // Alternative is to set individually for obs and orders as show below
    // // Set obs
    // this.obsValueAdapater.populateForm(this.form, adultFormObs.obs);

    // // Set orders
    // this.orderAdaptor.populateForm(this.form, formOrdersPayload);
  }

  public setUpCascadeSelectForWHOStaging() {
    const subject = new Subject();
    const source = this.dataSources.dataSources['conceptAnswers'];
    source.dataFromSourceChanged = subject.asObservable();

    const whoStageQuestion = this.form.searchNodeByQuestionId(
      'adultWHOStage'
    )[0];
    if (whoStageQuestion) {
      whoStageQuestion.control.valueChanges.subscribe((val) => {
        if (source.dataFromSourceChanged) {
          if (val === 'a89b2606-1350-11df-a1f1-0026b9348838') {
            subject.next([
              { value: 3, label: 'Stage 3 Symptom' },
              { value: 4, label: 'Stage 4 Symptom' }
            ]);
          } else {
            subject.next([
              { value: 5, label: 'Stage 5 Symptom' },
              { value: 6, label: 'Stage 6 Symptom' }
            ]);
          }
        }
      });
    }
  }

  public getSectionData(sectionId) {
    let data = {};
    data = this.sections[sectionId];
    return data;
  }

  public clickTab(tabNumber) {
    this.activeTab = tabNumber;
  }

  public createForm() {
    this.form = this.formFactory.createForm(
      this.schema,
      this.dataSources.dataSources
    );
  }

  public sampleResolve(): Observable<any> {
    const item = { value: '1', label: 'Art3mis' };
    return Observable.create((observer: Subject<any>) => {
      setTimeout(() => {
        observer.next(item);
      }, 1000);
    });
  }

  public sampleSearch(): Observable<any> {
    const items: Array<any> = [
      { value: '0', label: 'Aech' },
      { value: '5b6e58ea-1359-11df-a1f1-0026b9348838', label: 'Art3mis' },
      { value: '2', label: 'Daito' },
      { value: '3', label: 'Parzival' },
      { value: '4', label: 'Shoto' }
    ];

    return Observable.create((observer: Subject<any>) => {
      setTimeout(() => {
        observer.next(items);
      }, 1000);
    });
  }

  public onSubmit($event) {
    $event.preventDefault();

    // Set valueProcessingInfo
    this.form.valueProcessingInfo = {
      patientUuid: 'patientUuid',
      visitUuid: 'visitUuid',
      encounterTypeUuid: 'encounterTypeUuid',
      formUuid: 'formUuid',
      encounterUuid: 'encounterUuid',
      providerUuid: 'providerUuid',
      utcOffset: '+0300'
    };

    if (this.form.valid) {
      this.form.showErrors = false;
      const payload = this.encAdapter.generateFormPayload(this.form);

      // Alternative is to populate for each as shown below
      // // generate obs payload
      // let payload = this.obsValueAdapater.generateFormPayload(this.form);

      // // generate orders payload
      // let ordersPayload = this.orderAdaptor.generateFormPayload(this.form);
    } else {
      this.form.showErrors = true;
      this.form.markInvalidControls(this.form.rootNode);
    }
  }

}

app.component.html

<div *ngIf="form && form.rootNode">

	<form [formGroup]="form.rootNode.control">

		<form-renderer (onAction)="actionClicked($event)" [node]="form.rootNode"></form-renderer>

</form>

</div>

See src/app/adult-1.4.json for and src/app/mock/obs.json for sample encounter payload

To publish:

Update the version in both of the package.json files

$ git add -f dist

$ git commit -m 'Bump <Version>'

$ git tag <Version>

Reset branch so you don't commit the dist to the src repository

$ git reset HEAD~1 --hard

$ git checkout <version tag>

$ npm login

$ npm publish
2.12.23-alpha

10 months ago

2.12.23

12 months ago

2.12.22

12 months ago

2.12.21

1 year ago

2.12.19

1 year ago

2.12.18

1 year ago

2.12.20

1 year ago

2.12.17

2 years ago

2.12.16

2 years ago

2.12.15

2 years ago

2.12.14

2 years ago

3.0.0-alpha9

3 years ago

2.12.13

3 years ago

2.12.11

3 years ago

3.0.0-alpha8

3 years ago

2.12.10

3 years ago

3.0.0-alpha6

3 years ago

3.0.0-alpha5

4 years ago

3.0.0-alpha4

4 years ago

3.0.0-alpha3

4 years ago

3.0.0-alpha2

4 years ago

3.0.0-alpha1

4 years ago