Adding the riot-list sample webpart (#184)

* Adding the riot-list sample webpart

* Adding the latest Animated Gif file
This commit is contained in:
Sébastien Levert 2017-05-04 14:56:54 -04:00 committed by Vesa Juvonen
parent cf2dd21984
commit f3669414d9
40 changed files with 811 additions and 0 deletions

View File

@ -0,0 +1,25 @@
# EditorConfig helps developers define and maintain consistent
# coding styles between different editors and IDEs
# editorconfig.org
root = true
[*]
# change these settings to your own preference
indent_style = space
indent_size = 2
# we recommend you to keep these unchanged
end_of_line = lf
charset = utf-8
trim_trailing_whitespace = true
insert_final_newline = true
[*.md]
trim_trailing_whitespace = false
[{package,bower}.json]
indent_style = space
indent_size = 2

1
samples/riot-list/.gitattributes vendored Normal file
View File

@ -0,0 +1 @@
* text=auto

32
samples/riot-list/.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Logs
logs
*.log
npm-debug.log*
# Dependency directories
node_modules
# Build generated files
dist
lib
solution
temp
*.sppkg
# Coverage directory used by tools like istanbul
coverage
# OSX
.DS_Store
# Visual Studio files
.ntvs_analysis.dat
.vs
bin
obj
# Resx Generated Code
*.resx.ts
# Styles Generated Code
*.scss.ts

View File

@ -0,0 +1,14 @@
# Folders
.vscode
coverage
node_modules
sharepoint
src
temp
# Files
*.csproj
.git*
.yo-rc.json
gulpfile.js
tsconfig.json

View File

@ -0,0 +1,8 @@
{
"@microsoft/generator-sharepoint": {
"libraryName": "spfx-riot-list",
"framework": "none",
"version": "1.0.2",
"libraryId": "6e137a70-041c-49e1-aa9e-470b704180a0"
}
}

View File

@ -0,0 +1,55 @@
# List RiotJS Client-Side Web Part
## Summary
Simplistic sample Web Part that demonstrates the use of RiotJS in creating a SharePoint Framework web part. The properties pane for this web part display a drop down list of lists in the current web. Once the user selects one of the lists, the web part display the contents of the list.
![Screeshot of the Display List web part](./assets/riot-list-preview.gif)
> Does only show data when hosted in SharePoint. No mock data at this point for local testing the rendering.
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-ga-green.svg)
## Applies to
* [SharePoint Framework General Availability](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)
## Solution
Solution|Author(s)
--------|---------
spfx-riot-list|Sébastien Levert (MVP, Valo Intranet, @sebastienlevert)
## Version history
Version|Date|Comments
-------|----|--------
1.0|April 26, 2017|Initial release
## 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.**
---
## Minimal Path to Awesome
- Clone this repository
- in the command line run:
- `npm install`
- `gulp serve`
- `Open the *workbench* on your Office 365 Developer tenant`
- Basic functionality can be tested locally, data is only shown when used in context of SharePoint
## Features
The spfx-riot-webpart web part displays the content of the list specified in the web part properties pane.
This Web Part illustrates the following concepts on top of the SharePoint Framework:
* Using RiotJS as the component rendering engine
* The use of modular CSS
* The use of PnP JS Core with async/await support in TypeScript

Binary file not shown.

After

Width:  |  Height:  |  Size: 726 KiB

View File

@ -0,0 +1,13 @@
{
"entries": [
{
"entry": "./lib/webparts/listViewer/ListViewerWebPart.js",
"manifest": "./src/webparts/listViewer/ListViewerWebPart.manifest.json",
"outputPath": "./dist/list-viewer.bundle.js"
}
],
"externals": {},
"localizedResources": {
"listViewerStrings": "webparts/listViewer/loc/{locale}.js"
}
}

View File

@ -0,0 +1,3 @@
{
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,6 @@
{
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "spfx-riot-list",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,10 @@
{
"solution": {
"name": "riot-list-client-side-solution",
"id": "6e137a70-041c-49e1-aa9e-470b704180a0",
"version": "1.0.0.0"
},
"paths": {
"zippedPackage": "solution/riot-list.sppkg"
}
}

View File

@ -0,0 +1,9 @@
{
"port": 4321,
"initialPage": "https://localhost:5432/workbench",
"https": true,
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
// Display errors as warnings
"displayAsWarning": true,
// The TSLint task may have been configured with several custom lint rules
// before this config file is read (for example lint rules from the tslint-microsoft-contrib
// project). If true, this flag will deactivate any of these rules.
"removeExistingRules": true,
// When true, the TSLint task is configured with some default TSLint "rules.":
"useDefaultConfigAsBase": false,
// Since removeExistingRules=true and useDefaultConfigAsBase=false, there will be no lint rules
// which are active, other than the list of rules below.
"lintConfig": {
// Opt-in to Lint rules which help to eliminate bugs in JavaScript
"rules": {
"class-name": false,
"export-name": false,
"forin": false,
"label-position": false,
"member-access": true,
"no-arg": false,
"no-console": false,
"no-construct": false,
"no-duplicate-case": true,
"no-duplicate-variable": true,
"no-eval": false,
"no-function-expression": true,
"no-internal-module": true,
"no-shadowed-variable": true,
"no-switch-case-fall-through": true,
"no-unnecessary-semicolons": true,
"no-unused-expression": true,
"no-unused-imports": true,
"no-use-before-declare": true,
"no-with-statement": true,
"semicolon": true,
"trailing-comma": false,
"typedef": false,
"typedef-whitespace": false,
"use-named-parameter": true,
"valid-typeof": true,
"variable-name": false,
"whitespace": false
}
}
}

View File

@ -0,0 +1,3 @@
{
"cdnBasePath": "<!-- PATH TO CDN -->"
}

View File

@ -0,0 +1,6 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.initialize(gulp);

View File

@ -0,0 +1,33 @@
{
"name": "riot-list",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"dependencies": {
"@microsoft/sp-client-base": "~1.0.0",
"@microsoft/sp-core-library": "~1.0.0",
"@microsoft/sp-webpart-base": "~1.0.0",
"@types/webpack-env": ">=1.12.1 <1.14.0",
"office-ui-fabric-core": "5.0.1",
"office-ui-fabric-js": "^1.4.0",
"riot": "^3.4.2",
"riot-typescript": "^1.0.10",
"sp-client-custom-fields": "^1.3.3",
"sp-pnp-js": "^2.0.4"
},
"devDependencies": {
"@microsoft/sp-build-web": "~1.0.1",
"@microsoft/sp-module-interfaces": "~1.0.0",
"@microsoft/sp-webpart-workbench": "~1.0.0",
"gulp": "~3.9.1",
"@types/chai": ">=3.4.34 <3.6.0",
"@types/mocha": ">=2.2.33 <2.6.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
}
}

View File

@ -0,0 +1 @@
export * from './ListViewer/ListViewer.Index';

View File

@ -0,0 +1,4 @@
export interface IListViewerProps {
styles: any;
listId: string;
}

View File

@ -0,0 +1,2 @@
export * from './ListViewer';
export * from './IListViewerProps';

View File

@ -0,0 +1,26 @@
<list-viewer>
<div class="Placeholder">
<div class="Placeholder-container ms-Grid">
<div class="Placeholder-head ms-Grid-row">
<div class="ms-Grid-col ms-u-hiddenSm ms-u-md3">
</div>
<div class="Placeholder-headContainer ms-Grid-col ms-u-sm12 ms-u-md6">
<i class="Placeholder-icon ms-fontSize-su ms-Icon ms-Icon--GroupedList"></i>
<span class="Placeholder-text ms-fontWeight-light ms-fontSize-xxl">RiotJS List Viewer</span>
</div>
<div class="ms-Grid-col ms-u-hiddenSm ms-u-md3">
</div>
</div>
<div class="Placeholder-description ms-Grid-row" if="{ listId }">
<span class="Placeholder-descriptionText">{ list.Title }</span>
</div>
<div class="Placeholder-description ms-Grid-row" if="{ !listId }">
<span class="Placeholder-descriptionText">Please configure your webpart to select a list</span>
</div>
</div>
</div>
<office-ui-fabric-table is-loading="{ isLoading }" items="{ items }" styles="{ styles }" if="{ listId }"></office-ui-fabric-table>
</list-viewer>

View File

@ -0,0 +1,45 @@
import * as riot from "riot/riot+compiler";
import * as Riot from "./../../core/riot/Riot";
import pnp, { ODataEntityArray, ODataEntity } from "sp-pnp-js";
import { IListViewerProps } from "./IListViewerProps";
import { ItemObject, ListObject } from "./Models";
import { Table } from "./../../office-ui-fabric-riot/Components";
@Riot.template(require("./ListViewer.html") as string)
export class ListViewer extends Riot.Element {
private listId: string;
private styles: any;
private listTitle: string;
private items: ItemObject[] = new Array<ItemObject>();
private list: ListObject;
private isLoading: boolean = false;
constructor(options: IListViewerProps) {
super();
if(options) {
this.styles = options.styles;
this.listId = options.listId;
}
}
protected getSubComponentTypes(): any {
return [Table];
}
protected async mounted() {
await this.init();
}
protected async init() {
if(this.listId) {
this.isLoading = true;
this.update();
this.list = await pnp.sp.web.lists.getById(this.listId).getAs(ODataEntity(ListObject));
this.items = await pnp.sp.web.lists.getById(this.listId).items.getAs(ODataEntityArray(ItemObject));
this.isLoading = false;
this.update();
}
}
}

View File

@ -0,0 +1,11 @@
import { List, Item } from "sp-pnp-js";
export class ItemObject extends Item {
public Id: number;
public Title: string;
}
export class ListObject extends List {
public Id: string;
public Title: string;
}

View File

@ -0,0 +1,134 @@
import * as riot from "riot/riot+compiler";
export class Observable {
public on(events: string, callback: Function) {}
public one(events: string, callback: Function) {}
public off(events: string) {}
public trigger(eventName: string, ...args) {}
constructor() {
riot.observable(this);
}
}
export interface LifeCycle
{
mounted?(F: Function);
unmounted?(F: Function);
beforeMount?(F: Function);
beforeUnmount?(F: Function);
updating?(F: Function);
updated?(F: Function);
}
export interface HTMLRiotElement extends HTMLElement
{
_tag: Element;
}
export abstract class Element implements Observable, LifeCycle {
public opts: any;
public parent: Element;
public root: HTMLElement;
public tags: any;
public tagName: string;
public template: string;
public isMounted: boolean;
public element: HTMLElement;
public update(data?: any) { }
public unmount(keepTheParent?: boolean) { }
public on(eventName: string,fun: Function) { }
public one(eventName: string,fun: Function) { }
public off(events: string) {}
public trigger(eventName: string,...args) {}
public mixin(mixinObject: Object|Function|string, instance?: any) {}
protected abstract getSubComponentTypes() : any;
public static createElement(options?:any): HTMLRiotElement {
var tagName = (this.prototype as any).tagName;
var el = document.createElement(tagName);
riot.mount(el, tagName, options);
let riotElement = el as any as HTMLRiotElement;
return riotElement;
}
}
// new extend, works with getters and setters
function extend(d, element) {
var map = Object.keys(element.prototype).reduce((descriptors, key) => {
descriptors[key] = Object.getOwnPropertyDescriptor(element.prototype, key);
return descriptors;
},{}) as PropertyDescriptorMap;
Object.defineProperties(d, map);
}
export var precompiledTags: { [fileName: string]: CompilerResult } = {};
export function registerClass(element: Function) {
function registerTag(compiledTag: CompilerResult) {
var transformFunction = function (opts) {
extend(this,element); // copies prototype into "this"
element.apply(this, [opts]); // calls class constructor applying it on "this"
if(element.prototype.mounted !== undefined) this.on("mount" , this.mounted);
if(element.prototype.unmounted !== undefined) this.on("unmount" , this.unmounted);
if(element.prototype.updating !== undefined) this.on("update" , this.updating);
if(element.prototype.updated !== undefined) this.on("updated" , this.updated);
if(element.prototype.beforeMount !== undefined) this.on("before-mount" , this.beforeMount);
if(element.prototype.beforeUnmount !== undefined) this.on("before-unmount" , this.beforeUnmount);
};
riot.tag(compiledTag.tagName, compiledTag.html, compiledTag.css, compiledTag.attribs, transformFunction, riot.settings.brackets);
return compiledTag.tagName;
}
let compiled: CompilerResult;
// gets string template
if(element.prototype.template !== undefined) {
const tagTemplate: string = (element.prototype.template as string).replace(/\r\n/g, '');
compiled = riot.compile(tagTemplate, true, {entities: true})[0];
element.prototype.tagName = registerTag(compiled);
}
else throw "template property not specified";
}
// @template decorator
export function template(template: string) {
return (target: Function) => {
target.prototype["template"] = template;
registerClass(target);
};
}
export interface Router {
(callback: Function): void;
(filter: string, callback: Function): void;
(to: string, title?: string): void;
create(): Router;
start(autoExec?: boolean): void;
stop(): void;
exec(): void;
query(): any;
base(base: string): any;
parser(parser: (path: string)=>string, secondParser?: Function ): any;
}
export interface CompilerResult {
tagName: string;
html: string;
css: string;
attribs: string;
js: string;
}
export interface Settings {
brackets: string;
}

View File

@ -0,0 +1,15 @@
import {
BaseClientSideWebPart
} from '@microsoft/sp-webpart-base';
import * as riot from "riot/riot+compiler";
export abstract class RiotClientSideWebPart<TProperties> extends BaseClientSideWebPart<TProperties> {
protected abstract get tagName(): string;
protected abstract get webPartOptions(): any;
protected abstract get rootComponentType(): any;
public render(): void {
this.domElement.innerHTML = `<${this.tagName}></${this.tagName}>`;
riot.mount(this.domElement, this.tagName, this.webPartOptions);
}
}

View File

@ -0,0 +1,2 @@
export * from './Table/Table.Index';
export * from './Spinner/Spinner.Index';

View File

@ -0,0 +1,3 @@
@import '~office-ui-fabric-core/dist/sass/Fabric';
@import '~office-ui-fabric-js/dist/components/Table/Table';
@import '~office-ui-fabric-js/dist/components/Spinner/Spinner';

View File

@ -0,0 +1 @@
export * from './Spinner';

View File

@ -0,0 +1,7 @@
<office-ui-fabric-spinner>
<div class="{ opts.styles['ms-Spinner'] }">
<div class="{ opts.styles['ms-Spinner-label'] }" if="{ opts.label }">
{ opts.label }
</div>
</div>
</office-ui-fabric-spinner>

View File

@ -0,0 +1,128 @@
import * as riot from "riot/riot+compiler";
import * as Riot from "./../../core/riot/Riot";
@Riot.template(require("./Spinner.html") as string)
export class Spinner extends Riot.Element {
private eightSize: number = 0.2;
private animationSpeed: number = 90;
private interval: number;
private spinner: HTMLElement;
private numCircles: number;
private offsetSize: number;
private parentSize: number = 20;
private fadeIncrement: number = 0;
private circleObjects: Array<CircleObject> = [];
protected getSubComponentTypes(): any[] {
return [];
}
protected mounted() {
this.init();
}
public start(): void {
this.stop();
this.interval = setInterval(() => {
let i = this.circleObjects.length;
while (i--) {
this._fade(this.circleObjects[i]);
}
}, this.animationSpeed);
}
public stop(): void {
clearInterval(this.interval);
}
private init(): void {
this._setTargetElement();
this._setPropertiesForSize();
this._createCirclesAndArrange();
this._initializeOpacities();
this.start();
}
private _setPropertiesForSize(): void {
if (this.spinner.className.indexOf("large") > -1) {
this.parentSize = 28;
this.eightSize = 0.179;
}
this.offsetSize = this.eightSize;
this.numCircles = 8;
}
private _setTargetElement(): void {
this.spinner = this.root.getElementsByClassName(this.opts.styles['ms-Spinner'])[0] as HTMLElement;
}
private _initializeOpacities(): void {
let i = 0;
let j = 1;
let opacity;
this.fadeIncrement = 1 / this.numCircles;
for (i; i < this.numCircles; i++) {
let circleObject = this.circleObjects[i];
opacity = (this.fadeIncrement * j++);
this._setOpacity(circleObject.element, opacity);
}
}
private _fade(circleObject: CircleObject): void {
let opacity = this._getOpacity(circleObject.element) - this.fadeIncrement;
if (opacity <= 0) {
opacity = 1;
}
this._setOpacity(circleObject.element, opacity);
}
private _getOpacity(element: HTMLElement): number {
return parseFloat(window.getComputedStyle(element).getPropertyValue("opacity"));
}
private _setOpacity(element: HTMLElement, opacity: number): void {
element.style.opacity = opacity.toString();
}
private _createCircle(): HTMLElement {
let circle = document.createElement("div");
circle.className = this.opts.styles['ms-Spinner-circle'];
circle.style.width = circle.style.height = this.parentSize * this.offsetSize + "px";
return circle;
}
private _createCirclesAndArrange(): void {
let angle = 0;
let offset = this.parentSize * this.offsetSize;
let step = (2 * Math.PI) / this.numCircles;
let i = this.numCircles;
let circleObject;
let radius = (this.parentSize - offset) * 0.5;
while (i--) {
let circle = this._createCircle();
let x = Math.round(this.parentSize * 0.5 + radius * Math.cos(angle) - circle.clientWidth * 0.5) - offset * 0.5;
let y = Math.round(this.parentSize * 0.5 + radius * Math.sin(angle) - circle.clientHeight * 0.5) - offset * 0.5;
this.spinner.appendChild(circle);
circle.style.left = x + "px";
circle.style.top = y + "px";
angle += step;
circleObject = new CircleObject(circle, i);
this.circleObjects.push(circleObject);
}
}
}
class CircleObject {
public element: HTMLElement;
public j: number;
constructor(element, j) {
this.element = element;
this.j = j;
}
}

View File

@ -0,0 +1 @@
export * from './Table';

View File

@ -0,0 +1,22 @@
<office-ui-fabric-table>
<table class="{ opts.styles['ms-Table'] }">
<thead>
<tr>
<th>ID</th>
<th>Title</th>
</tr>
</thead>
<tbody>
<tr if="{ opts.isLoading }">
<td><office-ui-fabric-spinner label="Loading..." styles="{ opts.styles }"></office-ui-fabric-spinner></td>
</tr>
<tr if="{ !opts.isLoading && (!opts.items || opts.items.length == 0) }">
<td>No record found.</td>
</tr>
<tr each="{ item in opts.items }" if="{ opts.items && opts.items.length > 0 }">
<td>{ item.Id }</td>
<td>{ item.Title }</td>
</tr>
</tbody>
</table>
</office-ui-fabric-table>

View File

@ -0,0 +1,10 @@
import * as riot from "riot/riot+compiler";
import * as Riot from "./../../core/riot/Riot";
import { Spinner } from "./../Spinner/Spinner.Index";
@Riot.template(require("./Table.html") as string)
export class Table extends Riot.Element {
protected getSubComponentTypes(): any[] {
return [Spinner];
}
}

View File

@ -0,0 +1,3 @@
export interface IListViewerWebPartProps {
listId: string;
}

View File

@ -0,0 +1 @@
@import '../../office-ui-fabric-riot/Fabric.Components';

View File

@ -0,0 +1,19 @@
{
"$schema": "../../../node_modules/@microsoft/sp-module-interfaces/lib/manifestSchemas/jsonSchemas/clientSideComponentManifestSchema.json",
"id": "482dd760-ae6d-4d63-ae5a-3e28dfea1007",
"alias": "ListViewerWebPart",
"componentType": "WebPart",
"version": "0.0.1",
"manifestVersion": 2,
"preconfiguredEntries": [{
"groupId": "482dd760-ae6d-4d63-ae5a-3e28dfea1007",
"group": { "default": "Under Development" },
"title": { "default": "List Viewer" },
"description": { "default": "List Viewer of a selected SharePoint List of Document Library using RiotJS" },
"officeFabricIconFontName": "Page",
"properties": {
}
}]
}

View File

@ -0,0 +1,71 @@
import { Version } from '@microsoft/sp-core-library';
import {
IWebPartContext,
IPropertyPaneConfiguration
} from '@microsoft/sp-webpart-base';
import { PropertyFieldSPListPicker, PropertyFieldSPListPickerOrderBy } from 'sp-client-custom-fields/lib/PropertyFieldSPListPicker';
import styles from './ListViewer.module.scss';
import { IListViewerWebPartProps } from './IListViewerWebPartProps';
import { RiotClientSideWebPart } from './../../core/riot/RiotClientSideWebPart';
import { ListViewer, IListViewerProps } from './../../components/Components';
export default class ListViewerWebPart extends RiotClientSideWebPart<IListViewerWebPartProps> {
public constructor(context?: IWebPartContext) {
super();
this.onPropertyPaneFieldChanged = this.onPropertyPaneFieldChanged.bind(this);
}
protected get webPartOptions(): any {
var properties: IListViewerProps = {
styles: styles,
listId: this.properties.listId
};
return properties;
}
protected get rootComponentType(): any {
return ListViewer;
}
protected get tagName(): any {
return "list-viewer";
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: "Use this webpart to display the content of the selected list using RiotJS"
},
groups: [
{
groupName: "List Configuration",
groupFields: [
PropertyFieldSPListPicker('listId', {
label: 'Select a list',
selectedList: this.properties.listId,
includeHidden: false,
orderBy: PropertyFieldSPListPickerOrderBy.Title,
disabled: false,
onPropertyChange: this.onPropertyPaneFieldChanged.bind(this),
properties: this.properties,
context: this.context,
onGetErrorMessage: null,
deferredValidationTime: 0,
key: 'listPickerFieldId'
})
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,7 @@
define([], function() {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"DescriptionFieldLabel": "Description Field"
}
});

View File

@ -0,0 +1,10 @@
declare interface IListViewerStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
DescriptionFieldLabel: string;
}
declare module 'listViewerStrings' {
const strings: IListViewerStrings;
export = strings;
}

View File

@ -0,0 +1,9 @@
/// <reference types="mocha" />
import { assert } from 'chai';
describe('ListViewerWebPart', () => {
it('should do something', () => {
assert.ok(true);
});
});

View File

@ -0,0 +1,16 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "commonjs",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"types": [
"es6-promise",
"es6-collections",
"webpack-env"
]
}
}