SPFx - PropertyPane Custom Controls - page 2
In this page, I show how to create the control as a reusable module.
So let's create a new project for this control. Create a folder named numberpicker, find it it at the command line and run the command:
So let's create a new project for this control. Create a folder named numberpicker, find it it at the command line and run the command:
npm init
Fill the prompts and you have initialized the project. Now, open the created package.json file and replace it's contents:
{
"name": "spfx-number-picker",
"version": "1.0.1",
"description": "",
"main": "numberPicker.js",
"dependencies": {
"typescript": "^3.3.1"
},
"devDependencies": {
"@microsoft/sp-webpart-base": "^1.4.1",
"node-sass": "^4.11.0"
},
"scripts": {
"scss": "node-sass --output-style compressed ./scss/numberPicker.module.scss > ./css/numberPicker.css"
},
"author": "",
"license": "ISC"
}
- Now, create 2 subfolders for the styles: [css] and [scss]
- At the root of the project, create an empty .ts file. Call it numberPicker.ts.
- Create a tsconfig.json file with the following content:
{
"compileOnSave": true,
"compilerOptions": {
"target": "es5",
"module": "commonjs",
"declaration": true,
"preserveConstEnums": true,
"skipLibCheck": true,
"sourceMap": true
},
"typeRoots": [
"./node_modules/@types",
"./node_modules/@microsoft"
],
"exclude": [
"node_modules"
],
"include": [
"./"
]
}
- Now, install dependencies by running the command:
npm install
- Inside the scss folder, create a file called numberPicker.module.scss.
- Fill it with the following code:
$neutralLighter: '[theme:neutralLighter, default:white]';
$neutralLight: '[theme:neutralLight, default:lightgray]';
$neutralDark: '[theme:neutralDark, default:darkgray]';
$black: '[theme:black, default:black]';
$white: '[theme:white, default:white]';
$errorText: '[theme:errorText, default:red]';
.number-picker{
margin: 10px 0;
position: relative;
height: auto;
.wrapper{
position: relative;
margin: 5px 0;
height:31px;
width:100%;
}
.picker{
margin: 0;
position: absolute;
top: 0;
height: auto;
width: 50px;
}
.apply-wrapper{
width: 50%;
height:auto;
position: absolute;
top: 0;
left: 80px;
}
.apply-wrapper button{
font-weight: 600;
width:85px;
height:34px;
font-size:12px;
}
.wrapper .picker button{
margin:0;
position:absolute;
width:25px;
height:17px;
display:block;
}
.wrapper button,
.wrapper button:active,
.wrapper button:focus{
cursor:pointer;
border:0;
outline:0;
background: $neutralLighter;
color:$black;
&:disabled{
background: $neutralLighter;
color:$neutralLight;
}
}
.wrapper button:not([disabled]):hover{
background:$neutralLight;
}
.wrapper .up-arrow{
top:0;
right:-25px;
}
.wrapper .down-arrow{
bottom:0;
right:-25px;
}
.wrapper input{
border:0;
color:$black;
background:$white;
font-size:16px;
text-align:center;
width:100%;
height:30px;
border: 1px solid;
border-color: $neutralLight;
&:focus{
outline:0;
}
}
.description{
font-size: 11px;
color:$neutralDark;
}
.error-msg{
font-size: 11px;
color:$errorText;
display: none;
}
}
/* END OF NUMBER PICKER */
- Compile the .scss file with the command:
npm run scss
This will compile the .scss on a .css file.
Note: Everytime you change the .scss file you need to compile it again.
You should see the compiled .css file in the [css] directory. This is the stylesheet we will require on the .ts file.
- Now, paste the following code to the empty numberPicker.ts file
import { IPropertyPaneField, IPropertyPaneCustomFieldProps, PropertyPaneFieldType } from '@microsoft/sp-webpart-base';
require ('./css/numberPicker.css');
/**
* Public interface that exposes this control's custom properties
* @property {object} properties - The WebPart's properties Object (this.properties).
* @property {string} label - A label for this control.
* @property {string} [description] - A description for this control.
* @property {number} min - The minimum value this control allows.
* @property {number} max - The maximum value this control allows.
* @property {boolean} applyOnClick - Validate and apply changes on a button click (only useful for ReactivePropertyChanges mode).
* @property {number} deferredValidationTime - TimeOut in milisseconds to validate and apply changes (only useful for ReactivePropertyChanges mode)
*/
export interface NumberPickerFieldProps {
properties:any;
label:string;
description?:string;
min: number;
max: number;
applyOnClick?: boolean;
deferredValidationTime? : number;
}
/**
* The public NumberPicker Object
*/
export class NumberPicker implements IPropertyPaneField<IPropertyPaneCustomFieldProps>{
type: any = PropertyPaneFieldType.Custom;
targetProperty:string;
properties: IPropertyPaneCustomFieldProps;
shouldFocus:boolean;
private config : NumberPickerFieldProps;
private timeOut : any;
constructor(targetProperty:string, config : NumberPickerFieldProps, context?:any, shouldFocus?:boolean){
this.targetProperty = targetProperty;
this.shouldFocus = shouldFocus;
this.properties = {
key: "myNumberPicker",
context: context,
onRender : this.render.bind(this),
onDispose : this.dispose.bind(this)
}
this.config = config;
}
/**
* Drawing of the control
* @param element the element of the PropertyPane where this control will be drawn
* @param context the webpart context (if we passe it to the NumberPicker Object)
* @param changeCallback the callbakk to apply the property changes
*/
private render(element:HTMLElement, context:any, changeCallback:(targetProperty:string, newValue:any)=>void){
let currentValue = this.config.properties[this.targetProperty] || this.config.min;
let html =
`<div class="number-picker">
<label class="ms-Label">${this.config.label}</label>
<div class="wrapper">
<div class="picker">
<input type="text" value="${currentValue}"></input>
<button type="button" class="up-arrow">
<i class="ms-Icon ms-Icon--CaretSolidUp"></i>
</button>
<button type="button" class="down-arrow">
<i class="ms-Icon ms-Icon--CaretSolidDown"></i>
</button>
</div>
`;
if (this.config.applyOnClick) {
html += `
<div class="apply-wrapper">
<button type="button" class="apply-btn">Apply</button>
</div>`;
}
html +=`
</div>
<span class="error-msg"></span>
<span class="description">${this.config.description||""}</span>
</div>`;
element.innerHTML = html;
let arrowUpbutton : HTMLButtonElement = <HTMLButtonElement>element.getElementsByClassName('up-arrow')[0];
let arrowDownbutton : HTMLButtonElement = <HTMLButtonElement>element.getElementsByClassName('down-arrow')[0];
if (currentValue-1<this.config.min){
arrowDownbutton.disabled = true;
}
if (currentValue+1>this.config.max){
arrowUpbutton.disabled = true;
}
this.addEvents(element, changeCallback);
}
private addEvents(element:HTMLElement, callback:(targetProperty:string, newValue:any)=>void ){
let inputTextElement : HTMLInputElement = element.getElementsByTagName('input')[0];
let arrowUpbutton : HTMLButtonElement = <HTMLButtonElement>element.getElementsByClassName('up-arrow')[0];
let arrowDownbutton : HTMLButtonElement = <HTMLButtonElement>element.getElementsByClassName('down-arrow')[0];
let applyButton : HTMLButtonElement = <HTMLButtonElement>element.getElementsByClassName('apply-btn')[0];
arrowUpbutton.onclick=()=>{
let oldValue = parseInt(inputTextElement.value);
if (isNaN(oldValue)) oldValue = this.config.min;
if (oldValue+1 <= this.config.max){
inputTextElement.value = "" + (oldValue + 1);
arrowDownbutton.disabled = false;
if (oldValue+1 == this.config.max) arrowUpbutton.disabled = true;
if (!this.config.applyOnClick) this.applyChanges(element, inputTextElement, callback);
}else{
arrowUpbutton.disabled = true;
}
}
arrowDownbutton.onclick=()=>{
let oldValue = parseInt(inputTextElement.value);
if (isNaN(oldValue)) oldValue = this.config.min + 1;
if (oldValue-1 >= this.config.min){
inputTextElement.value = "" + (oldValue - 1);
arrowUpbutton.disabled = false;
if (oldValue-1 == this.config.min) arrowDownbutton.disabled = true;
if (!this.config.applyOnClick) this.applyChanges(element, inputTextElement, callback);
}else{
arrowDownbutton.disabled = true;
}
}
if (this.config.applyOnClick){
applyButton.onclick=()=>{
this.applyChanges(element, inputTextElement, callback);
}
}else{
inputTextElement.oninput=()=>{
this.applyChanges(element, inputTextElement, callback);
}
}
}
private applyChanges(element: HTMLElement, inputTextElement : HTMLInputElement, callback:(targetProperty:string, newValue:any)=>void ){
let newValue = parseInt(inputTextElement.value);
if (!newValue || isNaN(newValue)) newValue = this.config.min;
let errorSpan : HTMLSpanElement = <HTMLSpanElement> element.getElementsByClassName('error-msg')[0];
let time = this.config.deferredValidationTime || 0;
if (time < 0 || this.config.applyOnClick) time = 0;
clearTimeout(this.timeOut);
this.timeOut = setTimeout(()=>{
if (this.validateValue(newValue)) {
errorSpan.style.display = 'none';
callback(this.targetProperty, newValue);
}else{
this.renderValidationError(errorSpan);
}
}, time);
}
private validateValue(value:number){
return value >= this.config.min && value <= this.config.max;
}
private renderValidationError(errorSpan : HTMLSpanElement){
errorSpan.textContent = `Invalid number: min:${this.config.min}, max:${this.config.max}`;
errorSpan.style.display = 'block';
}
private dispose(element:HTMLElement){
element.innerHTML="";
}
}
- Finally, compile the module, with the command:
tsc
The module is ready!
Note: Everytime you change the numberPicker.ts file, you need to run the "tsc" command again, to compile the module.
Comments
Post a Comment