SPFx React Accordion sample (#599)

* React Accordion Webpart added

* Updated to imgur image url
This commit is contained in:
Gautam Sheth 2018-09-10 15:36:51 +05:30 committed by Vesa Juvonen
parent 53d14f2898
commit 4a47af2d14
26 changed files with 837 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

32
samples/react-accordion/.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,11 @@
{
"@microsoft/generator-sharepoint": {
"isCreatingSolution": true,
"environment": "spo",
"version": "1.5.1",
"libraryName": "react-accordion",
"libraryId": "6d6cf05b-cfe5-4d12-af19-19ec3aedcaf9",
"packageManager": "npm",
"componentType": "webpart"
}
}

View File

@ -0,0 +1,72 @@
## Using React Accordion plugin with SPFx
## Summary
This is a sample web Part that illustrates the use of React Accessible Accordion plugin for building SharePoint Framework client-side web parts to show SharePoint list data in Accordion format.
![Sample Web Part built using SPFx with React Framework showing list data in accordion format](https://i.stack.imgur.com/QsZ6o.png)
## Used SharePoint Framework Version
![drop](https://img.shields.io/badge/drop-1.5.1-green.svg)
## Applies to
* [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)
## Solution
Solution|Author(s)
--------|---------
react-accordion | Gautam Sheth (SharePoint Consultant, RapidCircle)
## Version history
Version|Date|Comments
-------|----|--------
1.0|August 17, 2018|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 repo
- in the command line run:
- `npm i`
- `gulp serve --nobrowser`
- in your SharePoint Site create a custom list named FAQ
- in the FAQ list, create a column Description(internal name) of type Enhanced rich text
- add some list items with Title and Description values
- navigate to the hosted version of SharePoint workbench, eg. **https://contoso.sharepoint.com/sites/test/_layouts/15/workbench.aspx**
- add the Web Part to canvas and in its configuration specify:
- name of the list where list items are stored, eg. **FAQ**
## Features
This project contains sample client-side web part built on the SharePoint Framework illustrating how to show list data in Accordion format using React framework.
This sample illustrates the following concepts on top of the SharePoint Framework:
- general
- performing SharePoint GET operation in React using inbuilt SP Http Client
- Using Fabric UI button component for pagination
- optimizing REST responses for performance using nometadata option of JSON light
- using PnP Webpart title control of @pnp/spfx-controls-react library
- showing SharePoint list data in Accordion format using React Accessible Accordion plugin
- searching in the fetched data by making use of Search Box from Office Fabric UI
<img src="https://telemetry.sharepointpnp.com/sp-dev-fx-webparts/samples/react-accordion" />
### Build options
gulp clean - TODO
gulp test - TODO
gulp serve - TODO
gulp bundle - TODO
gulp package-solution - TODO

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

View File

@ -0,0 +1,19 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/config.2.0.schema.json",
"version": "2.0",
"bundles": {
"react-accordion-web-part": {
"components": [
{
"entrypoint": "./lib/webparts/reactAccordion/ReactAccordionWebPart.js",
"manifest": "./src/webparts/reactAccordion/ReactAccordionWebPart.manifest.json"
}
]
}
},
"externals": {},
"localizedResources": {
"ReactAccordionWebPartStrings": "lib/webparts/reactAccordion/loc/{locale}.js",
"ControlStrings": "node_modules/@pnp/spfx-controls-react/lib/loc/{locale}.js"
}
}

View File

@ -0,0 +1,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/copy-assets.schema.json",
"deployCdnPath": "temp/deploy"
}

View File

@ -0,0 +1,7 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/deploy-azure-storage.schema.json",
"workingDir": "./temp/deploy/",
"account": "<!-- STORAGE ACCOUNT NAME -->",
"container": "react-accordion",
"accessKey": "<!-- ACCESS KEY -->"
}

View File

@ -0,0 +1,12 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/package-solution.schema.json",
"solution": {
"name": "react-accordion-client-side-solution",
"id": "6d6cf05b-cfe5-4d12-af19-19ec3aedcaf9",
"version": "1.0.0.0",
"includeClientSideAssets": true
},
"paths": {
"zippedPackage": "solution/react-accordion.sppkg"
}
}

View File

@ -0,0 +1,10 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/serve.schema.json",
"port": 4321,
"https": true,
"initialPage": "https://localhost:5432/workbench",
"api": {
"port": 5432,
"entryPath": "node_modules/@microsoft/sp-webpart-workbench/lib/api/"
}
}

View File

@ -0,0 +1,45 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/core-build/tslint.schema.json",
// 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-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,4 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx-build/write-manifests.schema.json",
"cdnBasePath": "<!-- PATH TO CDN -->"
}

7
samples/react-accordion/gulpfile.js vendored Normal file
View File

@ -0,0 +1,7 @@
'use strict';
const gulp = require('gulp');
const build = require('@microsoft/sp-build-web');
build.addSuppression(`Warning - [sass] The local CSS class 'ms-Grid' is not camelCase and will not be type-safe.`);
build.initialize(gulp);

View File

@ -0,0 +1,36 @@
{
"name": "react-accordion",
"version": "0.0.1",
"private": true,
"engines": {
"node": ">=0.10.0"
},
"scripts": {
"build": "gulp bundle",
"clean": "gulp clean",
"test": "gulp test"
},
"dependencies": {
"@microsoft/sp-core-library": "1.5.1",
"@microsoft/sp-lodash-subset": "1.5.1",
"@microsoft/sp-office-ui-fabric-core": "1.5.1",
"@microsoft/sp-webpart-base": "1.5.1",
"@pnp/spfx-controls-react": "1.7.0",
"@types/es6-promise": "0.0.33",
"@types/react": "15.6.6",
"@types/react-dom": "15.5.6",
"@types/webpack-env": "1.13.1",
"react": "15.6.2",
"react-accessible-accordion": "1.0.2",
"react-dom": "15.6.2"
},
"devDependencies": {
"@microsoft/sp-build-web": "1.5.1",
"@microsoft/sp-module-interfaces": "1.5.1",
"@microsoft/sp-webpart-workbench": "1.5.1",
"gulp": "~3.9.1",
"@types/chai": "3.4.34",
"@types/mocha": "2.2.38",
"ajv": "~5.2.2"
}
}

View File

@ -0,0 +1,33 @@
{
"$schema": "https://developer.microsoft.com/json-schemas/spfx/client-side-web-part-manifest.schema.json",
"id": "97a28c00-64ee-4ec7-b373-723e39069a96",
"alias": "ReactAccordionWebPart",
"componentType": "WebPart",
// The "*" signifies that the version should be taken from the package.json
"version": "*",
"manifestVersion": 2,
// If true, the component can only be installed on sites where Custom Script is allowed.
// Components that allow authors to embed arbitrary script code should set this to true.
// https://support.office.com/en-us/article/Turn-scripting-capabilities-on-or-off-1f2c515f-5d7e-448a-9fd7-835da935584f
"requiresCustomScript": false,
"preconfiguredEntries": [
{
"groupId": "5c03119e-3074-46fd-976b-c60198311f70", // Other
"group": {
"default": "Other"
},
"title": {
"default": "React Accordion App"
},
"description": {
"default": "SPFx webpart which shows SharePoint list data in Accordion format"
},
"officeFabricIconFontName": "Questionnaire",
"properties": {
"description": "SPFx webpart which shows SharePoint list data in Accordion format",
"listName": "FAQ",
"maxItemsPerPage": 5
}
}
]
}

View File

@ -0,0 +1,83 @@
import * as React from 'react';
import * as ReactDom from 'react-dom';
import { Version, DisplayMode } from '@microsoft/sp-core-library';
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneSlider
} from '@microsoft/sp-webpart-base';
import * as strings from 'ReactAccordionWebPartStrings';
import ReactAccordion from './components/ReactAccordion';
import { IReactAccordionProps } from './components/IReactAccordionProps';
export interface IReactAccordionWebPartProps {
listName: string;
choice: string;
title: string;
displayMode: DisplayMode;
maxItemsPerPage: number;
updateProperty: (value: string) => void;
}
export default class ReactAccordionWebPart extends BaseClientSideWebPart<IReactAccordionWebPartProps> {
public render(): void {
const element: React.ReactElement<IReactAccordionProps> = React.createElement(
ReactAccordion,
{
listName: this.properties.listName,
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
title: this.properties.title,
displayMode: this.displayMode,
maxItemsPerPage: this.properties.maxItemsPerPage,
updateProperty: (value: string) => {
this.properties.title = value;
}
}
);
ReactDom.render(element, this.domElement);
}
protected onDispose(): void {
ReactDom.unmountComponentAtNode(this.domElement);
}
protected get dataVersion(): Version {
return Version.parse('1.0');
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
groupFields: [
PropertyPaneTextField('listName', {
label: strings.ListNameLabel
}),
PropertyPaneSlider('maxItemsPerPage', {
label: strings.MaxItemsPerPageLabel,
ariaLabel: strings.MaxItemsPerPageLabel,
min: 3,
max: 20,
value: 5,
showValue: true,
step: 1
}),
]
}
]
}
]
};
}
}

View File

@ -0,0 +1,12 @@
import { SPHttpClient } from '@microsoft/sp-http';
import { DisplayMode } from '@microsoft/sp-core-library';
export interface IReactAccordionProps {
listName: string;
spHttpClient: SPHttpClient;
siteUrl: string;
title: string,
displayMode: DisplayMode,
maxItemsPerPage: number,
updateProperty: (value: string) => void;
}

View File

@ -0,0 +1,9 @@
import IAccordionListItem from '../models/IAccordionListItem';
export interface IReactAccordionState {
status: string;
items: IAccordionListItem[];
listItems: IAccordionListItem[];
isLoading: boolean;
loaderMessage: string;
}

View File

@ -0,0 +1,73 @@
@import '~@microsoft/sp-office-ui-fabric-core/dist/sass/SPFabricCore.scss';
.reactAccordion {
.container {
max-width: 100%;
margin: 0px auto;
}
.row {
@include ms-Grid-row;
@include ms-fontColor-white;
background-color: $ms-color-themeDark;
padding: 20px;
}
.column {
@include ms-Grid-col;
@include ms-lg10;
@include ms-xl8;
@include ms-xlPush2;
@include ms-lgPush1;
}
.title {
@include ms-font-xl;
@include ms-fontColor-white;
}
.subTitle {
@include ms-font-l;
@include ms-fontColor-white;
}
.description {
@include ms-font-l;
@include ms-fontColor-white;
}
.button {
// Our button
text-decoration: none;
height: 32px;
// Primary Button
min-width: 80px;
background-color: $ms-color-themePrimary;
border-color: $ms-color-themePrimary;
color: $ms-color-white;
// Basic Button
outline: transparent;
position: relative;
font-family: "Segoe UI WestEuropean","Segoe UI",-apple-system,BlinkMacSystemFont,Roboto,"Helvetica Neue",sans-serif;
-webkit-font-smoothing: antialiased;
font-size: $ms-font-size-m;
font-weight: $ms-font-weight-regular;
border-width: 0;
text-align: center;
cursor: pointer;
display: inline-block;
padding: 0 16px;
.label {
font-weight: $ms-font-weight-semibold;
font-size: $ms-font-size-m;
height: 32px;
line-height: 32px;
margin: 0 4px;
vertical-align: top;
display: inline-block;
}
}
}

View File

@ -0,0 +1,180 @@
import * as React from 'react';
import styles from './ReactAccordion.module.scss';
import { IReactAccordionProps } from './IReactAccordionProps';
import { SPHttpClient, SPHttpClientResponse, ISPHttpClientOptions } from '@microsoft/sp-http';
import { PrimaryButton } from 'office-ui-fabric-react/lib/Button';
import { SearchBox } from 'office-ui-fabric-react/lib/SearchBox';
import {
Spinner,
SpinnerSize
} from 'office-ui-fabric-react/lib/Spinner';
import {
Accordion,
AccordionItem,
AccordionItemTitle,
AccordionItemBody,
} from 'react-accessible-accordion';
import 'react-accessible-accordion/dist/react-accessible-accordion.css';
import { IReactAccordionState } from "./IReactAccordionState";
import IAccordionListItem from "../models/IAccordionListItem";
import { WebPartTitle } from "@pnp/spfx-controls-react/lib/WebPartTitle";
import './accordion.css';
export default class ReactAccordion extends React.Component<IReactAccordionProps, IReactAccordionState> {
constructor(props: IReactAccordionProps, state: IReactAccordionState) {
super(props);
this.state = {
status: this.listNotConfigured(this.props) ? 'Please configure list in Web Part properties' : 'Ready',
items: [],
listItems: [],
isLoading: false,
loaderMessage: ''
};
if (!this.listNotConfigured(this.props)) {
this.readItems();
}
this.searchTextChange = this.searchTextChange.bind(this);
}
private listNotConfigured(props: IReactAccordionProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
private searchTextChange(event) {
if (event === undefined ||
event === null ||
event === "") {
let listItemsCollection = [...this.state.listItems];
this.setState({ items: listItemsCollection.splice(0, this.props.maxItemsPerPage) });
}
else {
var updatedList = [...this.state.listItems];
updatedList = updatedList.filter((item) => {
return item.Title.toLowerCase().search(
event.toLowerCase()) !== -1 || item.Description.toLowerCase().search(
event.toLowerCase()) !== -1;
});
this.setState({ items: updatedList });
}
}
private readItems(): void {
let restAPI = this.props.siteUrl + `/_api/web/Lists/GetByTitle('${this.props.listName}')/items?$select=Title,Description`;
this.props.spHttpClient.get(restAPI, SPHttpClient.configurations.v1, {
headers: {
'Accept': 'application/json;odata=nometadata',
'odata-version': ''
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IAccordionListItem[] }> => {
return response.json();
})
.then((response: { value: IAccordionListItem[] }): void => {
let listItemsCollection = [...response.value];
this.setState({
status: "",
items: listItemsCollection.splice(0, this.props.maxItemsPerPage),
listItems: response.value,
isLoading: false,
loaderMessage: ""
});
}, (error: any): void => {
this.setState({
status: 'Loading all items failed with error: ' + error,
items: [],
isLoading: false,
loaderMessage: ""
});
});
}
public render(): React.ReactElement<IReactAccordionProps> {
let displayLoader;
let faqTitle;
let { listItems } = this.state;
let pageCountDivisor: number = this.props.maxItemsPerPage;
let pageCount: number;
let pageButtons = [];
let _pagedButtonClick = (pageNumber: number, listData: any) => {
let startIndex: number = (pageNumber - 1) * pageCountDivisor;
let listItemsCollection = [...listData];
this.setState({ items: listItemsCollection.splice(startIndex, pageCountDivisor) });
};
const items: JSX.Element[] = this.state.items.map((item: IAccordionListItem, i: number): JSX.Element => {
return (
<AccordionItem>
<AccordionItemTitle className="accordion__title">
<h3 className="u-position-relative ms-fontColor-white">{item.Title}</h3>
<div className="accordion__arrow ms-fontColor-white" role="presentation" />
</AccordionItemTitle>
<AccordionItemBody className="accordion__body">
<div className="" dangerouslySetInnerHTML={{ __html: item.Description }}>
</div>
</AccordionItemBody>
</AccordionItem>
);
});
if (this.state.isLoading) {
displayLoader = (<div className={`ms-Grid-row ms-bgColor-themeDark ms-fontColor-white ${styles.row}`}>
<div className='ms-Grid-col ms-u-lg12'>
<Spinner size={SpinnerSize.large} label={this.state.loaderMessage} />
</div>
</div>);
}
else {
displayLoader = (null);
}
if (this.state.listItems.length > 0) {
pageCount = Math.ceil(this.state.listItems.length / pageCountDivisor);
}
for (let i = 0; i < pageCount; i++) {
pageButtons.push(<PrimaryButton onClick={() => { _pagedButtonClick(i + 1, listItems); }}> {i + 1} </PrimaryButton>);
}
return (
<div className={styles.reactAccordion}>
<div className={styles.container}>
{faqTitle}
{displayLoader}
<WebPartTitle displayMode={this.props.displayMode}
title={this.props.title}
updateProperty={this.props.updateProperty} />
<div className='ms-Grid-row'>
<div className='ms-Grid-col ms-u-lg12'>
<SearchBox
onChange={this.searchTextChange}
/>
</div>
</div>
<div className={`ms-Grid-row`}>
<div className='ms-Grid-col ms-u-lg12'>
{this.state.status}
<Accordion accordion={false}>
{items}
</Accordion>
</div>
</div>
<div className='ms-Grid-row'>
<div className='ms-Grid-col ms-u-lg12'>
{pageButtons}
</div>
</div>
</div>
</div>
);
}
}

View File

@ -0,0 +1,109 @@
.accordion__title > *:last-child,
.accordion__body > *:last-child {
margin-bottom: 0;
}
.accordion__arrow {
display: inline-block;
position: relative;
width: 24px;
height: 12px;
right: 10px;
margin-top: -28px;
color: white !important;
float: right;
}
.accordion__arrow::after,
.accordion__arrow::before {
display: block;
position: absolute;
width: 10px;
height: 2px;
background-color: currentColor;
content: '';
}
.accordion__arrow::before {
left: 4px;
transform: rotate(45deg);
}
[aria-expanded="true"] .accordion__arrow::before {
transform: rotate(-45deg);
}
.accordion__arrow::after {
right: 4px;
transform: rotate(-45deg);
}
[aria-expanded="true"] .accordion__arrow::after {
transform: rotate(45deg);
}
.accordion__arrow::before, .accordion__arrow::after {
transition: transform .25s ease, -webkit-transform .25s ease;
}
.accordion__item {
background-color: "[theme: themePrimary, default: #0078d7]";
margin-bottom: 10px;
}
.accordion {
padding-top: 10px;
}
.accordion__item .accordion__title {
padding: 5px 20px;;
background-color: "[theme: themePrimary, default: #0078d7]";
}
.accordion__item .accordion__title h3 {
font-weight: normal;
width: 88%;
}
.accordion__item .accordion__body {
padding: 15px 20px;
background-color: "[theme: themeLighterAlt, default: #0078d7]";
color: "[theme: bodyText, default: #333333]";
}
.accordion__item .accordion__body a {
color: "[theme: themePrimary, default: #0078d7]" !important;
}
.accordion__item .accordion__body p {
color: "[theme: bodyText, default: #333333]";
}
/* -------------------------------------------------- */
/* ---------------- Animation part ------------------ */
/* -------------------------------------------------- */
@keyframes move-down {
0% { transform: translateY(0); }
10% { transform: translateY(0); }
20% { transform: translateY(5px); }
30% { transform: translateY(0); }
100% { transform: translateY(0); }
}
@keyframes move-up {
0% { transform: translateY(0); }
10% { transform: translateY(0); }
20% { transform: translateY(-5px); }
30% { transform: translateY(0); }
100% { transform: translateY(0); }
}
.accordion__title--animated:hover .accordion__arrow {
animation-name: move-down;
animation-duration: 1.5s;
}
.accordion__title--animated[aria-expanded="true"]:hover .accordion__arrow {
animation-name: move-up;
animation-duration: 1.5s;
}

View File

@ -0,0 +1,8 @@
define([], function () {
return {
"PropertyPaneDescription": "Description",
"BasicGroupName": "Group Name",
"ListNameLabel": "List Name",
"MaxItemsPerPageLabel": "Max number of items per page"
}
});

View File

@ -0,0 +1,11 @@
declare interface IReactAccordionWebPartStrings {
PropertyPaneDescription: string;
BasicGroupName: string;
ListNameLabel: string;
MaxItemsPerPageLabel: string
}
declare module 'ReactAccordionWebPartStrings' {
const strings: IReactAccordionWebPartStrings;
export = strings;
}

View File

@ -0,0 +1,6 @@
interface IAccordionListItem {
Id: number;
Title: string;
Description: string;
}
export default IAccordionListItem;

View File

@ -0,0 +1,26 @@
{
"compilerOptions": {
"target": "es5",
"forceConsistentCasingInFileNames": true,
"module": "esnext",
"moduleResolution": "node",
"jsx": "react",
"declaration": true,
"sourceMap": true,
"experimentalDecorators": true,
"skipLibCheck": true,
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"types": [
"es6-promise",
"webpack-env"
],
"lib": [
"es5",
"dom",
"es2015.collection"
]
}
}

View File

@ -0,0 +1,3 @@
{
"rulesDirectory": "./config"
}