【验证模型】3-7 「今晚,我想来点⋯⋯」动手做的餐点选择器进化!(中集)

继昨天的渲染函式後,我们接下来要完成各自页面的功能。

而根据昨天的 Wireframe 来看,主画面的显示与取得新的饲料是需要计算完整个清单後才能从里头随机抽取一个出来,因此,显而易见的是我们必须先完成填写饲料页面的部分,并且顺便完成新增与删除的功能。

流程三:饲料清单之渲染功能

在开发饲料清单的功能之前,如同写渲染页面函式一样,我们一样要先把渲染清单的样板处理完成,而有关於饲料清单样板的部分之前在 getTemplate 函式里面是写死的,因此现在我们将其透过另一个样板函式 getMealListTemplate 来取得其内容:

function getTemplate (pagename){
  var template = {
    mainpage: `
      <div class="card-wrapper">
        <h1 class="card-title">Today's meal</h1>
        <div class="card-content">
            <p class="meal-text">Gooold</p>
        </div>
        <div class="card-footer">
            <div class="btn btn-get">Get Meal</div>
        </div>
      </div>
    `,
    settingpage: `
      <div class="card-wrapper">
        <h1 class="card-title">Menu</h1>
        <div class="card-content">
            <ol class="meal-list">
                ${getMealListTemplate()}
            </ol>
        </div>
        <div class="card-footer">
            <div class="btn btn-add">Add Meal</div>
        </div>
      </div>
    `
  }
  return template[pagename]
}

而在 getMealListTemplate 的实作过程中,我们先预设一笔假资料来方便我们产生样板,同样地我们采用集中进入点与离开点统一的部分来产生样板:

var mealList = ['杜格','喜碗','优格','金罐','星球']

function getMealListTemplate(){
  var template = '' // 样板初始值

  // 组装样板的过程
  for(var i = 0; i < mealList.length; i++){
    template += `<li class="meal-item" >${mealList[i]} <div class="btn btn-delete">delete</div></li>`
  }

  return template // 完成的样板
}

完成 getMealListTemplate 後,先前产生 setting page 的页面就可以藉由这个函式来动态的计算饲料清单的内容,如此一来,往後我们若是新增或删除 mealList 阵列中的值後,再重新渲染一次页面即可显示最新的资料结果。

到此流程的程序码可参考 这里

而若是跟着实作的你或许会发现,在先前单纯撰写 JavaScript 程序码时,我们所关注的点只需要让整个逻辑流程顺畅就好,然而在实作浏览器中的功能时,最重要的地方在於 资料与画面上的显示要一致,也就是说运作功能时的资料逻辑要处理,但也同时要记得处理画面上的更新才不会看起来没有运作完成。

流程四:饲料清单之新增功能

在上面实作渲染功能时,我们的渲染功能机制是基於 mealList 的资料再处理渲染的,也就是说接下来的新增与删除功能,其实就是基於在 mealList 中的操作来更新资料,最後再重新渲染一次就可以完成。

首先在新增饲料之前,使用者的第一步骤一定是透过输入饲料名称再点击新增来完成,而这里的规划当初是采用使用原生的 prompt 视窗来询问并新增,因此在这里我们仅需要先考虑到绑定事件的问题。

而对於绑定的最佳时机来说是在我们一开始渲染这个页面时再针对所需要的区块做绑定。

而由於我们最终想触发的元素是在透过动态产生出来的元素,若是直接绑定在该元素上会无法产生任何效果,因此这时後要回想起在绑定事件的时候我们曾经提过的 事件代理(event delevation) 机制的议题:

var view = document.querySelector('.view-wrapper')
var btn_mainpage = document.querySelector('.tab-item.mainpage')
var btn_setting = document.querySelector('.tab-item.setting')

function initPage(){
  renderPage('mainpage')

  btn_mainpage.addEventListener('click', function(){
    renderPage('mainpage')
    view.removeEventListener('click', handleSettingPageClick)
  })

  btn_setting.addEventListener('click', function(){
    renderPage('settingpage')
    view.addEventListener('click', handleSettingPageClick)
    
  })
  
}

我们将事件绑定在主要渲染画面的 .view-wrapper 元素上,并且预设统一透过 handleSettingPageClick 函式来处理後续的点击行为,藉此可以让我们在不同的页面中,取消不必要的事件绑定。

这时你可以透过简单的模拟行为来确认是否有绑定成功,此时应该只有在设定饲料的页面当中才会触发此函式:

function handleSettingPageClick(e){
  console.log('click')
}

在确认完毕 handleSettingPageClick 的作用後,我们要透过触发事件 Event 物件中的 event.target.className 来捕捉到当我们是点击新增按钮时才执行某些事情:

function handleSettingPageClick(e){

  if(e.target.className === 'btn btn-add'){ // 确认点击新增按钮才执行
    addMeal() // 预设一个新增餐点的函式
    renderPage('settingpage') // 新增完毕後触发渲染页面来更新内容
  }

}

接着我们要来处理 addMeal 函式,而由於在渲染的部分我们已经交由 renderPage 函式来执行了,因此在 addMeal 中我们只需要关注更新资料上的问题:

function addMeal(){
  var mealName = prompt('add meal') // 透过原生的 prompt 函式来取得饲料名称
  if(mealName){ // 确保使用者有输入才执行
    mealList.push(mealName) // 将饲料名称新增至 mealList 中,後续渲染时就会根据最新的 mealList 资料来进行处理
  }
}

完成 addMeal 函式後我们再次回顾整体的的逻辑流程如下:

  • 绑定事件
  • 点击後询问使用者饲料名称并更新至 mealList 当中
  • 渲染函式 renderPage 中的饲料清单会根据 mealList 中的资料来渲染(因此会产生类似新增的效果)

而到此步骤的程序码可以参考 这里,眼尖的你或许会发现这个实作其实与常见的待办清单做法可说是几乎一致的,所以这种实作都是做久了就能够融会贯通,常见的功能几乎都大同小异。

流程五:饲料清单之删除功能

有了新增就有删除!

删除功能基本上大同小异,都是在操作 mealList 的资料,但在删除的过程当中有一些地方要注意。

首先我们绑定删除事件的方式如新增时的作法一样,我们在 handleSettingPageClick 函式中透过 btn btn-delete 确定事件代理触发的对象是删除按钮时才触发後续的行为:

function handleSettingPageClick(e){

  if(e.target.className === 'btn btn-add'){ // 确保代理触发的对象为 新增按钮
    addMeal()
    renderPage('settingpage')
  }

  if(e.target.className === 'btn btn-delete'){ // 确保代理触发的对象为 删除按钮
    deleteMeal() // 同样预设一个删除函式来处理
    renderPage('settingpage')
  }
}

跟着实作到这里的读者,可能会开始想像 deleteMeal 中的作法:

  • 透过 indexOf 来搜寻阵列中相同字串,藉此删除 mealList 中的资料
  • 透过删除按钮的索引值index,来删除阵列中对应索引值的位置

然而以上作法在实作後都会遇到一些问题:

  • 透过 indexOf 会遇到相同名称的问题(只是这个故事还尚可接受误删相同名称实际资料却不同范例)
  • 新增与删除过後的索引值会变得不稳定。

因此较佳的作法是要提供一个几乎是唯一值的辨识码,也就相当於资料库中的 主键(primary key)

原先资料:

var mealList = ['杜格','喜碗','优格','金罐','星球']

加入 +new Date() 时间戳表示的资料:

var mealList = [
  {
    id: 1602386309696,
    content: '杜格'
  },
  {
    id: 1602386312825,
    content: '喜碗'
  },
  {
    id: 1602386315632,
    content: '优格'
  },
  {
    id: 1602386379297,
    content: '金罐'
  },
  {
    id: 1602386385508,
    content: '星球'
  },
]

如此一来我们只要核对资料中的 id 就可以确保删除的资料是同一笔资料,而不会误删其他看起来相同的资料了。

也因为之後我们要依据这个资料格式的参考,因此这时候要来修改与 mealList 有关的函式:

负责处理清单渲染的 getMealListTemplatealList 函式:

function getMealListTemplate(){
  var template = ''

  for(var i = 0; i < mealList.length; i++){
    template += `<li class="meal-item" >${mealList[i].content} <div class="btn btn-delete">delete</div></li>`
  }

  return template
}

负责新增饲料的 addMeal 函式:


function addMeal(){
  var mealName = prompt('add meal')
  if(mealName){
    mealList.push({ // 调整成物件格式
      id: +new Date(), // 新增时给予一个当下的时间戳,由於几乎不可能在同一毫秒时连续新增两笔资料,因此几乎可以当作唯一值来判断
      content: mealName // 将原先的名称移至 content 属性中
    })
  }
}

更改完後再回头测试看看,此时应该会发现画面上显示的跟原本没改过的一模一样,但这时每笔资料已经从原本的纯字串值改为物件型态的值,因此在 getMealListTemplate 中,我们还可以加上 data-id 在删除按钮上来方便後续取得对应的 id

function getMealListTemplate(){
  var template = ''

  for(var i = 0; i < mealList.length; i++){
    template += `<li class="meal-item" >${mealList[i].content}
        <div class="btn btn-delete" data-id="${mealList[i].id}">delete</div>
      </li>`
  }

  return template
}

接着就如同处理新增饲料名称的方式一样,新增一个 deleteMeal 函式来处理删除事件:

function deleteMeal(e){
  var targetID = e.target.dataset.id // 透过点击事件中的 Event 物件里找到刚刚在产生样板时所加入的 data-id

  mealList = mealList.filter(item => { // 透过 Array.filter 方法与 data-id 来筛选阵列中的资料
    return item.id !== +targetID
  })
}

另外也别忘了刚刚 handleSettingPageClick 中也要将事件资料 e 传递进去:

function handleSettingPageClick(e){

  if(e.target.className === 'btn btn-add'){
    addMeal()
    renderPage('settingpage')
  }

  if(e.target.className === 'btn btn-delete'){
    deleteMeal(e)
    renderPage('settingpage')
  }
}

到这个阶段中,我们删除饲料的功能就大功告成了。

现在在你的程序中应该能够渲染出正确的饲料清单并且能够新增与删除饲料清单中的任何一笔资料,并且显示於画面当中,而到此阶段的程序码可以参考这里

而我们明天要再把剩下的主画面显示与储存机制的部分完成。


<<:  Day 30: Greedy Method

>>:  认识 React Hooks 之二

Day 9 - 利用路由协议来组 SD-WAN 网路

那路由器及虚拟机都安装好後,我们要来异地组网啦! 在此之前,我们先来介绍一下吧 什麽是 SD-WAN...

AE卷轴制作5-Day6

1.将要遮罩的Shape>Pre compose 2.最後就是最简单的部分,找张图用遮罩就完成...

Day 28 Chatbot integration- 汇率预测小工具

Chatbot integration- 汇率预测小工具 丑话先说在前头,模型虽然可以达到一定程度准...

Day01:铁人赛开场

一、主题内容 虽然知道全端工程师的路不好走,自己目前也还不是很称职,仍想以自己转职的角度、回顾的方式...

[Python 爬虫这样学,一定是大拇指拉!] DAY19 - Python:Requests 基本应用 (2)

今天要来讲的是,读取送出 Request 後拿回来的 Response。 读取 Response 以...