React Content Query : Added configurable external scripts and external block helpers (#285)

* Added the files to the new repository

* Modified the .gitignore

* Added the generated sppkg file

* Added animated gifs for the read me.

* Added another animated gif

* Added a first temporary version of the readme.md

* Update README.md

* Update README.md

* Updated the readme.md file by removing spaces between brackets in the handlebars templates examples.

* Updated the WebPart with inline template editing

Added inline template editing within the toolpane, automatic template
generation and fixed a bug related to the "Me" checkbox while filtering
user fields.

* Removed the debug folder from sources

* Updated the toolpart animated gif

* Removed toolpart animated gif

* Updated the README.md file with the latest changes.

* Update README.md

* Update README.md

* Update README.md

* Adding AceEditor as Code Editor

* Removing old Monato Artefacts

* Finalized the code editor update

Added documentation for the code editor, fixed the npm-shrinkwrap to
support the code editor dependencies, updated images for the new
documentation, regenerated the .sppkg file and updated the sources on
the public CDN for version 1.0.1

* Added a .gif for the new code editor documentation

* Updated the WebPart with the latest spfx packages.

Multiple bottle-neck bugs have been fixed in SPFX which allows to update
the WebPart with the latest SPFX packages. Overall performance should
now be better and other minor bugs have been fixed.

* ...

* Updated the readme

* Updated the readme with the solution author

* Added the External Scripts property to the toolpart

The new External Scripts property allows the user to add external
JavaScript files to the WebPart that can run precisely before or after
the rendering of the template.

* Fixed the readme.md imaged broke in the previous commit

* Added the ExternalScripts.png picture to readme.md

* Updated readme.md

* Update the externalScripts image.
This commit is contained in:
Simon-Pierre Plante 2017-08-14 02:49:21 -04:00 committed by Vesa Juvonen
parent 1bc64cf26c
commit 70fce10f75
14 changed files with 359 additions and 133 deletions

View File

@ -1,52 +1,52 @@
# Spfx Webpart: File Upload using AngularJs # Spfx Webpart: File Upload using AngularJs
## Summary ## Summary
File Update/Delete webpart using AngularJs and ngOfficeUIFabric with the SharePoint Framework. File Update/Delete webpart using AngularJs and ngOfficeUIFabric with the SharePoint Framework.
![File Upload using Angular](http://i.imgur.com/U5qg4II.png) ![File Upload using Angular](http://i.imgur.com/U5qg4II.png)
Edit webpart properties to set Document library Name. Initially, It has been set to `Documents`. Edit webpart properties to set Document library Name. Initially, It has been set to `Documents`.
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/version-GA-green.svg) ![drop](https://img.shields.io/badge/version-GA-green.svg)
## Applies to ## Applies to
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview) * [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant) * [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution ## Solution
Solution|Author(s) Solution|Author(s)
--------|--------- --------|---------
angular-ngofficeuifabric-file-upload | Atish Kumar Dipongkor (MVP, Office Development), Gautam Sheth(SharePoint Consultant,RapidCircle,@gautamdsheth) angular-ngofficeuifabric-file-upload | Atish Kumar Dipongkor (MVP, Office Development), Gautam Sheth(SharePoint Consultant,RapidCircle,@gautamdsheth)
## Version history ## Version history
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|November 24, 2016|Initial release 1.0|November 24, 2016|Initial release
2.0|May 26, 2017|GA release 2.0|May 26, 2017|GA release
2.1|July 19, 2017|Bug fix 2.1|July 19, 2017|Bug fix
## Disclaimer ## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
--- ---
## Minimal Path to Awesome ## Minimal Path to Awesome
- Clone this repository - Clone this repository
- in the command line run: - in the command line run:
- `npm install` - `npm install`
- `tsd install` - `tsd install`
- `gulp serve --nobrowser` - `gulp serve --nobrowser`
## Features ## Features
This Web Part illustrates the following concepts on top of the SharePoint Framework & AngularJs: This Web Part illustrates the following concepts on top of the SharePoint Framework & AngularJs:
- `BaseService`: By injecting this Angular Service, GET, POST, UPDATE & DELETE requests can be made easily. It's a resuable service. - `BaseService`: By injecting this Angular Service, GET, POST, UPDATE & DELETE requests can be made easily. It's a resuable service.
- `CustomFileChange`: It's a custom Angular directive. It binds the file with model on file change event. - `CustomFileChange`: It's a custom Angular directive. It binds the file with model on file change event.
- `IsoToDateString`: It's a custom Angular filter. It formats ISO date string to `{0:yyyy}-{0:MM}-{0:dd}` format. - `IsoToDateString`: It's a custom Angular filter. It formats ISO date string to `{0:yyyy}-{0:MM}-{0:dd}` format.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/angular-ngofficeuifabric-file-upload" /> <img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/angular-ngofficeuifabric-file-upload" />

View File

@ -1,69 +1,69 @@
# JQuery, Photopile.JS & Office UI Fabric Client-Side Web Part # JQuery, Photopile.JS & Office UI Fabric Client-Side Web Part
## Summary ## Summary
This is a sample web part that illustrated the use of JQuery and [Photopile.Js](https://github.com/bigbhowell/Photopile-JS) This is a sample web part that illustrated the use of JQuery and [Photopile.Js](https://github.com/bigbhowell/Photopile-JS)
with the SharePoint Framework. with the SharePoint Framework.
With this web part you can display the photos contained in a SharePoint pictures library and it With this web part you can display the photos contained in a SharePoint pictures library and it
simulates a pile of photos scattered about on a surface. Thumbnail clicks remove photos from the pile, simulates a pile of photos scattered about on a surface. Thumbnail clicks remove photos from the pile,
(enlarging them as if being picked up by the user) and once in view a second click returns the photo to the pile. (enlarging them as if being picked up by the user) and once in view a second click returns the photo to the pile.
![Photopile Web Part displayed in SharePoint Workbench](./assets/photopileoverview.gif) ![Photopile Web Part displayed in SharePoint Workbench](./assets/photopileoverview.gif)
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-drop1-red.svg) ![drop](https://img.shields.io/badge/drop-drop1-red.svg)
## Applies to ## Applies to
* [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview) * [SharePoint Framework Developer Preview](http://dev.office.com/sharepoint/docs/spfx/sharepoint-framework-overview)
* [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant) * [Office 365 developer tenant](http://dev.office.com/sharepoint/docs/spfx/set-up-your-developer-tenant)
## Solution ## Solution
Solution|Author(s) Solution|Author(s)
--------|--------- --------|---------
jquery-photopile|Olivier Carpentier (@olivierc) jquery-photopile|Olivier Carpentier (@olivierc)
## Version history ## Version history
Version|Date|Comments Version|Date|Comments
-------|----|-------- -------|----|--------
1.0|September 9, 2016|Initial release 1.0|September 9, 2016|Initial release
## Disclaimer ## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
--- ---
## Minimal Path to Awesome ## Minimal Path to Awesome
- clone this repo - clone this repo
- in the command line run: - in the command line run:
- `npm install` - `npm install`
- `tsd install` - `tsd install`
- `gulp serve` - `gulp serve`
## Features ## Features
This web part uses React, Office UI Fabric, JQuery, JQuery UI and Photopile.js. This web part is available in English (en-us) This web part uses React, Office UI Fabric, JQuery, JQuery UI and Photopile.js. This web part is available in English (en-us)
and French (fr-fr). and French (fr-fr).
It is able to: It is able to:
* List picture libraries contained in the current SharePoint web site * List picture libraries contained in the current SharePoint web site
* List all the pictures in the selected List * List all the pictures in the selected List
* Render the pictures as a cool photopile * Render the pictures as a cool photopile
* Personalize the layout thanks to editable settings * Personalize the layout thanks to editable settings
This web part illustrates the following concepts on top of the SharePoint Framework: This web part illustrates the following concepts on top of the SharePoint Framework:
* Include JQuery and external framework in your solution * Include JQuery and external framework in your solution
* Implement rich web part properties panel with controls like DropDown, Sliders, Toggle, etc. * Implement rich web part properties panel with controls like DropDown, Sliders, Toggle, etc.
* Load dynamic data from SharePoint as web part properties * Load dynamic data from SharePoint as web part properties
* Load dynamic data from SharePoint REST Services, as lists or items * Load dynamic data from SharePoint REST Services, as lists or items
* Implement mock system to test your solution in the local workbench or on a SharePoint site * Implement mock system to test your solution in the local workbench or on a SharePoint site
* Include Office UI Fabric controls in your project * Include Office UI Fabric controls in your project
* Render content with React * Render content with React
* Etc. * Etc.
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/jquery-photopile" /> <img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/jquery-photopile" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -4,7 +4,7 @@
The `React Content Query WebPart` is a modern version of the good old `Content by Query WebPart` that was introduced in SharePoint 2007. Built for *SharePoint 2016* and *Office 365*, this modern version is built against the new **SharePoint Framework (SPFx)** and uses the latest *Web Stack* practices. While the original WebPart was based on a `XSLT` templating engine, this *React* WebPart is based on the well known [Handlebars templating engine](http://handlebarsjs.com), which empowers users to create simple, yet powerfull `HTML` templates for rendering the queried content. This new version also lets the user query `any site collections` which resides on the same domain url, add `unlimited filters`, query *DateTime* fields to the `nearest minute` rather than being limited to a day, and much more. The `React Content Query WebPart` is a modern version of the good old `Content by Query WebPart` that was introduced in SharePoint 2007. Built for *SharePoint 2016* and *Office 365*, this modern version is built against the new **SharePoint Framework (SPFx)** and uses the latest *Web Stack* practices. While the original WebPart was based on a `XSLT` templating engine, this *React* WebPart is based on the well known [Handlebars templating engine](http://handlebarsjs.com), which empowers users to create simple, yet powerfull `HTML` templates for rendering the queried content. This new version also lets the user query `any site collections` which resides on the same domain url, add `unlimited filters`, query *DateTime* fields to the `nearest minute` rather than being limited to a day, and much more.
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/toolpart.gif" /> <img src="Misc/toolpart.gif" />
## Used SharePoint Framework Version ## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-GA-green.svg) ![drop](https://img.shields.io/badge/drop-GA-green.svg)
@ -26,6 +26,7 @@ Version|Date|Comments
-------|----|-------- -------|----|--------
1.0.0|May 04, 2017|Initial release 1.0.0|May 04, 2017|Initial release
1.0.1|July 23rd 15, 2017|Updated to GA Version 1.0.1|July 23rd 15, 2017|Updated to GA Version
1.0.3|August 12, 2017|Added external scripts functionnality
## Disclaimer ## Disclaimer
**THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.** **THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.**
@ -36,21 +37,21 @@ Version|Date|Comments
The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well. The WebPart uses the search in order to get all sites under the current domain, which makes it possible to query not only subsites but other site collections and their subsites as well.
![DateTime](https://github.com/spplante/react-content-query/blob/master/Misc/allsites.gif "DateTime") <img src="Misc/allsites.gif" />
<br> <br>
### Unlimited filters ### Unlimited filters
The user isn't limited to 3 filters anymore, an unlimited amount of filters can be added to narrow down your query The user isn't limited to 3 filters anymore, an unlimited amount of filters can be added to narrow down your query
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/filters.gif" width="500" /> <img src="Misc/filters.gif" width="500" />
<br> <br>
### Improved date time filters ### Improved date time filters
It is now possible to include time validation when querying date fields, giving the ability to be more precise when it comes to querying items against date values. It is now possible to include time validation when querying date fields, giving the ability to be more precise when it comes to querying items against date values.
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/datetime.gif" width="500" /> <img src="Misc/datetime.gif" width="500" />
<br> <br>
### Handlebars templating engine ### Handlebars templating engine
@ -64,9 +65,17 @@ For advanced users, more than 150 Handlebars block helpers are available by defa
Edit your Handlebars template directly within the toolpane using a built-in [code editor](https://ace.c9.io/) which provides code folding, syntax highlighting, line wrapping, indentation and many more features to the tip of your fingers. Edit your Handlebars template directly within the toolpane using a built-in [code editor](https://ace.c9.io/) which provides code folding, syntax highlighting, line wrapping, indentation and many more features to the tip of your fingers.
<img src="https://github.com/spplante/react-content-query/blob/master/Misc/editor.gif" /> <img src="Misc/editor.gif" />
<br> <br>
### Include your own external scripts and/or block helpers!
You can now specify your own external scripts that needs to be loaded either **before** or **after** rendering the Handlebars template.
<img src="Misc/externalScripts.png" />
External scripts can be used to include either libraries such as *jQuery*, or even *custom logic scripts* that can leverage the exposed **onPrerender** and **onPostRender** methods for advanced functionnalities.
## Getting Started ## Getting Started
### Adding the WebPart to your page ### Adding the WebPart to your page
@ -183,6 +192,91 @@ Property | Description
``` ```
<br> <br>
### Including your own external scripts and/or block helpers
#### Including basic library files
For including JavaScript files within the WebPart, file URLs must be added to the **External Scripts** parameter available in the toolpart.
<img src="Misc/externalScripts.png" />
Each file URL must be on its own line, and placed in the desired order. The scripts will be loaded asynchronously, but in a sequential fashion, which means that the WebPart will wait until a script is completely loaded before proceeding to load the next one.
#### Including custom logic files
If you need custom logic files that can interact precisely **before** or **after** the rendering of the HTML generated by the *Handlebars* template, you must follow the patern below in order for the WebPart to recognize the endpoints and call them when needed :
```javascript
ReactContentQuery.ExternalScripts.MyScriptFile = {
onPreRender: function(wpContext, handlebarsContext) {
// Do someting before rendering (ie: adding a custom block helper)
},
onPostRender: function(wpContext, handlebarsContext) {
// Do something after rendering (ie: calling a plugin on the generated HTML)
}
}
```
Looking at this example, here are the key things that needs to be respected in order for the file to work :
_Namespace_
- [x] The script uses a namespace which starts by **ReactContentQuery.ExternalScripts.**, followed by the name of its own file
- [x] The name of the file has to be written without its **.js** extension, and without any caracters that aren't letters or numbers
- [x] The name of the file needs to respect the same casing as in it's URL
Examples :
*https://www.mysite.com/MyScript.js*
```javascript
ReactContentQuery.ExternalScripts.MyScript {
...
}
```
*https://www.mysite.com/_My-Funky*named*Script_.js*
```javascript
ReactContentQuery.ExternalScripts.MyFunkynamedScript {
...
}
```
_Functions_
- [x] The script implements the **onPreRender** function for code that has to be executed before rendering
- [x] The scripts implements the **onPostRender** function for code that has to be executed after rendering
Both functions provide the following parameters :
Parameter | Description
----------------------|-------------
**wpContext** | Represents the context of the WebPart who called the function, which exposes all kinds of usefull informations such as **wpContext.domElement** which represents the HTML element on which the current WebPart is being rendered.
**handlebarsContext** | Represents the handlebars context used for generating the template of the current WebPart. Can be used for adding handlebar block helpers in the **onPreRender** function for example.
#### Including custom block helpers
Custom *block helpers* that can be used directly within the *Handlebars* template can be added simply by using a custom logic script file that implements the **onPreRender** function.
Example:
*https://www.mysite.com/MyCustomBlockHelper.js*
```javascript
ReactContentQuery.ExternalScripts.MyCustomBlockHelper = {
onPreRender: function(wpContext, handlebarsContext) {
// Adds the "testHelper"
handlebarsContext.registerHelper('testHelper', function(param, options) {
return "Output from testHelper : " + param;
});
}
}
```
## Building the code ## Building the code
```bash ```bash

View File

@ -1,3 +1,3 @@
{ {
"cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query-1.0.2" "cdnBasePath": "https://publiccdn.sharepointonline.com/spptechnologies.sharepoint.com/110700492eeea162ee5bad0f35b1f0061ded8bf436ce0199efe2a4d24109e1c0df1ec594/react-content-query-1.0.3"
} }

View File

@ -1,6 +1,6 @@
{ {
"name": "react-content-query", "name": "react-content-query",
"version": "1.0.2", "version": "1.0.3",
"private": true, "private": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"

View File

@ -15,4 +15,5 @@ export class ContentQueryConstants {
public static readonly propertyTemplateText = "templateText"; public static readonly propertyTemplateText = "templateText";
public static readonly propertyTemplateUrl = "templateUrl"; public static readonly propertyTemplateUrl = "templateUrl";
public static readonly propertyhasDefaultTemplateBeenUpdated = "hasDefaultTemplateBeenUpdated"; public static readonly propertyhasDefaultTemplateBeenUpdated = "hasDefaultTemplateBeenUpdated";
public static readonly propertyExternalScripts = "externalScripts";
} }

View File

@ -48,13 +48,14 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
private viewFieldsChecklist: PropertyPaneAsyncChecklist; private viewFieldsChecklist: PropertyPaneAsyncChecklist;
private templateTextDialog: PropertyPaneTextDialog; private templateTextDialog: PropertyPaneTextDialog;
private templateUrlTextField: IPropertyPaneField<IPropertyPaneTextFieldProps>; private templateUrlTextField: IPropertyPaneField<IPropertyPaneTextFieldProps>;
private externalScripts: IPropertyPaneField<IPropertyPaneTextFieldProps>;
/*************************************************************************** /***************************************************************************
* Returns the WebPart's version * Returns the WebPart's version
***************************************************************************/ ***************************************************************************/
protected get dataVersion(): Version { protected get dataVersion(): Version {
return Version.parse('1.0.2'); return Version.parse('1.0.3');
} }
@ -91,6 +92,8 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
querySettings: querySettings, querySettings: querySettings,
templateText: this.properties.templateText, templateText: this.properties.templateText,
templateUrl: this.properties.templateUrl, templateUrl: this.properties.templateUrl,
wpContext: this.context,
externalScripts: this.properties.externalScripts ? this.properties.externalScripts.split('\n').filter((script) => { return (script && script.trim() != ''); }) : null,
strings: strings.contentQueryStrings, strings: strings.contentQueryStrings,
stateKey: new Date().toString() stateKey: new Date().toString()
} }
@ -201,6 +204,16 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
onGetErrorMessage: this.onItemLimitChange.bind(this) onGetErrorMessage: this.onItemLimitChange.bind(this)
}); });
// Creates a PropertyPaneTextField for the externalScripts property
this.externalScripts = PropertyPaneTextField(ContentQueryConstants.propertyExternalScripts, {
label: strings.ExternalScriptsLabel,
deferredValidationTime: 500,
placeholder: strings.ExternalScriptsPlaceholder,
multiline: true,
rows: 5,
onGetErrorMessage: () => { return ''; }
});
return { return {
pages: [ pages: [
{ {
@ -242,6 +255,17 @@ export default class ContentQueryWebPart extends BaseClientSideWebPart<IContentQ
] ]
} }
] ]
},
{
header: { description: strings.ExternalPageDescription },
groups: [
{
groupName: strings.ExternalGroupName,
groupFields: [
this.externalScripts
]
}
]
} }
] ]
}; };

View File

@ -12,4 +12,5 @@ export interface IContentQueryWebPartProps {
templateText: string; templateText: string;
templateUrl: string; templateUrl: string;
hasDefaultTemplateBeenUpdated: boolean; hasDefaultTemplateBeenUpdated: boolean;
externalScripts: string;
} }

View File

@ -3,15 +3,26 @@ import * as Handlebars from "handlebars";
import * as strings from 'contentQueryStrings'; import * as strings from 'contentQueryStrings';
import { Checkbox, Spinner } from 'office-ui-fabric-react'; import { Checkbox, Spinner } from 'office-ui-fabric-react';
import { isEmpty } from '@microsoft/sp-lodash-subset'; import { isEmpty } from '@microsoft/sp-lodash-subset';
import { Text } from '@microsoft/sp-core-library'; import { Text, Log } from '@microsoft/sp-core-library';
import { IContentQueryProps } from './IContentQueryProps'; import { IContentQueryProps } from './IContentQueryProps';
import { IContentQueryState } from './IContentQueryState'; import { IContentQueryState } from './IContentQueryState';
import { IContentQueryTemplateContext } from './IContentQueryTemplateContext'; import { IContentQueryTemplateContext } from './IContentQueryTemplateContext';
import { SPComponentLoader } from '@microsoft/sp-loader';
import styles from './ContentQuery.module.scss'; import styles from './ContentQuery.module.scss';
export default class ContentQuery extends React.Component<IContentQueryProps, IContentQueryState> { export default class ContentQuery extends React.Component<IContentQueryProps, IContentQueryState> {
/*************************************************************************************
* Constants
*************************************************************************************/
private readonly logSource = "ContentQuery.tsx";
private readonly nsReactContentQuery = "ReactContentQuery";
private readonly nsExternalScripts = "ExternalScripts";
private readonly callbackOnPreRenderName = "onPreRender";
private readonly callbackOnPostRenderName = "onPostRender";
/************************************************************************************* /*************************************************************************************
* Stores the timestamps of each async calls in order to wait for the last call in * Stores the timestamps of each async calls in order to wait for the last call in
* case multiple calls have been fired in a short lapse of time by updaing the * case multiple calls have been fired in a short lapse of time by updaing the
@ -33,6 +44,10 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
handlebars: Handlebars handlebars: Handlebars
}); });
// Ensures the WebPart's namespace for external scripts
window[this.nsReactContentQuery] = window[this.nsReactContentQuery] || {};
window[this.nsReactContentQuery][this.nsExternalScripts] = window[this.nsReactContentQuery][this.nsExternalScripts] || {};
this.onGoingAsyncCalls = []; this.onGoingAsyncCalls = [];
this.state = { loading: true, processedTemplateResult: null, error: null }; this.state = { loading: true, processedTemplateResult: null, error: null };
} }
@ -46,6 +61,34 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
} }
/*************************************************************************************
* Loads the external scritps sequentially (one after the other) if any
*************************************************************************************/
private loadExternalScriptsSequentially(scriptUrls:string[]): Promise<{}> {
var index = 0;
var _this_ = this;
return new Promise((resolve, reject) => {
function next() {
if (scriptUrls && index < scriptUrls.length) {
SPComponentLoader.loadScript(scriptUrls[index++])
.then(next)
.catch((error) => {
// As of August 12th 2017, Log.error doesn't seem to do anything, so I use a console.log on top of it for now.
Log.error(_this_.logSource, error, _this_.props.wpContext.serviceScope);
console.log(error);
next();
});
}
else {
resolve();
}
}
next();
});
}
/************************************************************************************* /*************************************************************************************
* Loads the items asynchronously and wraps them into a context object for handlebars * Loads the items asynchronously and wraps them into a context object for handlebars
*************************************************************************************/ *************************************************************************************/
@ -57,7 +100,7 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
let currentCallTimeStamp = new Date().valueOf(); let currentCallTimeStamp = new Date().valueOf();
this.onGoingAsyncCalls.push(currentCallTimeStamp); this.onGoingAsyncCalls.push(currentCallTimeStamp);
// Resets the state if this is the first call // Sets the state to "loading" only if it's the only async call going on (otherwise it's already loading)
if(this.onGoingAsyncCalls.length == 1) { if(this.onGoingAsyncCalls.length == 1) {
this.setState({ this.setState({
loading: true, loading: true,
@ -123,6 +166,9 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
*************************************************************************************/ *************************************************************************************/
private processTemplate(templateContent: string, templateContext: IContentQueryTemplateContext) { private processTemplate(templateContent: string, templateContext: IContentQueryTemplateContext) {
try { try {
// Calls the external OnPreRender callbacks if any
this.executeExternalCallbacks(this.callbackOnPreRenderName);
// Processes the template // Processes the template
let template = Handlebars.compile(templateContent); let template = Handlebars.compile(templateContent);
let result = template(templateContext); let result = template(templateContext);
@ -131,6 +177,9 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
if(this.onGoingAsyncCalls.length == 0) { if(this.onGoingAsyncCalls.length == 0) {
this.setState({ loading: false, processedTemplateResult: result, error: null }); this.setState({ loading: false, processedTemplateResult: result, error: null });
} }
// Calls the external OnPostRender callbacks if any
this.executeExternalCallbacks(this.callbackOnPostRenderName);
} }
catch(error) { catch(error) {
this.setState({ loading: false, processedTemplateResult: null, error: Text.format(this.props.strings.errorProcessingTemplate, error) }); this.setState({ loading: false, processedTemplateResult: null, error: Text.format(this.props.strings.errorProcessingTemplate, error) });
@ -138,6 +187,47 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
} }
/*************************************************************************************
* Executes the specified callback for every external script, if available
*************************************************************************************/
private executeExternalCallbacks(callbackName: string) {
if(this.props.externalScripts) {
// Gets the ReactContentQuery namespace previously created in the constructor
var ReactContentQuery = window[this.nsReactContentQuery];
// Loops through all the external scripts of the current WebPart
for(let scriptUrl of this.props.externalScripts) {
// Generates a valid namespace suffix based on the current file name
var namespaceSuffix = this.generateNamespaceFromScriptUrl(scriptUrl);
// Checks if the current file's namespace is available within the page
var scriptNamespace = ReactContentQuery[this.nsExternalScripts][namespaceSuffix];
if(scriptNamespace) {
// Checks if the needed callback is available in the script's namespace
var callback = scriptNamespace[callbackName];
if(callback) {
callback(this.props.wpContext, Handlebars);
}
}
}
}
}
/*************************************************************************************
* Extracts the file name out of the specified url and normalizes it for a namespace
*************************************************************************************/
private generateNamespaceFromScriptUrl(scriptUrl: string): string {
return scriptUrl.substring(scriptUrl.lastIndexOf('/') + 1).replace('.js', '').replace(/[^a-zA-Z0-9]/g, "");
}
/************************************************************************************* /*************************************************************************************
* Returns whether all mandatory fields are configured or not * Returns whether all mandatory fields are configured or not
*************************************************************************************/ *************************************************************************************/
@ -162,7 +252,9 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
* Called once after initial rendering * Called once after initial rendering
*************************************************************************************/ *************************************************************************************/
public componentDidMount(): void { public componentDidMount(): void {
this.loadTemplateContext(); this.loadExternalScriptsSequentially(this.props.externalScripts).then(() => {
this.loadTemplateContext();
});
} }
@ -171,7 +263,9 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
*************************************************************************************/ *************************************************************************************/
public componentDidUpdate(prevProps: IContentQueryProps, prevState: IContentQueryState): void { public componentDidUpdate(prevProps: IContentQueryProps, prevState: IContentQueryState): void {
if(prevProps.stateKey !== this.props.stateKey) { if(prevProps.stateKey !== this.props.stateKey) {
this.loadTemplateContext(); this.loadExternalScriptsSequentially(this.props.externalScripts).then(() => {
this.loadTemplateContext();
});
} }
} }
@ -185,6 +279,8 @@ export default class ContentQuery extends React.Component<IContentQueryProps, IC
const error = this.state.error ? <div className={styles.cqwpError}>{this.state.error}</div> : <div />; const error = this.state.error ? <div className={styles.cqwpError}>{this.state.error}</div> : <div />;
const mandatoryFieldsConfigured = this.areMandatoryFieldsConfigured(); const mandatoryFieldsConfigured = this.areMandatoryFieldsConfigured();
console.log('Rendering now...');
return ( return (
<div className={styles.cqwp}> <div className={styles.cqwp}>

View File

@ -1,14 +1,16 @@
import { IWebPartContext } from '@microsoft/sp-webpart-base';
import { IContentQueryTemplateContext } from './IContentQueryTemplateContext'; import { IContentQueryTemplateContext } from './IContentQueryTemplateContext';
import { IContentQueryStrings } from './IContentQueryStrings'; import { IContentQueryStrings } from './IContentQueryStrings';
import { IQuerySettings } from './IQuerySettings'; import { IQuerySettings } from './IQuerySettings';
export interface IContentQueryProps { export interface IContentQueryProps {
onLoadTemplate: (templateUrl: string) => Promise<string>; onLoadTemplate: (templateUrl: string) => Promise<string>;
onLoadTemplateContext: (querySettings: IQuerySettings, callTimeStamp: number) => Promise<IContentQueryTemplateContext>; onLoadTemplateContext: (querySettings: IQuerySettings, callTimeStamp: number) => Promise<IContentQueryTemplateContext>;
querySettings: IQuerySettings; querySettings: IQuerySettings;
templateText?: string; templateText?: string;
templateUrl?: string; templateUrl?: string;
wpContext: IWebPartContext;
externalScripts?: string[];
strings: IContentQueryStrings; strings: IContentQueryStrings;
stateKey: string; stateKey: string;
} }

View File

@ -3,9 +3,11 @@ define([], function() {
SourcePageDescription: "Specify where the WebPart should get the results from.", SourcePageDescription: "Specify where the WebPart should get the results from.",
QueryPageDescription: "If needed, choose the sorting behavior, limit the results, or add filters in order to narrow the query down.", QueryPageDescription: "If needed, choose the sorting behavior, limit the results, or add filters in order to narrow the query down.",
DisplayPageDescription: "Specify which fields should be available for rendering within the HandleBars template, and edit your handlebars template.", DisplayPageDescription: "Specify which fields should be available for rendering within the HandleBars template, and edit your handlebars template.",
ExternalPageDescription: "If needed, specify the external resources that needs to be loaded such as javascript files.",
SourceGroupName: "Source", SourceGroupName: "Source",
QueryGroupName: "Query", QueryGroupName: "Query",
DisplayGroupName: "Display", DisplayGroupName: "Display",
ExternalGroupName: "External",
WebUrlFieldLabel: "Web Url", WebUrlFieldLabel: "Web Url",
WebUrlFieldPlaceholder: "Select the source web...", WebUrlFieldPlaceholder: "Select the source web...",
WebUrlFieldLoadingLabel: "Loading webs from current site...", WebUrlFieldLoadingLabel: "Loading webs from current site...",
@ -22,6 +24,8 @@ define([], function() {
ErrorItemLimit: "Value must be a number between 1 to 999", ErrorItemLimit: "Value must be a number between 1 to 999",
TemplateUrlFieldLabel: "Template Url", TemplateUrlFieldLabel: "Template Url",
TemplateUrlPlaceholder: "Enter a valid HandleBars .htm file url", TemplateUrlPlaceholder: "Enter a valid HandleBars .htm file url",
ExternalScriptsLabel: "External Scripts",
ExternalScriptsPlaceholder: "https://mysite.com/SiteAssets/library1.js\nhttps://mysite.com/SiteAssets/library2.js\nhttps://mysite.com/SiteAssets/mylogic.js\n...",
ErrorTemplateExtension: "The template must be a valid .htm or .html file", ErrorTemplateExtension: "The template must be a valid .htm or .html file",
ErrorTemplateResolve: "Unable to resolve the specified template : {0}", ErrorTemplateResolve: "Unable to resolve the specified template : {0}",
ErrorWebAccessDenied: "You do not have access to the previously configured web url '{0}'. Either leave the WebPart properties as is or select another web url.", ErrorWebAccessDenied: "You do not have access to the previously configured web url '{0}'. Either leave the WebPart properties as is or select another web url.",

View File

@ -2,9 +2,11 @@ declare interface IContentQueryStrings {
SourcePageDescription: string; SourcePageDescription: string;
QueryPageDescription: string; QueryPageDescription: string;
DisplayPageDescription: string; DisplayPageDescription: string;
ExternalPageDescription: string;
SourceGroupName: string; SourceGroupName: string;
QueryGroupName: string; QueryGroupName: string;
DisplayGroupName: string; DisplayGroupName: string;
ExternalGroupName: string;
WebUrlFieldLabel: string; WebUrlFieldLabel: string;
WebUrlFieldPlaceholder: string; WebUrlFieldPlaceholder: string;
WebUrlFieldLoadingLabel: string; WebUrlFieldLoadingLabel: string;
@ -21,6 +23,8 @@ declare interface IContentQueryStrings {
ErrorItemLimit: string; ErrorItemLimit: string;
TemplateUrlFieldLabel: string; TemplateUrlFieldLabel: string;
TemplateUrlPlaceholder: string; TemplateUrlPlaceholder: string;
ExternalScriptsLabel: string;
ExternalScriptsPlaceholder: string;
ErrorTemplateExtension: string; ErrorTemplateExtension: string;
ErrorTemplateResolve: string; ErrorTemplateResolve: string;
ErrorWebAccessDenied: string; ErrorWebAccessDenied: string;