Skip to content

Commit

Permalink
feat(FEC-10076): add support for dynamic injection (#538)
Browse files Browse the repository at this point in the history
* `UIManager` Exposes `addComponent` api which actually deligates to `PlayerAreaProvider._addNewComponentAndUpdateListeners` method (using _Render-Props_ technique)
* Remove empty array condition for removal flow 
* Extract `Fragment` children since add and remove the component behaves worried when children wrapped by a `Fragment`
  • Loading branch information
yairans committed Aug 19, 2020
1 parent b767a8d commit f180721
Show file tree
Hide file tree
Showing 7 changed files with 393 additions and 41 deletions.
24 changes: 24 additions & 0 deletions docs/ui-components.md
Original file line number Diff line number Diff line change
Expand Up @@ -177,6 +177,30 @@ export class MyCustomPlugin extends KalturaPlayer.core.BasePlugin {
}
```

## Injecting and removing a UI component dynamically

The `UiManager` exposes an api `addComponent` to add a UI component dynamically.
This method returns a function for removing the injected component.

The UI component declaration is the same as describe above.

```javascript
const removeFunc = uiManager.addComponent({
label: 'niceComponent',
presets: ['Playback', 'Live'],
container: 'BottomBarRightControls',
get: customComponent,
beforeComponent: '',
afterComponent: '',
replaceComponent: 'Volume',
props: {myProp: true}
});
...
...
...
removeFunc(); // remove customComponent ('Volume' will back to its place)
```

### Useful tips

1. The player will add your component only once matching a relevant preset and container. If you fail to see your components review again the configuration and make sure the preset and container names are correct.
Expand Down
1 change: 1 addition & 0 deletions src/components/player-area/player-area-provider.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ class PlayerAreaProvider extends Component {
* @return {void}
*/
componentDidMount() {
this.props.setApi(this._addNewComponentAndUpdateListeners);
this._initializePlayerComponents();
}

Expand Down
23 changes: 16 additions & 7 deletions src/components/player-area/player-area.js
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ class PlayerArea extends Component {
};

_unregisterListenerCallback: ?Function;
_actualChildren: Array<any>;

/**
* should component update handler
Expand Down Expand Up @@ -118,9 +119,6 @@ class PlayerArea extends Component {
* @private
*/
_updateAreaComponents = (playerAreaComponents: Array<Object>): void => {
if (playerAreaComponents.length < 1) {
return;
}
const {activePresetName, name: playerAreaName} = this.props;

this.props.logger.debug(`Player area '${playerAreaName}' in preset '${activePresetName}' - update children components`);
Expand Down Expand Up @@ -163,6 +161,17 @@ class PlayerArea extends Component {
this.props.logger.debug(`Player area '${this.props.name}' - handle did mount`);
this.setState(initialState);
this._registerListener();
this._actualChildren = [];
}

/**
* component will update
* @param {Object} nextProps - the next props
* @return {void}
*/
componentWillUpdate(nextProps: Object): void {
const {children} = nextProps;
this._actualChildren = children && children.type === Fragment ? children.props.children : children;
}

/**
Expand Down Expand Up @@ -250,12 +259,12 @@ class PlayerArea extends Component {
* @memberof PlayerArea
*/
render(): React$Element<any> | null {
const {children, show, name} = this.props;
const {show, name} = this.props;
const {playerAreaComponents, hasPositionedComponents, presetComponentsOnlyMode} = this.state;
this.props.logger.debug(`Player area '${name}' - render`);

if (presetComponentsOnlyMode) {
return this.renderContent(this.props.children);
return this.renderContent(this._actualChildren);
}

if (!playerAreaComponents || !show) {
Expand All @@ -265,9 +274,9 @@ class PlayerArea extends Component {
let newChildren = [];

if (hasPositionedComponents) {
newChildren = this._getPositionedComponents(children);
newChildren = this._getPositionedComponents(this._actualChildren);
} else {
newChildren.push(...toChildArray(children));
newChildren.push(...toChildArray(this._actualChildren));
}

const appendedChildren = playerAreaComponents.appendedComponents.map(component => {
Expand Down
21 changes: 20 additions & 1 deletion src/ui-manager.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
//@flow
/* eslint-disable no-unused-vars */
import {h, render} from 'preact';
import {Provider} from 'react-redux';
import {IntlProvider} from 'preact-i18n';
Expand Down Expand Up @@ -41,6 +42,7 @@ class UIManager {
_translations: {[langKey: string]: Object} = {en: en_translations['en']};
_locale: string = 'en';
_uiComponents: Array<KPUIComponent>;
addComponent: (component: KPUIComponent) => Function;

/**
* Creates an instance of UIManager.
Expand Down Expand Up @@ -90,6 +92,17 @@ class UIManager {
}
}

/**
* Add a component dynamically
*
* @param {KPUIComponent} component - The component to add
* @returns {Function} - Removal function
* @memberof UIManager
*/
addComponent(component: KPUIComponent): Function {
return () => {};
}

/**
* build default UIs
*
Expand Down Expand Up @@ -167,7 +180,13 @@ class UIManager {
// i18n, redux and initial player-to-store connector setup
const template = (
<Provider store={this.store}>
<PlayerAreaProvider uiComponents={this._uiComponents}>
<PlayerAreaProvider
uiComponents={this._uiComponents}
setApi={api => {
if (api) {
this.addComponent = api;
}
}}>
<IntlProvider definition={this._translations[this._locale]}>
<PlayerProvider player={this.player}>
<EventDispatcherProvider player={this.player} store={this.store}>
Expand Down
183 changes: 181 additions & 2 deletions test/src/ui-manager.spec.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,184 @@
import {setup} from 'kaltura-player-js';
import * as TestUtils from './utils/test-utils';
import {h, Component} from 'preact';

describe('UIManager', function () {
it('', function (done) {
done();
const targetId = 'player-placeholder_ui.spec';
class customComponent extends Component {
render() {
return <div className={'custom-component'} />;
}
}

let player;
const config = {
targetId,
provider: {}
};

before(function () {
TestUtils.createElement('DIV', targetId);
});

beforeEach(function () {
player = setup(config);
});

afterEach(function () {
player.destroy();
});

after(function () {
TestUtils.removeVideoElementsFromTestPage();
});

describe('addComponent', function () {
it('Should exist but do nothing before ui built', function (done) {
try {
const func = player.ui.addComponent();
(typeof func).should.equals('function');
done();
} catch (e) {
done(e);
}
});

it('Should add a component in BottomBarLeftControls', function (done) {
const removeFunc = player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarRightControls',
get: customComponent
});
try {
(typeof removeFunc).should.equals('function');
setTimeout(() => {
const rightControls = document.querySelector('.playkit-bottom-bar .playkit-right-controls');
rightControls.lastElementChild.className.should.equals('custom-component');
done();
}, 0);
} catch (e) {
done(e);
}
});

it('Should remove the injected component', function (done) {
let rightControls;
const removeFunc = player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarRightControls',
get: customComponent
});
setTimeout(() => {
try {
rightControls = document.querySelector('.playkit-bottom-bar .playkit-right-controls');
rightControls.lastElementChild.className.should.equals('custom-component');
removeFunc();
setTimeout(() => {
rightControls.lastElementChild.className.should.not.equals('custom-component');
done();
}, 0);
} catch (e) {
done(e);
}
}, 0);
});

it('Should add a component before other', function (done) {
let leftControls;
player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarLeftControls',
beforeComponent: 'Rewind',
get: customComponent
});
setTimeout(() => {
try {
leftControls = document.querySelector('.playkit-bottom-bar .playkit-left-controls');
const rewindIndex = Array.from(leftControls.children).findIndex(
child => child.className === 'playkit-control-button-container playkit-no-idle-control'
);
leftControls.children[rewindIndex - 1].className.should.equals('custom-component');
done();
} catch (e) {
done(e);
}
}, 0);
});

it('Should add a component after other', function (done) {
let leftControls;
player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarLeftControls',
afterComponent: 'Rewind',
get: customComponent
});
setTimeout(() => {
try {
leftControls = document.querySelector('.playkit-bottom-bar .playkit-left-controls');
const rewindIndex = Array.from(leftControls.children).findIndex(
child => child.className === 'playkit-control-button-container playkit-no-idle-control'
);
leftControls.children[rewindIndex + 1].className.should.equals('custom-component');
done();
} catch (e) {
done(e);
}
}, 0);
});

it('Should replace a component', function (done) {
let leftControls;
player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarLeftControls',
replaceComponent: 'Rewind',
get: customComponent
});
setTimeout(() => {
try {
leftControls = document.querySelector('.playkit-bottom-bar .playkit-left-controls');
const forwardIndex = Array.from(leftControls.children).findIndex(
child => child.className === 'playkit-control-button-container playkit-no-idle-control'
);
leftControls.children[forwardIndex - 1].className.should.equals('custom-component');
done();
} catch (e) {
done(e);
}
}, 0);
});

it('Should remove the injected component and re-append the replaced one', function (done) {
let leftControls;
const removeFunc = player.ui.addComponent({
label: 'customComponent',
presets: ['Playback'],
container: 'BottomBarLeftControls',
replaceComponent: 'Rewind',
get: customComponent
});
setTimeout(() => {
try {
leftControls = document.querySelector('.playkit-bottom-bar .playkit-left-controls');
const forwardIndex = Array.from(leftControls.children).findIndex(
child => child.className === 'playkit-control-button-container playkit-no-idle-control'
);
leftControls.children[forwardIndex - 1].className.should.equals('custom-component');
removeFunc();
setTimeout(() => {
leftControls.children[forwardIndex - 1].className.should.equals('playkit-control-button-container playkit-no-idle-control');
done();
});
} catch (e) {
done(e);
}
}, 0);
});
});
});

0 comments on commit f180721

Please sign in to comment.