Info
As you know, the well-know Office UI Fabric front-end infrastructure has been renamed as Fluent UI ! (more info here)
Recently, I had to develop a certification exam with SPFx. One of the features was to provide a countdown within the questions. As I'm familiar with React and the Fluent UI components, I was wondering if it was possible to reuse the ProgressIndicator component and set it up in reverse mode.
So here we go !
Existing sample
First, let's have a look at the sample provided here.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68 | import * as React from 'react';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
import { Async } from 'office-ui-fabric-react/lib/Utilities';
export interface IProgressIndicatorBasicExampleState {
percentComplete: number;
}
const INTERVAL_DELAY = 100;
const INTERVAL_INCREMENT = 0.01;
const RESTART_WAIT_TIME = 2000;
export class ProgressIndicatorBasicExample extends React.Component<{}, IProgressIndicatorBasicExampleState> {
private _interval: number;
private _async: Async;
constructor(props: {}) {
super(props);
this._async = new Async(this);
this.state = {
percentComplete: 0,
};
this._startProgressDemo = this._startProgressDemo.bind(this);
}
public componentDidMount(): void {
this._startProgressDemo();
}
public componentWillUnmount(): void {
this._async.dispose();
}
public render(): JSX.Element {
const { percentComplete } = this.state;
return (
<ProgressIndicator
label="Example title"
description="Example description"
percentComplete={percentComplete} />
);
}
private _startProgressDemo(): void {
// reset the demo
this.setState({
percentComplete: 0,
});
// update progress
this._interval = this._async.setInterval(() => {
let percentComplete = this.state.percentComplete + INTERVAL_INCREMENT;
// once complete, set the demo to start again
if (percentComplete >= 1.0) {
percentComplete = 1.0;
this._async.clearInterval(this._interval);
this._async.setTimeout(this._startProgressDemo, RESTART_WAIT_TIME);
}
this.setState({
percentComplete: percentComplete,
});
}, INTERVAL_DELAY);
}
}
|
This code gives this behavior:
Countdown mode
Now, let's reverse the process. Let's say that you want your countdown set to 30 seconds. You can replace the const variable INTERVAL_INCREMENT
(which won't be used anymore) by COUNTDOWN_DURATION
. For this demo, the duration will be in milliseconds.
1
2
3
4
5
6
7
8
9
10
11
12 | import * as React from 'react';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
import { Async } from 'office-ui-fabric-react/lib/Utilities';
export interface IProgressIndicatorBasicExampleState {
percentComplete: number;
}
const INTERVAL_DELAY = 100;
const COUNTDOWN_DURATION = 30000;
const RESTART_WAIT_TIME = 2000;
//...
|
Then, you can set the percentComplete
state to 1 in the constructor (remember that this variable is used as a parameter of the component, which goes from 0 to 1). So that the initial state of the Indicator is 1.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | //...
export class ProgressIndicatorBasicExample extends React.Component<{}, IProgressIndicatorBasicExampleState> {
private _interval: number;
private _async: Async;
constructor(props: {}) {
super(props);
this._async = new Async(this);
this.state = {
percentComplete: 1, // --> initial state
};
this._startProgressDemo = this._startProgressDemo.bind(this);
}
//...
}
|
In the method called when the component is mounted, make the same update.
| //...
private _startProgressDemo(): void {
// reset the demo
this.setState({
percentComplete: 1,
});
//...
}
|
Now comes the tricky part. When you want to properly decrease the progress indicator, you have to calculate the value to decrease from 1 (the initial state, remember ?). This depends on the delay of the countdown. You can add this variable as a private member of the component.
| //...
export class ProgressIndicatorBasicExample extends React.Component<{}, IProgressIndicatorBasicExampleState> {
private _interval: number;
private _async: Async;
// value = [initial state of ProgressIndicator] / [countdown duration in milliseconds] / [interval of refresh in milliseconds]
private _intervalDecrement: number = 1 / COUNTDOWN_DURATION / INTERVAL_DELAY;
//...
}
|
We want now to calculate the remaining time of the countdown, to refresh the progress indicator component.
1
2
3
4
5
6
7
8
9
10
11
12
13
14 | //...
this._interval = this._async.setInterval(() => {
let percentComplete = this.state.percentComplete - this._intervalDecrement;
// once 0 reached, set the demo to start again
if (percentComplete <= 0) {
this._async.clearInterval(this._interval);
this._async.setTimeout(this._startProgressDemo, RESTART_WAIT_TIME);
}
this.setState({
percentComplete: percentComplete,
});
}, INTERVAL_DELAY)
|
Updated code
So here is the complete updated code.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69 | import * as React from 'react';
import { ProgressIndicator } from 'office-ui-fabric-react/lib/ProgressIndicator';
import { Async } from 'office-ui-fabric-react/lib/Utilities';
export interface IProgressIndicatorBasicExampleState {
percentComplete: number;
}
const INTERVAL_DELAY = 100;
const COUNTDOWN_DURATION = 30000;
const RESTART_WAIT_TIME = 2000;
export class ProgressIndicatorBasicExample extends React.Component<{}, IProgressIndicatorBasicExampleState> {
private _interval: number;
private _async: Async;
// value = [initial state of ProgressIndicator] / ([countdown duration in milliseconds] / [interval of refresh in milliseconds])
private _intervalDecrement: number = 1 / (COUNTDOWN_DURATION / INTERVAL_DELAY);
constructor(props: {}) {
super(props);
this._async = new Async(this);
this.state = {
percentComplete: 1, // --> initial state
};
this._startProgressDemo = this._startProgressDemo.bind(this);
}
public componentDidMount(): void {
this._startProgressDemo();
}
public componentWillUnmount(): void {
this._async.dispose();
}
public render(): JSX.Element {
const { percentComplete } = this.state;
return (
<ProgressIndicator
label="Example title"
description="Example description"
percentComplete={percentComplete} />
);
}
private _startProgressDemo(): void {
// reset the demo
this.setState({
percentComplete: 1,
});
this._interval = this._async.setInterval(() => {
let percentComplete = this.state.percentComplete - this._intervalDecrement;
// once 0 reached, set the demo to start again
if (percentComplete <= 0) {
this._async.clearInterval(this._interval);
this._async.setTimeout(this._startProgressDemo, RESTART_WAIT_TIME);
}
this.setState({
percentComplete: percentComplete,
});
}, INTERVAL_DELAY)
}
}
|
And that's it! You can find the Codepen example here.
Furthermore
If you want this control to be reset (for example from a question to another in a session exam), you can turn it into a fully uncontrolled component using a key
!
You can also add some text to display the remaining time like this:
1
2
3
4
5
6
7
8
9
10
11
12 | //...
constructor(props) {
super(props);
this._async = new Async(this);
this.state = {
percentComplete: 1,
timeLeft: COUNTDOWN_DURATION, // --> init timeleft to 30 seconds
};
}
//...
|
| public render(): JSX.Element {
const { percentComplete } = this.state;
return (
<ProgressIndicator
label={`Remaining time : ${this.state.timeLeft > 0 ? Math.floor(this.state.timeLeft / 1000) : 0} seconds`}
percentComplete={percentComplete} />
);
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24 | //...
private _startProgressDemo(): void {
// reset the demo
this.setState({
percentComplete: 1,
timeLeft: COUNTDOWN_DURATION,
});
this._interval = this._async.setInterval(() => {
let percentComplete = this.state.percentComplete - this._intervalDecrement;
let remainingTime = this.state.timeLeft - INTERVAL_DELAY; // --> update the remaining time
// once 0 reached, set the demo to start again
if (percentComplete <= 0) {
this._async.clearInterval(this._interval);
this._async.setTimeout(this._startProgressDemo, RESTART_WAIT_TIME);
}
this.setState({
percentComplete: percentComplete,
timeLeft: remainingTime,
});
}, INTERVAL_DELAY)
}
|
Here's the result:
You could change the color of the progress indicator component regarding the remaining time and so on... Use your imagination 😉
Happy coding!
(Thanks to Charlie B for its help)