SharePoint Formulare mit React Formik 1/2

Mit der Version 1.15 vom SharePoint Framework können wir Formulare anpassen. Mit React Formix geht das noch einfacher.

Das ist ein zweiteiliges Tutorial. In diesem Teil beschreibe ich, wie man generell einen Form-Customizer aufsetzen kann und wie man ein sehr einfaches Formular erstellt. Im zweiten Teil erkläre ich, wie man auch komplizierte Formulare mittels dieser Technologie und mittels React Formik umsetzen kann.

SharePoint und Formulare

Ein häufiger Anpassungwunsch an SharePoint ist die Anpassung der Standard-Listenformulare. Normalerweise kommt man hier mit dem Standard-Listenformatting schon zu beeindruckenden Formularen, aber das stößt auch manchmal an seine Grenzen.

Dann hatte ich bisher nur die Möglichkeit, dieses Formular per PowerApps zu customizen. Das geht sehr gut und einfach - hat aber ein paar Nachteile. Zum einen kann ich das Formular nicht so einfach transportieren und an eine andere Liste mit denselben Feldern hängen. Das macht ein Szenario mit einem Test- und einem Produktivsystem schwierig, ganz zu schweigen von dem Fall, dass ich mehrere hundert gleiche Listen habe, die die gleichen Formulare haben sollen.

Mit der Version 1.15 vom SharePoint Framework können wir nun "Form Customizer" erstellen und Formulare in Javascript entwickeln.

Erstellen einer Liste

Erstmal brauchen wir eine Liste, auf der wir den Form-Customizer entwickeln wollen Erstellt zunächst auf einer beliebigen SharePoint Site die Liste "FormCustomizer". Für diesen Teil des Tutorials braucht sie keine weiteren Spalten, wir fügen aber im nächsten Teil neue Spalten mit hinzu. Legt dort aber ein Element an.

Ich nehme im folgenden an, dass eine solche Liste unter der URL "https://.sharepoint.com/sites//Lists/FormCustomizer/" vorhanden ist.

Erstellen eines Form-Customizers

Öffnet zuerst eine Powershell und führt die folgenden Befehle aus, um ein leeres SharePoint Framework Projekt zu erstellen.

md react-formix-form-customizer
cd react-formix-form-customizer

npx -p yo -p @microsoft/generator-sharepoint yo @microsoft/sharepoint
# oder falls ihr @microsoft/sharepoint-spfx und yeoman lokal installiert habt
# yo @microsoft/sharepoint

Beantwortet die nun folgenden Fragen so:

  • What is your solution name?: react-formix-form-customizer
  • Which type of client-side component to create?: Form Customizer
  • Which type of client-side extension to create? Formix Form Customizer
  • What is your Form Customizer name? FormixCustomizer
  • Which template would you like to use?: React

Führt danach noch das hier aus, um zusätzliche Bibliotheken zu installieren, die wir noch brauchen werden

npm install @pnp/sp --save

Nachdem der Installationsprozess abgeschlossen ist, müssen wir noch zwei Einstellungen machen, damit die die Erweiterung testen können.

Öffnet zuerst die Datei config/serve.json und passt dort alle "serveConfigurations" an. Ersetzt dort alle vier Einträge von "https://{tenantDomain}" durch "https://.sharepoint.com/sites/" und vier Einträge von "/sites/mySite/Lists/MyList" durch "/sites//Lists/FormCustomizer"

// in serve.json
//...
   {
     "formixCustomizer_EditForm": {
      "pageUrl": "https://<tenant>.sharepoint.com/sites/MyAppPage/_layouts/15/SPListForm.aspx",
      "formCustomizer": {
        "componentId": "431c36f6-3901-469f-8af2-3077d6cd39e7",
        "PageType": 8,
        "RootFolder": "/sites/<MeineSeite>/Lists/FormCustomizer",
        "ID": 1,
        "properties": {
          "sampleText": "Value"
        }
      }
    }
}
//...

Das sind die verschiedenen Konfigurationen unserer Test-Einstellungen für die "New", "Edit" und "Display" Varianten unseres Formulars. In dem Property "ID" gebe ich an, welches ListenElement ich konkret in meinem Formular laden möchte. Unsere Liste hat nur ein Element, deswegen wähle ich hier die "1".

Ihr könnt hier aber auch die Adressen von einer existierenden Liste einpflegen und das Formular sogar gegen eine "produktive" Liste entwickeln. Ändert dafür einfach die Listen-Url und die ID des zu bearbeitenden Dokumentes.

Danach füge ich gerne noch in der package.json Datei neue Scripte hinzu, über die ich den SPFx Development server einfach starten kann

// in serve.json
//...
{
  "scripts": {
    "serve:newform": "gulp serve --config=formixCustomizer_NewForm",
    "serve:editform": "gulp serve --config=formixCustomizer_EditForm",
    "serve:viewform": "gulp serve --config=formixCustomizer_NewForm"
  },
}  
//...

Nun kann ich das Projekt als "NewForm" schnell mittels des folgenden Kommandos starten

npm run serve:newform

Mein Browser öffnet sich, ich bestätige einmal das Popup zum akzeptieren der Debug Skripte und dann ich sehe den konfigurieren Form-Customizer.

Ihr könnt euch den generierten Code für dieses Beispiel hier ansehen und herunterladen.

Anpassen des Customizers

Beziehungsweise genaugenommen sehe ich nun nichts, nur einen leeren Bildschirm. Das liegt daran, dass der Form-Customizer im Moment noch nicht mehr kann. Sehen wir uns mal den Code an.

In der Datei "src\extensions\formixCustomizer" finden wir die Haupteinstiegspunkt des Projektes in der Datei FormixCustomizerFormCustomizer.ts.

Das Laden und Speichern der Formulardaten finden wir nicht. Das liegt daran, dass wir diese Funktionalitäten selber entwickeln und dem Form-Customizer hinzufügen müssen. Dafür haben wir in einem oberen Schritt schonmal die PnPjs Bibliothek installiert und müssen Sie nun konfigurieren.

Zunächst öffnen wir die Datei FormixCustomizerFormCustomizer.ts fügen den Import der nötigen Bibliotheken mit hinzu

import { spfi, SPFI, SPFx } from "@pnp/sp";
import "@pnp/sp/webs";
import "@pnp/sp/lists";
import "@pnp/sp/items";

Danach fügen wir in der Klasse "FormixCustomizerFormCustomizer" oben zwei neue Klassenvariablen mit hinzu. Die eine ist die Instanz für die PnPjs Bibliothek und die andere ist das Listenitem, welches wir geladen haben.

   private _spfi: SPFI;
   private _item: any={};

Nun passen wir die "onInit" Methode an. Diese wird beim Start des Customizers ausgeführt. In ihr initiieren wir nun das PnPjs Objekt und laden danach direkt das Listenelement. Letzteres machen wir aber nur, wenn wir eine ListenID haben und damit auf einem Display- oder Edit-Formular sind. Auf einem "New" Formular haben wir offensichtlich kein Element, welches wir beim Start laden müssen.

    public onInit(): Promise<void> {
    // Setup PnPjs
    this._spfi = spfi().using(SPFx( this.context ));

    if (this.context.itemId!==undefined){
      // itemId is set on an Edit and Displayform
      // if it is set load data from SharePoint
      return this._spfi
      .web
      .lists
      .getById(this.context.list.guid.toString())
      .items
      .getById(this.context.itemId)()
      .then((item:any)=>{ 
        // The following fields need to be removed from the item, if we want to save the object again
        delete item["odata.editLink"];
        delete item["odata.etag"];
        delete item["odata.id"];
        delete item["odata.metadata"];
        delete item["odata.type"];     
        delete item.odata;
        this._item=item;
        console.log(item);
      })
    }  
    
    return Promise.resolve();
  }

Aus dem geladenen Objekt entfernen wir ein paar Eigenschaften wie etwa "odata.editLink". Diese Eigenschaften würden sonst stören, wenn wir dasselbe Objekt wieder speichern werden.

Jetzt passen wir noch die "_onSave" Methode an. Zum einen ändern wir die Signatur, so dass wir der Methode ein Objekt übergeben können, welches wir speichern wollen. Zum anderen prüfen wir nun, ob wir auf dem "Edit" oder "New" Formular sind und rufen danach die relevanten Methoden aus dem "PnPjs" Framework auf. Wichtig ist hierbei, dass nach dem erfolgreichen Speichern immer die Methode "this.formSaved()" aufgerufen wird. Diese sorgt dafür, dass sich das Formular beendet.

    private _onSave = (item:any): void => {

    // If we are in Edit Mode: Update the existing item
    if (this.displayMode === FormDisplayMode.Edit  && this.context.itemId!==undefined) {
      this._spfi
      .web
      .lists
      .getById(this.context.list.guid.toString())
      .items
      .getById(this.context.itemId)
      .update(item)
      .then(()=>{
        this.formSaved();
      })
    };

     // If we are in New Mode: Create a new Item
    if (this.displayMode === FormDisplayMode.New) {
      this._spfi
      .web
      .lists
      .getById(this.context.list.guid.toString())
      .items
      .add(item)
      .then(()=>{
        this.formSaved();
      })
    }   
  }

Schließlich passen wir noch die "render()" Methode an und übergeben dem neu erstellten React Element zusätzlich noch das geladene Item als Parameter mit

   public render(): void {
    const formixCustomizer: React.ReactElement<{}> =
      React.createElement(FormixCustomizer, {
        item:this._item, // <-NEW
        context: this.context,
        displayMode: this.displayMode,
        onSave: this._onSave,
        onClose: this._onClose
       } as IFormixCustomizerProps);

    ReactDOM.render(formixCustomizer, this.domElement);
  }

Damit haben wir alle notwendigen Änderungen an der Datei FormixCustomizerFormCustomizer.ts durchgeführt. Ihr werden in eurer IDE aber wahrscheinlich noch Fehler haben.

Das liegt neben ein paar weiteren Anpassungen am Code noch daran, dass euer Typescript-Compiler sich über ein paar benutze "any" Ausdrücke und nicht abgefangene Promises beschwert. Denkt daran, dass das hier nur ein Tutorial ist und ich die entsprechende Behandlung dieser not-so-best Practices der Einfachheit halber hier nicht durchführe.

Um die Warnungen zu entfernen, öffne ich die ".eslintrc.js" Datei und setze die Werte für die beiden Checks auf 0. Macht das aber nicht so in einem echten Projekt.

...
  '@typescript-eslint/no-explicit-any': 0,
  '@typescript-eslint/no-floating-promises': 0,
...

Auf der anderen Seite müssen wir noch in der Datei components/FormixCustomizer.tsx das Interface "IFormixCustomizerProps" so anpassen, dass wir das geladene Item mit an die React-Komponente übergeben können.

Öffnet nun die Datei components/FormixCustomizer.tsx ..... Und löscht allen Inhalt aus ihr. Wir erstellen sie von Grund auf neu.

Fügt zuerst neue Importe hinzu:

import * as React from 'react';
import { FormDisplayMode } from '@microsoft/sp-core-library';
import { FormCustomizerContext } from '@microsoft/sp-listview-extensibility';
import { PrimaryButton,DefaultButton } from "office-ui-fabric-react/lib/Button";
import { TextField } from "office-ui-fabric-react";

Wir laden hier neben React Objekte, die wir später brauchen, um zwischen Edit- und Display-Modus zu unterscheiden. Des weiterein laden wir noch Buttons und das TextFeld aus der UI-Fabric Bibliothek.

Danach fügen wir das Interface für die Properties hinzu, die wir unserer React-Komponente "FormixCustomizer" übergeben können wollen.

export interface IFormixCustomizerProps {
  context: FormCustomizerContext;
  displayMode: FormDisplayMode;  
  onSave: (item:any) => void;
  onClose: () => void;
  item:any; //<-- New
}

Das ist praktisch identisch mit dem generierten Interface, wir haben hier nur das Attribut "item" mit hinzugefügt. Darin transportieren wir das vom Webpart geladene Element zur Komponente.

Dann erstellen wir die Funktionale React Komponente "FormixCustomizer" und exportieren sie als "Standard-Export"

export const FormixCustomizer=(props:IFormixCustomizerProps)=>{

  return <div>
   <TextField
        name="Title"    
        label="Title"
      />   
    <PrimaryButton text="Save"/>
    <DefaultButton text="Cancel" />
  </div>
}
export default FormixCustomizer;

Dieses Formular hat nun alle Elemente, die wir verwenden wollen, aber noch keine Logik. Wir fügen daher als erstes einen "onClick"-Handler an den Cancel-Button an, der die "onClose" Methode des Webpart aufruft. Damit können wir das Formular schonmal schließen, wenn wir wollen.

...
 <DefaultButton text="Cancel" onClick={
        ()=>{
          props.onClose()
        }
    }/>
...    

Das Textfeld hat aber noch keine Daten und wir können seine Daten noch nicht auslesen. Dazu fügen wir nun mittels der "React.useState" Methode einen dynamischen Status in der Komponente ein und verbinden diesen mit dem Textfeld.

 ...
  const [text,setText]=React.useState(props.item.Title)
  return <div>
    <TextField
        name="Title"
        value={text}
        label="Title"
        onChange={(e,value) => {
          setText(value);
        }}
      />   
...      

In der Variablen "text" ist nun immer der Wert des Textfeldes gespeichert. Jetzt müssen wir nur noch dazu sorgen, dass beim Klick auf den "Speichern"-Knopf das geladene Element angepasst und danach gespeichert wird.

 ...
     <PrimaryButton text="Save" onClick={
        ()=>{
          props.item.Title=text;
          props.onSave(props.item)
        }
    }/>
...      

Damit sieht unsere gesamte Komponente so aus:

export const FormixCustomizer=(props:IFormixCustomizerProps)=>{
  const [text,setText]=React.useState(props.item.Title)
  return <div>
    <TextField
        name="Title"
        value={text}
        label="Title"
        onChange={(e,value) => {
          setText(value);
        }}
      />   
    <PrimaryButton text="Save" onClick={
        ()=>{
          props.item.Title=text;
          props.onSave(props.item)
        }
    }/>
     <DefaultButton text="Cancel" onClick={
        ()=>{
          props.onClose()
        }
    }/>
  </div>
}

Ihr könnt euch den gesamten Code für dieses Beispiel hier ansehen und herunterladen.

Wenn wir mehr Felder in dem Formular bearbeiten wollen, dann müssen wir nur mehr dynamische Status-Variablen über mehr Aufrufe von "React.useState" und diese dann mit neuen Controls verknüpfen.

Bei dem letzten Satz ist euch vielleicht aufgefallen, dass dieses Vorgehen zwar einfach ist, aber natürlich auf Dauer mit einer Wachsenden Parameteranzahl zu sehr, sehr vielen Variablen und einer sehr unübersichtlichen Komponente führen wird.

Wir man das mit React Formik sehr einfach halten kann, erzähle ich euch im nächsten Artikel diese Serie.

Hat dir das gefallen? Vielleicht magst du auch...

Die versteckte SharePoint Benutzerinformationsliste

In einer versteckten Liste speichert SharePoint Informationen über Benutzer

SharePoint Json List Formatting und die nicht existierende WEEKDAY Funktion

Es gibt keine Funktion beim JSON List Formatting, mit der sich der Wochentag berechnen lässt. Man kann ihn aber selbst berechnen.

Quick Tip: Eine Communication Site als Subsite anlegen

Man kann über die UI keine Communication Site als Subsite anlegen. Per Powershell geht es aber problemlos.