diff --git a/R4.01_R4.A.10/README.md b/R4.01_R4.A.10/README.md index 088660f..a0312ba 100644 --- a/R4.01_R4.A.10/README.md +++ b/R4.01_R4.A.10/README.md @@ -3,3 +3,7 @@ #### Semaine 1 [Compléments de javascript](cours/jscomp.pdf) javascript, [tp1](./td_tp/tp1) +#### Semaine 2 +[DOM](cours/dom.pdf), [tp2](./td_tp/tp2), [tpmvc](td_tp/tp2mvc) + + diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/README.md b/R4.01_R4.A.10/td_tp/tp2mvc/README.md new file mode 100644 index 0000000..152456f --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/README.md @@ -0,0 +1,163 @@ +#### Ex3 : modele MVC +Le but est d'écrire une todolist en javascript, en respectant le pattern MVC. Il est important de mener à bout cet exercice, car il nous servira +de fil rouge notamment en ajoutant une api rest et ajax pour la communication avec le service. + +
+ +
+ +Le contrôleur a accès à la vue et au modèle. + +##### Le modèle + +Cette "classe" (rien à compléter) utilise +l'objet [localStorage](https://developer.mozilla.org/fr/docs/Web/API/Window/localStorage) +pour sauvegarder la todolist. + +Chaque todo est un objet json de la forme + +```js +{ id : 1 , text : "apprendre le js", done : false } +``` + +La liste des méthodes publiques de cette classe + +```js +/** @brief return the todolist + * @param filter "all" or "active" or "done" + * @return array of todos + */ +getTodos(filter) + +/** @brief add (and save) a new todo with todoText + * @param todoText : text of the new todo + */ +add(todoText) + +/** @brief delete a todo + * @param id id of the todo + */ +delete(id) + +/** @brief update a todo + * @param id id of the todo + */ +edit(id,updatedText) + +/** @brief toggle a todo (done<->active) + * @param id id of the todo + * @param updatedText text of the todo + */ +toggle(id) +``` + +##### La vue +Cette "classe" permet au contrôleur de gérer la vue. + + +Liste des méthodes publiques + +```js +/** @brief change the active tab (all,active or done) + * @param filter the active tab (all, active or done) + */ +setFilterTabs(filter) + +/** @brief update the todo list with todos + * @param todos array of todo + */ +renderTodoList(todos) +``` + +Le contrôleur peut s'abonner (notification de la vue au acontrôleur) +aux événements suivants : + +add/delete/edit/toggle todo : avec `bind{Add|Delete|Edit|Toggle}Todo` + +Ces méthodes d'abonnement prennent en paramètre une fonction du contrôleur qui est appelé par la vue pour lui notifier +une requête. + +```js +/** @brief subscribe to add event + * @param handler function(text) + */ +bindAddTodo(handler) + +/** @brief suscribe to delete event + * @param handler function(id) + */ +bindDeleteTodo(handler) + +/** @brief suscribe to edit event + * @param handler function(id,text) + */ +bindEditTodo(handler) + +/** @brief suscribe to toggle event + * @param handler function(id) + */ +bindToggleTodo(handler) +``` + +##### Le contrôleur +C'est lui qui gére le routage. Il y a trois urls possibles `index.html/#/{all|active|done}` (listener sur l'évenement + dom `hashchange`. + +Liste des méthodes +```js +/** @brief get the todolist from the model + * and use the view to render + */ +filterTodoList() + +/** @brief binding function called by the view + * to add a new todo + * @param text text of the new todo + */ +addTodo(text) + +/** @brief binding function called by the view + * to delete a todo + * @param id id of the todo +*/ +deleteTodo(id) + +/** @brief binding function to toggle the state + * of a todo + * @param id id of the todo + */ +toggleTodo(id) + +/** @brief binding function to change the text + * of a todo + * @param id id of the todo + * @param changedText new text for the todo +*/ +editTodo(id,changedText) +``` + +Par exemple, lorsque l'utilisateur ajoute une todo : + +1. la vue est sensible à l'événement de soumission du formulaire, +2. la fonction reflexe récupére le texte saisie, +3. elle appelle la fonction avec laquelle le contrôleur s'est abonné, en lui passant le texte, +4. la fonction du contrôleur `addTodo` récupére le texte, +5. le contrôleur demande au modèle la création d'une nouvelle todo, +6. le contrôleur demande un rafraichissement de la vue. + + +#### Travail à faire +- Commpléter la méthode privée dans la vue `#createNewTodoElement` qui permet de créer un + nouvel élément dom représentant une todo. Ce qui est attendu est de la forme + ```html +
  • + + +
  • + ``` +- Compléter la méthode `bindDeleteTodo` de la vue. +- Compléter la méthode `bindToggleTodo` de la vue. +- Compléter la méthode `bindEditTodo` de la vue. diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/img/todo.png b/R4.01_R4.A.10/td_tp/tp2mvc/img/todo.png new file mode 100644 index 0000000..26a6585 Binary files /dev/null and b/R4.01_R4.A.10/td_tp/tp2mvc/img/todo.png differ diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/src/css/style.css b/R4.01_R4.A.10/td_tp/tp2mvc/src/css/style.css new file mode 100644 index 0000000..a2185a4 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/src/css/style.css @@ -0,0 +1,63 @@ +:focus-visible {outline:none;} + +.is-active { + color : #8EB901; +} + +#add_form > div { + display : flex; + justify-content:space-between; +} + +.todo { + display : flex; + align-items: center; +} +.todo checkbox,.todo i { + flex-grow : 0; + flex-shrink : 0; + flex-basis : auto; +} +.todo span { + flex-grow : 1; + flex-shrink : 1; + flex-basis : auto; +} + + +.todolist { + padding-left: 0; +} + +.todolist li { + list-style: none; + padding: var(--pico-form-element-spacing-vertical) 0; + display: flex; + justify-content: space-between; + align-items: center; +} +#todos_filter { + display:flex; + justify-content : center; +} + +#todos_filter a { + margin:1em; +} + +.todolist i { + cursor: pointer; +} + +.todolist li:not(:last-child) { + border-bottom: 1.5px solid var(--pico-form-element-border-color); +} + +.todolist > li > label:has(input:checked) { + text-decoration: line-through; +} + +footer { + text-align: center; +} + diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/src/index.html b/R4.01_R4.A.10/td_tp/tp2mvc/src/index.html new file mode 100644 index 0000000..a652640 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/src/index.html @@ -0,0 +1,53 @@ + + + + + + + + Todo + + + + + + + +
    +

    To Do List

    +
    + +
    +
    +
    +
    +
    + + +
    +
    + + + + + + + 0/50 characters +
    +
    +
    + + +
    +
    + + + + + + + + diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/src/js/controller.js b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/controller.js new file mode 100644 index 0000000..c837fa1 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/controller.js @@ -0,0 +1,64 @@ +class Controller { + constructor(model, view) { + + this.model = model; + this.view = view; + this.filter = "all"; + + /** Abonnements à la vue **/ + + this.view.bindAddTodo(this.addTodo.bind(this)); + this.view.bindDeleteTodo(this.deleteTodo.bind(this)); + this.view.bindToggleTodo(this.toggleTodo.bind(this)); + this.view.bindEditTodo(this.editTodo.bind(this)); + + /** filtrages par url **/ + + this.routes = ['all','active','done']; + + /** Routage **/ + window.addEventListener("load",this.routeChanged.bind(this)); + window.addEventListener("hashchange",this.routeChanged.bind(this)); + } + + + routeChanged(){ + let route = window.location.hash.replace(/^#\//,''); + this.filter = this.routes.find( r => r === route) || 'all'; + this.filterTodoList(); + } + + + + + filterTodoList () { + let todos = this.model.getTodos(this.filter) + this.view.renderTodoList(todos) + this.view.setFilterTabs(this.filter) + } + + addTodo(text) { + this.model.add(text) + this.filterTodoList() + } + + deleteTodo(id) { + this.model.delete(parseInt(id)) + this.filterTodoList() + } + + toggleTodo(id) { + this.model.toggle(parseInt(id)) + this.filterTodoList() + } + + editTodo(id, text) { + this.model.edit(parseInt(id),text) + this.filterTodoList() + } +} + +const model = new Model() +const view = new View() +const app = new Controller(model, view) + diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/src/js/model.js b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/model.js new file mode 100644 index 0000000..cde92cc --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/model.js @@ -0,0 +1,48 @@ +class Model { + constructor() { + this.todos = JSON.parse(localStorage.getItem('todos')) || [] + } + + #commit(todos) { + localStorage.setItem('todos', JSON.stringify(todos)) + } + + getTodos(filter){ + if (filter === "active") + return this.todos.filter(todo => !todo.done) + if (filter === "done") + return this.todos.filter(todo => todo.done) + + return this.todos + } + + add(todoText) { + const todo = { + id: this.todos.length > 0 ? this.todos[this.todos.length - 1].id + 1 : 1, + text: todoText, + done : false, + } + this.todos.push(todo) + this.#commit(this.todos) + } + + edit(id, updatedText) { + this.todos = this.todos.map(todo => + todo.id === id ? { id: todo.id, text: updatedText, done: todo.done} : todo + ) + this.#commit(this.todos) + } + + delete(id) { + this.todos = this.todos.filter(todo => todo.id !== id) + this.#commit(this.todos) + } + + toggle(id) { + this.todos = this.todos.map(todo => + todo.id === id ? { id: todo.id, text: todo.text, done: !todo.done } : todo + ) + this.#commit(this.todos) + } +} + diff --git a/R4.01_R4.A.10/td_tp/tp2mvc/src/js/view.js b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/view.js new file mode 100644 index 0000000..fe85933 --- /dev/null +++ b/R4.01_R4.A.10/td_tp/tp2mvc/src/js/view.js @@ -0,0 +1,83 @@ +class View { + charLimit = 70; + + constructor() { + this.form = document.querySelector("#add_form") + this.input = document.querySelector("#input_todo") + this.list = document.querySelector("#todos_list") + this.tabs = document.querySelectorAll("#add_form span a") + this.loader = document.querySelector("#loader") + this.count = document.querySelector("#count") + + + this.input.addEventListener("input", (e) => { + if (e.target.value.length >= this.charLimit) { + e.target.value = e.target.value.substring(0,this.charLimit); + } + count.textContent = e.target.value.length; + }); + count.nextSibling.textContent = `/${this.charLimit} characters` + } + + #getTodo() { + return this.input.value + } + + #resetInput() { + this.input.value = '' + count.textContent = 0 + } + + #createNewTodoElement(todo){ + let li = document.createElement("li") + + // TODO + + return li + } + + + setFilterTabs(filter){ + this.tabs.forEach( tab => { + if (filter === tab.id) + tab.classList.add("is-active") + else + tab.classList.remove("is-active") + }) + } + + renderTodoList(todos) { + let list = new DocumentFragment() + for (let todo of todos){ + list.appendChild(this.#createNewTodoElement(todo)) + } + this.list.replaceChildren(list) + } + + + + + /** Abonnements événements **/ + + bindAddTodo(handler) { + this.form.addEventListener("submit", (e=>{ + e.preventDefault() + let text = this.#getTodo() + handler(text) + this.#resetInput() + })) + } + + + bindDeleteTodo(handler) { + // TODO + } + + bindEditTodo(handler) { + // TODO + } + + bindToggleTodo(handler) { + // TODO + } +}