Technologies Web synchrones et multi-dispositifs
This project is maintained by aurelient
L’objectif du TP est de mettre en place une Single Page Application (SPA) permettant de créer et controler des présentations. Elle sera développée principalement côté client avec React, avec un serveur Node/Express léger. Client et serveur seront codés en JavaScript.
Les points suivants seront abordés
Ce TP s’étalera sur 4 séances et fera l’objet d’un rendu en binome et d’une note. Voir les critères d’évaluation en bas de la page.
Vous ferez le rendu sur la forge, créez un projet git dès maintenant, puis un projet (npm init).
Pensez à remplir le formulaire de rendu.
Nous allons repartir du TP1 pour ce projet, vous pouvez donc le cloner, puis le pousser dans un nouveau repo dédié au TP2 (pour les 4 séances du TP).
Si vous n’avez pas utilisé react-bootstrap dans le TP précédent. Installez le.
Lire l’introduction à la structuration d’application React.
Nous allons commencer par créer un squelette d’application statique, nous rajouterons les parties dynamiques par la suite.
L’application est composée de transparents, d’outils d’édition, d’outils de navigation, et d’outils de présentations (notes, timer, …)
Les transparents auront (à minima) deux modèles:
Imaginez que le serveur envoie ce type de données (qui peuvent être améliorées/modifiées selon vos besoins) :
[
{type: 'title', title: 'TIW 8', visible: true, notes: ""},
{type: 'content', title: 'TP 1', text: "Le TP porte sur des rappels de developpement Web", visible: false, notes: "ce transparent est caché"},
{type: 'content', title: 'TP 2', text: "Le TP porte sur la creation d'un outil de presentation HTML", visible: true, notes: ""},
{type: 'content', title: 'TP 3', text: "Le TP 3", visible: true, notes: ""},
{type: 'content', title: 'TP 4', text: "Le TP 4", visible: true, notes: ""},
{type: 'title', title: 'Question ?', visible: true, notes: ""},
];
Créez la structure des composants correspondant à cette application, en suivant le guide et l’exemple de Thinking in React.
Voici une structure pour démarrer, pensez à utiliser les composants bootstrap plutôt que du html pur:
class Slide extends React.Component {
render() {
const slide = this.props.slide;
const type = slide.type ? "title" : ...
return (
<h1> {slide.title} </h1>
...
);
}
}
class Slides extends React.Component {
render() {
this.props.slides.forEach((slide) => {
...
});
return (
<div>
...
</div>
);
}
}
class Toolbar extends React.Component {
render() {
return (
<div>
... d'autres composants ...
</div>
);
}
}
class SlideShow extends React.Component {
render() {
return (
<div>
<Slides slides={this.props.slides}/>
<Toolbar slides={this.props.slides} />
</div>
);
}
}
const SLIDES = [
{type: 'title', title: 'TIW 8', visible: true, notes: ""},
{type: 'content', title: 'TP 1', text: "Le TP porte sur des rappels de developpement Web", visible: false, notes: "ce transparent est caché"},
{type: 'content', title: 'TP 2', text: "Le TP porte sur la creation d'un outil de presentation HTML", visible: true, notes: ""},
{type: 'content', title: 'TP 3', text: "Le TP 3", visible: true, notes: ""},
{type: 'content', title: 'TP 4', text: "Le TP 4", visible: true, notes: ""},
{type: 'title', title: 'Question ?', visible: true, notes: ""},
];
ReactDOM.render(
<FilterableProductTable slides={SLIDES} />,
document.getElementById('container')
);
Ce code est donné à titre indicatif. Commencez progressivement et testez régulièrement.
La toolbar doit contenir deux boutons avant/arrière pour naviguer entre les transparents. Faites en sorte que l’état du slideshow change lorsque vous pressez un bouton, et que ce changement d’état soit reflété au niveau de l’application. Pour cela il va falloir ajouter un flux inverse (faire en sorte que le bouton parle à des composants parents). Suivez les instructions et l’exemple de Thinking in React sur les “Inverse Data Flow”.
Pour terminer ce TP nous allons rajouter la gestion de routes, pour qu’il soit possible d’avoir un lien dédié pour chaque transparent. En plus d’avoir un état interne à l’application pour savoir quel transparent afficher, nous allons utiliser une route qui pointe vers le transparent en question. En chargeant cette route, l’état sera modifié.
Nous allons utiliser react-router. Pour en comprendre la logique (et les différences avec d’autres outils de routing), je vous invite à lire cette page.
React router requiert d’envelopper votre application dans un composant Router
.
En l’occurrence HashRouter
(et non BrowserRouter
qui demande une configuration côté serveur). L’idée est que charger un url de type http://monsite.net/#/3 charge le 3e transparent. Importez bien react-router-dom
non.
class components
, vous pouvez récupérer la valeur de la route en utilisant un props dédié passé par le routeur. Suivez cet exemplefunctional components
, avec le hook useParams();
vous pouvez récupérer la valeur de la route. Suivez cet exemple.Une fois la valeur de la route récupérée, modifier l’état de l’application, pour qu’il corresponde au transparent à afficher.
Nous allons maintenant gérer l’état de l’application sur plusieurs dispositifs en utilisant Redux et des Websockets. L’objectif est que vous puissiez changer l’état de votre application de présentation sur un dispositif (ex: mobile), et que l’état de l’application soit mis à jour partout (ex: vidéo-projection, personne qui regarde votre présentation à distance sur sa machine…)
Nous allons gérer l’état qui comprend la liste des transparents et le transparent en cours.
Pensez à relire le cours et les ressources associées pour être au clair sur ce que vous êtes en train de faire.
Afin de vous faciliter le debug du TP, vous pouvez activer la création d’un Source Map dans votre webpack.config.js
: devtool: 'eval-source-map'
.
Installez Redux et les dépendances associées pour React (react-redux
). Par défaut Redux n’est pas lié à React et peut être utilisé avec d’autres Framework.
Créez dans src
des dossiers pour organiser votre store
, vos reducers
, actions
, et containers
.
Nous allons commencer par créer le store qui va gérer les états.
// src/store/index.js
import { createStore } from "redux";
import rootReducer from "../reducers/index";
const store = createStore(rootReducer);
export default store;
On importe createStore
depuis redux et aussi rootReducer
, on verra plus bas la définition des reducers.
createStore
peut aussi prendre un état initial en entrée, mais c’est en React c’est les reducers qui produisent l’état de l’application (y compris l’état initial).
On crée un premier reducer qui va initialiser l’application. Le rootReducer
a un état initial constant (immutable) à compléter.
const initialState = {
index: 1, // initialise votre presentation au transparent 1
slides: [] // vous pouvez réutiliser votre état de slides initial.
};
function rootReducer(state = initialState, action) {
switch (action.type) {
case ADD_SLIDE:
return ...
case REMOVE_SLIDE:
return ...
case NEXT_SLIDE:
return ...
case PREVIOUS_SLIDE:
return ...
default:
return state
}
return state;
};
export default rootReducer;
Dans votre index.js
principal exposez le store pour pouvoir l’afficher via la console du navigateur.
Cela permettra d’effectuer les premiers tests de Redux, sans l’avoir branché à votre application React.
import store from "store/index"; // verifiez que le chemin est correct
window.store = store;
Un devtool pour Redux est disponible pour Chrome et Firefox, il necessite quelques légères modification de votre code.
Suivre le guide de Redux sur la création d’action en transposant la gestion de todos à la gestion de slides.
Vous aurez à définir les actions suivantes dans actions/index.js
export const ADD_SLIDE = "ADD_SLIDE";
export const REMOVE_SLIDE = "REMOVE_SLIDE";
export const NEXT_SLIDE = 'NEXT_SLIDE'
export const PREVIOUS_SLIDE = 'PREVIOUS_SLIDE'
Ces actions sont utilisées comme signaux par Redux, ce ne que des objects JavaScript Simle. C’est le Reducer qui va faire le travail.
Les actions forcent principalement à définir des traitement unitaire.
Une bonne pratique Redux consiste à envelopper les actions dans une fonction pour s’assurer que la création de l’objet est bien faite. Ces fonction s’appellent action creator
.
export function addSlide(payload) {
return { type: ADD_SLIDE, payload };
}
Ne le faites pas maintenant. Toutefois, quand votre application deviendra plus complexe, vous pourrez utiliser la convention Redux duck pour organiser votre code.
Toujours dans votre index.js
principal, exposez les actions pour vérifier qu’elles changent bien le contenu du store.
Redux n’est toujours pas branché à React, il est donc normal que l’interface ne change pas pour le moment. Mais vous pouvez observer l’état via l’extension Redux ou un simple console.log()
dans votre Reducer.
import { nextSlide } from "actions/index"; // verifiez que le chemin est correct
window.nextSlide = nextSlide;
Nous allons lier votre composant principal à Redux.
Pour commencer Redux
fournit un composant <Provider>
à placer à la racine de votre application.
Voir l’exemple de la documentation.
Ce provider
va rendre le store Redux disponible dans l’application.
Il faut ensuite ce connecter à ce store. Pour cela on utilise la fonction connect()
, et en définissant une fonction mapStateToProps()
qui va faire le lien entre le state Redux et les props de votre composant.
const mapStateToProps = (state) => {
return {
slides: state.slides,
}
}
... VOTRE_COMPOSANT ...
export default connect(mapStateToProps)(VOTRE_COMPOSANT)
Maintenant nous allons modifier l’état de index
en cliquant sur les boutons précédent/suivant de votre Toolbar
. Pour cela nous allons utiliser mapDispatchToProps()
qui va connecter notre application React aux actions Redux.
react-redux
, et les actions depuis votre fichier de définition d’action.
import { connect } from "react-redux";
import { action1, action2 } from "..../actions/index";
mapDispatchToProps
et le connecter avec votre composant.const mapDispatchToProps = dispatch => {
return {
nextSlide: () => dispatch(nextSlide(true)),
previousSlide: () => dispatch(previousSlide(true))
}
}
// ... VOTRE_COMPOSANT
export default withRouter(connect(null, mapDispatchToProps)(VOTRE_COMPOSANT));
onClick={() => {this.props.previousSlide}
Normalement l’intégration avec React Router se passe bien (pas de changements nécessaire). Si jamais ce n’était pas le cas, suivez l’utilisation de Redux avec React Router telle que présentée dans la documentation de React Router ou celle de Redux pour configurer votre projet.
Afin de rendre notre application disponible sur les internets, nous allons la déployer sur Heroku. Suivre le guide de Heroku pour déployer une application via git : https://devcenter.heroku.com/articles/git#creating-a-heroku-remote
N’oubliez pas de désactiver l’option watch
de webpack si vous lancez Webpack en --mode production
voir ici.
Nous allons maintenant travailler à la distribution de l’application sur plusieurs dispositifs et à leur synchronisation.
Nous allons définir une route par situations d’usage :
controler
: route pour dispositif mobile qui va controler la présentation et afficher les notes de présentation.present
: route pour le mode présentation plein écran, seule une diapositive plein écran sera affichée (pas de toolbar).edit
: mode actuel permettant l’édition des transparentsIl n’existe pas de bibliothèque à l’heure actuelle pour gérer de manière simple de la distribution d’interface, nous allons donc devoir le faire “à la main”.
Rajouter des Redirect
(doc) à la racine de votre application pour faire une redirection vers une route en fonction du dispositif utilisé et de son état.
Vous pouvez utiliser react-device-detect
(doc) pour détecter le dispositif (mobile ou non). Et la fullscreen API
(doc) pour controler le plein écran.
Déployez et tester.
Cette vue pour mobile affiche les notes de présentation associées à un transparet ainsi que les boutons suivant précédent.
Nous allons travailler sur la synchronisation entre les dispositifs ci-dessous. Pour l’instant la vue doit simplement afficher les notes correspondant au transparent courant.
Nous allons maintenant préparer la synchronisation des dispositifs. Pour cela nous allons devoir gérer le transparent courant dans notre état (currentSlide
dans le store).
ReactRouter
n’est pas conçu pour bien gérer le lien entre route et état. Et les routeur alternatifs (type connected-react-router
) ont aussi des limites. Nous allons donc gérer cette partie de la route à la main.
En écoutant l’évènement popstate
nous pouvons êtres informé d’un changement dans l’url du navigateur. Si ce changement correspond à un changement dans le numéro de transparent à afficher, nous allons déclencher l’action setSlide
, avec le numéro de transparent approprié.
// Update Redux if we navigated via browser's back/forward
// most browsers restore scroll position automatically
// as long as we make content scrolling happen on document.body
window.addEventListener('popstate', () => {
// `setSlide` is an action creator that takes
// the hash of the url and pushes to the store it.
// TODO add parsing and checks on the location.hash
// to make sure it is a proper slide index.
slideIndex = window.location.hash
store.dispatch(setSlide(slideIndex))
})
Si vous n’avez pas encore définit l’action setSlide
, créez le action creator correspondant, et le traitement associé dans le reducer.
En écoutant les changements dans le store nous allons pouvoir être notifiés de changement de l’état et les répercuter dans la barre d’url (utile pour la suite, quand nous allons synchroniser des dispositifs):
// The other part of the two-way binding is updating the displayed
// URL in the browser if we change it inside our app state in Redux.
// We can simply subscribe to Redux and update it if it's different.
store.subscribe(() => {
const hash = "#/" + store.getState().currentSlide
if (location.hash !== hash) {
window.location.hash = hash
// Force scroll to top this is what browsers normally do when
// navigating by clicking a link.
// Without this, scroll stays wherever it was which can be quite odd.
document.body.scrollTop = 0
}
})
Avant de passer à la suite, nous allons simplifier les ACTIONS de Redux. Supprimez les actions NEXT_SLIDE
et PREVIOUS_SLIDE
de votre liste d’actions et de votre Reducer. Aux endroits où ces actions étaient utilisées, remplacer par l’action SET_SLIDE
avec une incrémentation ou une décrémentation de l’index courant.
Pour comprendre la logique du Middleware suivez la documentation Redux. Faites un essai qui reprend l’idée et logue dans la console toutes les actions déclenchées (voir ici sans le crashReporter).
Nous allons maintenant faire communiquer plusieurs navigateurs entre eux gràce à socket.io. Pour cela nous allons rajouter un middleware dédié. Sur un navigateur, quand la slide courante sera changée, un message sera envoyé aux autres navigateurs afin qu’ils changent eux aussi leur slide courante.
Côté serveur, importez socket.io
(tuto officiel) et mettez en place le callback permettant de recevoir les messages set_slide
provenant d’un client et de les propager à tous les autres clients. Ce guide permet de créer et tester une micro-application express utilisant socket.io en local et sur Heroku.
Côté client créez un Middleware dans lequel vous importerez socket.io-client
. Le middleware devra, dès qu’il intercepte une action de type SET_SLIDE
, propager un message adéquat via le socket, avant de faire appel à next(action)
Toujours dans le middleware, configurez la socket pour qu’à la réception des messages set_slide
, les actions soient dispatchées au store.
const propagateSocket = store => next => action => {
if (action.meta.propagate) {
if (action.type === SET_SLIDE) {
socket.emit('action', {type:'set_slide', value: action.hash})
}
}
next(action)
}
socket.on('action', (msg) => {
console.log('action',msg)
switch (msg.type) {
case 'set_slide':
store.dispatch(setSlide(msg.value, false))
break
}
})
Vous remarquerez sans doute qu’au point où nous en sommes nous allons provoquer une boucle infinie d’émissions de messages. Pour éviter cela, les actions SET_SLIDE
peuvent embarquer un information supplémentaire grâce la propriété meta
. Faites en sorte que seuls les dispatchs provenant d’un clic sur un bouton ou d’une modification de l’URL provoquent la propagation d’un message via Websocket.
N’oubliez pas d’utiliser applyMiddleware
lors de la création du votre store. Si vous avez précédement installé le devtool Redux, référez-vous à cette page pour modifier de nouveau votre code.
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const store = createStore(rootReducer, composeEnhancers(applyMiddleware(propagateSocket)));
Nous allons maintenant ajouter des fonctions de dessin à nos slides. En utilisant un stylet, un utilisateur pourra mettre en avant des elements sur la slide courante, et ce de manière synchronisée avec les autres appareils.
Pour simplier on ne dessine que sur la slide courante, et on efface/oublie le dessin quand on change de slide.
Pour cette partie, nous prendrons exemple sur ce tutoriel W. Malone.
Dans un premier temps, dans le composant Slide
ajoutez un élément canvas
avec avec les handlers d’événements onPointerDown, onPointerMove et onPointerUp ainsi qu’en déclarant une Référence React. Utilisez useRef
si vous êtes dans un ‘function component’, ou createRef
si vous êtes dans un ‘class-based component’ (voir ici):
<canvas className="stroke"
ref={refCanvas}
onPointerDown={pointerDownHandler}
onPointerMove={pointerMoveHandler}
onPointerUp={pointerUpEvent}></canvas>
Ces handlers nous permettront de d’écouter les événements provenant de pointer
.
Afin de vous faciliter la tâche, voici le code presque complet pour faire marcher le dessin sur le canvas.
Assurez-vous de bien faire les imports nécessaires au bon fonctionnement du code ci-dessous. Faites en sortes que l’on ne dessine que si c’est un stylet qui est utilisé.
var clickX = new Array()
var clickY = new Array()
var clickDrag = new Array()
var paint = false
// Cette ligne permet d'avoir accès à notre canvas après que le composant aie été rendu. Le canvas est alors disponible via refCanvas.current
// Si vous utilisez des Class Components plutôt que des function Components, voir ici https://stackoverflow.com/a/54620836
let refCanvas = useRef(null)
function addClick(x, y, dragging)
{
clickX.push(x),
clickY.push(y),
clickDrag.push(dragging)
}
function redraw(){
let context = refCanvas.current.getContext('2d')
let width = refCanvas.current.getBoundingClientRect().width
let height = refCanvas.current.getBoundingClientRect().height
//Ceci permet d'adapter la taille du contexte de votre canvas à sa taille sur la page
refCanvas.current.setAttribute('width', width)
refCanvas.current.setAttribute('height', height)
context.clearRect(0, 0, context.width, context.height); // Clears the canvas
context.strokeStyle = "#df4b26";
context.lineJoin = "round";
context.lineWidth = 2;
for(var i=0; i < clickX.length; i++) {
context.beginPath();
if(clickDrag[i] && i){
context.moveTo(clickX[i-1]*width, clickY[i-1]*height);
}else{
context.moveTo((clickX[i]*width)-1, clickY[i]*height);
}
context.lineTo(clickX[i]*width, clickY[i]*height);
context.closePath();
context.stroke();
}
}
function pointerDownHandler (ev) {
console.error('HEY ! ICI ON PEUT DIFFERENCIER QUEL TYPE DE POINTEUR EST UTILISE !')
let width = refCanvas.current.getBoundingClientRect().width
let height = refCanvas.current.getBoundingClientRect().height
var mouseX = (ev.pageX - refCanvas.current.offsetLeft)/width
var mouseY = (ev.pageY - refCanvas.current.offsetTop)/height
paint = true
addClick(mouseX, mouseY, false)
redraw()
}
function pointerMoveHandler (ev) {
if(paint){
let width = refCanvas.current.getBoundingClientRect().width
let height = refCanvas.current.getBoundingClientRect().height
addClick((ev.pageX - refCanvas.current.offsetLeft)/width, (ev.pageY - refCanvas.current.offsetTop)/height, true)
redraw()
}
}
function pointerUpEvent (ev) {
paint = false
}
Dans votre état initial, rajoutez l’attribut suivant :
drawing: {
clickX: [],
clickY: [],
clickDrag: []
}
Et créez les actions ADD_DRAW_POINTS
et RESET_DRAW_POINTS
.
ADD_DRAW_POINTS
devra accepter au moins 3 paramètres de type Array (clickX, clickY, clickDrag)
qui seront concaténés à l’état du store.
RESET_DRAW_POINTS
réinitialiseras les tableaux du store à vide.
Dans votre composant Slide, réalisez la connexion avec le store :
const mapStateToProps = state => {
return {
drawing: state.drawing
}
}
const mapDispatchProps = dispatch => {
return {
addPoints: (x, y, drag) => dispatch(addDrawPoints(x, y, drag, true))
}
}
Une fois ceci fait, faites en sorte qu’à chaque fois qu’une ligne est finie de dessiner (pointerUpEvent
), que vous copiez les points de la nouvelle ligne dans le store. Bien sûr, maintenant il faut aussi dessiner les lignes stockées dans le store (props.drawing.
).
Ajoutez un bouton “Effacer” à votre toolbar, ce bouton déclenchera l’action RESET_DRAW_POINTS
Vous pouvez maintenant ajouter à votre Middleware de nouveaux cas permettant de propager les nouvelles lignes dessinées aux autres appareils.
// ...
else if (action.type === ADD_DRAW_POINTS) {
socket.emit('action', {type: 'add_draw_points', value: {
x: action.x,
y: action.y,
drag: action.drag
}
})
} else if (action.type === RESET_DRAW_POINTS) {
socket.emit('action', {type: 'reset_draw_points'})
}
//...
Vous remarquerez qu’à l’ouverture sur un autre appareil, votre dessin n’apparait que si vous dessinez aussi sur cet appareil. Pour remédier à ce problème, utilisez useEffect afin d’exécuter redraw()
au moment opportun.
Pour terminer, nous allons effectuer de la reconnaissance de geste lors d’évènements touch.
Pour ce faire nous allons utiliser le $1 recognizer vu en cours. Nous allons utiliser une version modifiée de OneDollar.js pour fonctionner avec React. Il n’y a pas de module JS récent pour cette bibliothèque. Nous devrions donc le créer, mais pour plus de simplicité nous allons placer directement la bibliothèque dans le dossier src/
pour qu’elle soit facilement bundlée par Webpack.
Au niveau de votre Slide
, importer et initialiser votre le One Dollar Recognizer.
// Voir ici pour le détails de options https://github.com/nok/onedollar-unistroke-coffee#options
const options = {
'score': 80, // The similarity threshold to apply the callback(s)
'parts': 64, // The number of resampling points
'step': 2, // The degree of one single rotation step
'angle': 45, // The last degree of rotation
'size': 250 // The width and height of the scaling bounding box
};
const recognizer = new OneDollar(options);
// Let's "teach" two gestures to the recognizer:
recognizer.add('triangle', [[627,213],[626,217],[617,234],[611,248],[603,264],[590,287],[552,329],[524,358],[489,383],[461,410],[426,444],[416,454],[407,466],[405,469],[411,469],[428,469],[453,470],[513,478],[555,483],[606,493],[658,499],[727,505],[762,507],[785,508],[795,508],[796,505],[796,503],[796,502],[796,495],[790,473],[785,462],[776,447],[767,430],[742,390],[724,362],[708,340],[695,321],[673,289],[664,272],[660,263],[659,261],[658,256],[658,255],[658,255]]);
recognizer.add('circle', [[621,225],[616,225],[608,225],[601,225],[594,227],[572,235],[562,241],[548,251],[532,270],[504,314],[495,340],[492,363],[492,385],[494,422],[505,447],[524,470],[550,492],[607,523],[649,531],[689,531],[751,523],[782,510],[807,495],[826,470],[851,420],[859,393],[860,366],[858,339],[852,311],[833,272],[815,248],[793,229],[768,214],[729,198],[704,191],[678,189],[655,188],[623,188],[614,188],[611,188],[611,188]]);
Etendre les fonctions pointerDownHandler
, pointerMoveHandler
, pointerUpHandler
pour qu’elles traite différemment les sources touch
, pen
et mouse
.
Nous allons associer les gestes au touch
. Toutefois pour débugger plus facilement, vous pouvez commencer traiter les gestes sur le pointerEvent mouse
, et basculer sur le touch une fois que cela marche bien.
Stocker les points composants le geste dans un Array gesturePoints
.
Dans la fonction de dessin redraw
vous pouvez ajouter un cas à la fin qui dessine en cas de geste (les points composant le geste sont stockés dans gesturePoints
).
Vous devrez être vigilant à convertir vos points pour être dans le référentiel du canvas, comme dans le code fournit ci-dessus.
function redraw(){
...
if (gesture) {
context.strokeStyle = "#666";
context.lineJoin = "round";
context.lineWidth = 5;
context.beginPath();
context.moveTo(gesturePoints[0][0]*width, gesturePoints[0][1]*height);
for(var i=1; i < gesturePoints.length; i++) {
context.lineTo(gesturePoints[i][0]*width-1, gesturePoints[i][1]*height);
}
context.stroke();
}
}
Quand le geste se termine (pointerUpHandler
), vous pouvez lancer la reconnaissance du geste.
let gesture = recognizer.check(gesturePoints);
Inspectez l’objet gesture dans la console, et vérifiez que vous arrivez bien à reconnaiter un cercle et un triangle.
Pensez à réinitialiser gesturePoints
une fois le geste terminé.
Toujours dans pointerUpHandler
, vous pouvez imprimer les trajectoires correspondants à des gestes.
console.log('[['+gesturePoints.join('],[')+']]')
Utiliser cette sortie pour ajouter deux nouveaux gestes: ‘>’ et ‘<’ (partant du haut vers le bas) à votre recognizer.
Une fois le geste exécute, s’il correspond à un de ces deux nouveaux gestes (recognized == true
), dispatcher les actions suivant ou précédent.
Pour faire cela, nous allons nous appuyer sur un mapDispatchToProps
qu’il faudra connecter à votre composant.
const matchDispatchProps = dispatch => {
return {
nextSlide: () => {
dispatch(setSlide(store.getState().currentSlide+1, true))
dispatch(resetDrawPoints(true))
},
previousSlide: () => {
dispatch(setSlide(store.getState().currentSlide-1, true))
dispatch(resetDrawPoints(true))
},
resetDrawPoints: () => dispatch(resetDrawPoints(true))
}
resetDrawPoints
est l’action associée à l’effaçage des dessins effectué sur le transparent.
Vérifier que l’action est bien distribuée sur tous les dispositifs connectés.
Vous pouvez maintenant tester, nettoyer le code, et rendre.
À rendre pour le dimanche 17 à 23h59.
README.md
décrivant le process de build en dev, en prod, et de déploiement.package.json
nettoyé ne contenant que les dépendances nécessaires.Slideshow
, les Slides
, la Toolbar
.