koltyakov/sp-rest-proxy

Full SPAddIn example

PSchicht opened this issue · 4 comments

Hey,

I'm in a project where we host an angular 5 application in SharePoint 2013 using an SharePoint hosted Add-In. I have tried to use sp-rest-proxy with the add-in, but no luck. Can you please provide a step by step guide on how to use sp-rest-proxy with hosted Add-In?

Thanks!

Hey @PSchicht,

Thanks for the interest!
Have you checked this blog post how it can be used in Angular?

What exactly does your Add-In request?
Host data using _api/SP.AppContextSite(@target) or data from the Add-In itself?

Are you targeting the proxy to Add-In's SPWeb?

I can take a look and provide some walkthrough, yet can't name any ETA.

Hey @koltyakov ,

yes i have checked the post, for me it's not really clear on many points. I'm stuck with the installation. I followed installation instructions (https://github.com/koltyakov/sp-rest-proxy) and created the server.js file, run the npm run serve command and followed the instructions. However, when i try to open the http://localhost:8080/ url, i receive the following error message: Error: ENOENT: no such file or directory, open 'C:_Dev[...]\static\index.html'

What am I doing wrong?

Many thx for your help!

Hey @koltyakov,

I got it to work. I had to cover some new topics and stuff. I documented the steps, so I want to share them to help others :)

Here are the steps that got me a fully functioning SPHostedAddIn with all the Angular 5 / Rest-Proxy good stuff:

I used the "SharePoint Hosted Add-In with Angular 4" to get me started with Angular / SP-Hosted Add-In development a while back. (https://ramirezmery.wordpress.com/2017/08/16/sharepoint-hosted-add-in-with-Angular-4/). The basic creation and integration of the SharePoint project are based on this.

  • First create a new SPHosted Add-In Project somewhere (ex. c:\Dev\RestProxyTest).
  • Create a new developer site on your SharePoint and point the Add-In project to the newly created site.
  • Deploy it once and make sure that it compiles and deploys successfully and that you can open it after it has been deployed.
  • Make sure that you have installed node.js / npm / Angular CLI (i used node lts 8.9.4 / npm 5.6.0 / Angular cli 1.6.8).
  • Open a new node.js console and navigate to the Add-In solution folder (Not the project folder!).
  • Create a new Angular app (ex. ng new RestProxyTest --prefix rpt --style scss (you can adjust the prefix and style parameters, it's just my preference)). Make sure that you use the same name for the Add-In and the Angular App, that way all the Angular stuff is created in your project folder. If you choose a different name, you need to copy all files from the Angular app folder to the project folder manually.
  • In Visual Studio, remove the SharePoint modules: Scripts, Images, and Content.
  • Add a new SharePoint module: app.
  • Add a new folder "src" in the app module.
  • Copy the Default.aspx file from the Pages module to the app module and delete the Sample.txt file from the app module.
  • Delete the Pages module.
  • Remove the JQuery nuget module
  • include the files: tsconfig.json, package.json and the src folder into your project
  • Important: Always make sure that the Deployment Type property of all Angular related files (ex. typescript and json files) are set to "NoDeployment"!
    • Change deployment type of all added files
  • Open the Default.aspx file and remove the JQuery, App.css and App.js references
  • Replace the content of the PlaceHolderMain placeholder with the tag of your anguar application
<rpt-root>Loading...</rpt-root>
  • open the .Angular-cli.json file and change the "outDir" to "app/src" (this way the bundle files will be created directly in your app module directory)
  • build your application (ng build) and check if the bundles have been created in your app/src directory
  • Add the files from app/src folder to your project (At this point, there are also map files and stuff. For debug builds I included them)
  • Open the Default.aspx file and add the following references below you Angular app:
<rpt-root>Loading...</rpt-root>
<script src="../app/src/inline.bundle.js"></script>
<script src="../app/src/polyfills.bundle.js"></script>
<script src="../app/src/styles.bundle.js"></script>
<script src="../app/src/vendor.bundle.js"></script>
<script src="../app/src/main.bundle.js"></script>
  • Deploy your app to SharePoint and make sure that you see the usual new Angular app html stuff.
  • Open the package.json file and change the start script to: ng serve --delete-output-path false --open (this way you can use npm start without deleting the files from your app/src folder).

At this point, you should have a working Angular 5 Application. There are still some edges but for the most part it should be a good start. Now the HMR part:

Follow the instructions to include HMR to your project: https://github.com/Angular/Angular-cli/wiki/stories-configure-hmr

  • Create a new environment file environment.hmr.ts with contents (also remember to set the DeploymentType for the file!):
export const environment = {
	 production: false,
	 hmr: true
};
  • Update the existing environment.prod.ts and environment.ts files to include the hmr flag.
  • Open the Angular-cli.json file and add the hmr to the environment objects:
"environmentSource": "environments/environment.ts",
"environments": {
  "dev": "environments/environment.ts",
  "hmr": "environments/environment.hmr.ts",
  "prod": "environments/environment.prod.ts"
}
  • add the hmr dev dependency: npm install --save-dev @Angularclass/hmr
  • create a new file src/hmr.ts with the following content (also remember to set the DeploymentType for the file!):
import { NgModuleRef, ApplicationRef } from '@Angular/core';
import { createNewHosts } from '@Angularclass/hmr';

export const hmrBootstrap = (module: any, bootstrap: () => Promise<NgModuleRef<any>>) => {
  let ngModule: NgModuleRef<any>;
  module.hot.accept();
  bootstrap().then(mod => ngModule = mod);
  module.hot.dispose(() => {
    const appRef: ApplicationRef = ngModule.injector.get(ApplicationRef);
    const elements = appRef.components.map(c => c.location.nativeElement);
    const makeVisible = createNewHosts(elements);
    ngModule.destroy();
    makeVisible();
  });
};
  • Replace the content of the main.ts file with this:
import { enableProdMode } from '@Angular/core';
import { platformBrowserDynamic } from '@Angular/platform-browser-dynamic';

import { AppModule } from './app/app.module';
import { environment } from './environments/environment';

import { hmrBootstrap } from './hmr';

if (environment.production) {
  enableProdMode();
}

const bootstrap = () => platformBrowserDynamic().bootstrapModule(AppModule);

if (environment.hmr) {
  if (module[ 'hot' ]) {
    hmrBootstrap(module, bootstrap);
  } else {
    console.error('HMR is not enabled for webpack-dev-server!');
    console.log('Are you using the --hmr flag for ng serve?');
  }
} else {
  bootstrap();
}
  • run ng serve --hmr -e=hmr to check if it works

Now we can include and configure SP-REST-PROY:

  • Install sp-rest-proy: npm install sp-rest-proxy --save-dev
  • Create server.js in the projects folder with the following code (also remember to set the DeploymentType for the file!):
const RestProxy = require('sp-rest-proxy');

const settings = {
    configPath: './config/private.json', // Location for SharePoint instance mapping and credentials
    port: 8080,                          // Local server port
    staticRoot: './app/src'               // Root folder for static content in the app modules src folder
};

const restProxy = new RestProxy(settings);
restProxy.serve();
  • Add npm task "serve" for serve into package.json:
"scripts": {
    "serve": "node ./server.js",
    ...
}
  • run ng build to make sure that your app/src files are present (normal ng serve commands will delete the output dir)
  • Run npm run serve
    • Enter the SharePoint URL to the developer site you created for the Add-In
    • Choose NTLM (Just my preference and current configuration)
    • Provide your username and password (ex. domain\user ...)
    • The REST-Proxy should now have been started on http://localhost:8080
    • open the url, you should now see your Angular app running on http://localhost:8080

At this point the basic configuration is done. Now you can take the battle notes from here to refine your configuration.

I added the proxy.conf.json file and changed my package.json to include at least the ""hmr": "ng serve --hmr --environment=hmr --verbose --proxy=proxy.conf.json"," script.

To complete the application, I added a new service and created a method to query some SharePoint data

  • Create new service: ng g s shared/spRest (also remember to set the DeploymentType for the file!)
  • Replace the content of the sp-rest.service.ts file:
import { Injectable } from '@angular/core';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { environment } from '../../environments/environment';

@Injectable()
export class SpRestService {

  SPLocalUrl: string = 'http://localhost:4200';

  SPHostUrl: string = '';
  SPAppWebUrl: string = '';

  constructor(private http: HttpClient) {

    if (!environment.hmr) {
      this.SPHostUrl = this.getQueryStringParameter('SPHostUrl');
      this.SPAppWebUrl = this.getQueryStringParameter('SPAppWebUrl');
    }
  }

  async executeTestQuery() {

    let webServiceUrl: string = '';
    let header: HttpHeaders;

    if (environment.hmr) {
      webServiceUrl = this.SPLocalUrl + "/_api/web/lists";
      header = new HttpHeaders({ 'Content-Type': 'application/json;odata=verbose', 'Accept': 'application/json;odata=verbose' });
    }
    else {
      webServiceUrl = this.SPAppWebUrl + "/_api/SP.AppContextSite(@target)/web/lists?@target='" + this.SPHostUrl + "'";
      let digest: string = (<HTMLInputElement>document.getElementById('__REQUESTDIGEST')).value;
      header = new HttpHeaders({ 'Content-Type': 'application/json;odata=verbose', 'Accept': 'application/json;odata=verbose', 'X-RequestDigest': digest });
    }

    let result = await this.executeRestQuery(webServiceUrl, header);
    return result;
  }

  private executeRestQuery(webServiceUrl: string, header: HttpHeaders) {
    
    return new Promise((resolve, reject) => {
      let results: any = [];
      this.http.get(webServiceUrl, { headers: header, withCredentials: true }).subscribe(
        data => {
          results = (data as any).d.results;
        },
        err => {
          reject('Something went wrong!');
        },
        () => {
          resolve(results);
        }
      );
    });
  }

  private getQueryStringParameter(queryStringParameter): string {
    var params =
      document.URL.split("?")[1].split("&");
    var strParams = "";
    for (var i = 0; i < params.length; i = i + 1) {
      var singleParam = params[i].split("=");
      if (singleParam[0] == queryStringParameter)
        return decodeURIComponent(singleParam[1].replace(/\+/g, ' '))
    }
  }
}
  • Open the app.module.ts file and replace the content:
import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { AppComponent } from './app.component';
import { HttpClientModule } from '@angular/common/http';
import { SpRestService } from './shared/sp-rest.service';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    HttpClientModule
  ],
  providers: [
    SpRestService
  ],
  bootstrap: [AppComponent]
})
export class AppModule { }
  • Open the app.component.html file and insert the following HTML:
<ul>
  <li *ngFor="let item of queryResult">{{ item.Title }}</li>
</ul>
  • Open the app.component.ts file and replace the content:
import { Component } from '@angular/core';
import { SpRestService } from './shared/sp-rest.service';

@Component({
  selector: 'rpt-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.scss']
})
export class AppComponent {
  title = 'rpt';

  queryResult: any = [];

  constructor(private spRestService: SpRestService) {
    this.executeTestQuery();
  }

  async executeTestQuery() {
    this.queryResult = await this.spRestService.executeTestQuery();
  }
}
  • Open the AppManifest.xml file and set the Permissions to Scope: Web and Permission: Read

The application is now complete.

To start it locally:

  • open a node.js window and navigate to the project dir
  • execute: npm run serve (This executes SP-REST-PROY)
  • open another node.js window and navigate to the project dir
  • execute: npm run hmr
  • open a browser and go to http://localhost:4200
  • the list names of all hostweb lists are displayed, in the first node js window, you should see the api call

To deploy it to SharePoint:

  • right click deploy

That's it so far. I'm not sure about the staticRoot: './app/src' part of the server.js file. Is it necessary to define the staticRoot?

What do you think, is there something i missed?

I will play around with the project a little more. So far i like it :)
Cheers!
Pascal

Cool stuff! It deserves to be a blog post!
Glad you've figured out how to apply the lib into your dev workflow.

I can mention issue's tips which you've kindly created in project's readme.