在本系列中,我将使用我的Meeting Planner 应用程序作为现实生活中的示例,指导您从概念到现实启动一家初创公司 。在此过程中的每一步,我都会将 Meeting Planner 代码作为开源示例发布,您可以从中学习。我还将解决与启动相关的业务问题。
介绍
在今天的教程中,我将指导您完成对会议安排界面的初步全面更改。我的目标是使用 ajax 使所有常见的调度活动成为可能,而无需任何页面刷新。事实证明,这其中的某些方面很简单,而其他方面则相当复杂。在这一集中,我将专注于简单的部分:如何在基于 php 的 Yii 应用程序中基本构建Ajax UX 请求。
在第二部分中,我将介绍更困难的内容——在初始页面加载后调试 Ajax 和重新初始化 Bootstrap 小部件。我还将分享我如何使用 Google 的 chrome 浏览器开发者控制台来帮助我识别损坏的代码。
坦率地说,虽然最初的更新进展顺利,但我遇到了很多障碍和困难,以至于有时我认为我可能不得不放弃测试版的目标。
奇怪的是,有些代码路径似乎让我接近完成,然后遇到了无法逾越的障碍——我必须从新方法开始。最终,我能够通过 Ajax 成功完成测试版的完整调度。
今天跟随我指导您完成工作的核心部分。
如果您尚未试用Meeting Planner,请继续 使用新的交互式功能安排您的第一次会议。我确实参与了下面的评论线程,所以告诉我你的想法!你也可以在 Twitter @reifman 上联系我。如果您想为未来的教程推荐新功能或主题,我特别感兴趣。
提醒一下,Meeting Planner 的所有代码都是用 PHP 的 Yii2 F ram ework 编写的。如果您想了解更多关于 Yii2 的信息,请查看我们的并行系列 Programming With Yii2。
简化调度 UX
我对产品这个阶段的主要目标是使用 Ajax 实现所有关键的调度功能,并消除当前编辑主题、添加参与者、添加日期时间和地点以及注释所需的页面刷新。
背景
由于我之前在站点中构建了一些 Ajax,因此我有一些想法什么会好,什么会困难。
和我一起了解 ajaxifying 调度的开始元素。
编辑会议主题
我从编辑会议主题面板开始,因为它由几个静态字段、一个输入和一个文本区域组成。但是,主题字段确实使用了 jquery Typeahead 小部件。小部件会使事情复杂化,因为您需要能够使用 Ajax 来初始化它们。
在这种情况下,我预加载隐藏的表单并加载小部件库。它没有真正的复杂性。在下一集中,您将看到日期时间和地点面板上的 Bootstrap Switch 小部件使这变得更加困难。
预加载所有javascript
因此,为了简化每个调度面板(参与者、主题、日期时间、地点和注释)的 ajaxifying,我会预先加载它们并扩展 meeting.js 的初始内容。
我还扩展了 MeetingAsset.php 定义以包含更多 JavaScript:
<?php namespace frontend\assets; use yii\web\AssetBundle; class MeetingAsset extends AssetBundle { public $basePath = '@webroot'; public $baseUrl = '@web'; public $css = [ 'css/bootstrap-combobox.css', ]; public $js = [ 'js/meeting.js', 'js/meeting_time.js', 'js/jstz.min.js', 'js/bootstrap-combobox.js', 'js/create_place.js', ]; public $depends = [ 'yii\web\YiiAsset', 'yii\bootstrap\BootstrapAsset', ]; }
MeetingAsset 由 Meeting view.php 文件加载:
<?php use yii\helpers\Basehtml; use yii\helpers\Html; use yii\widgets\DetailView; use yii\widgets\ListView; use common\components\MiscHelpers; use frontend\assets\MeetingAsset; MeetingAsset::register($this);
加载主题面板
主题和会议详细信息是 _panel_what.php 部分的一部分。下面,我将其设置为在加载时隐藏#editWhat
:
<?php if ($model->has_subject || $model->subject == \frontend\models\Meeting::DEFAULT_SUBJECT) { ?> <div id="collapseWhat" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingWhat"> <div class="panel-body"> <div id="showWhat"> <?php if (empty($model->message)) { echo Html::encode($this->title); // note: required because couldn't prevent extra space } else { echo Html::encode($this->title).': '.Html::encode($model->message).' '; } ?> </div> <div id="editWhat" class="hidden"> <?= $this->render('_form', [ 'model' => $model, 'subjects' => $model->defaultSubjectList(), ]) ?> </div> </div> </div> <?php } else { ?>
我在 _panel_what.php 中连接了编辑按钮(带有铅笔图标)来调用 javaScriptshowWhat()
切换函数来显示或隐藏编辑表单。这是代码:
<?php if ($model->isOrganizer() && $model->status <= Meeting::STATUS_confirmED) { //['update', 'id' => $model->id] echo Html::a('', 'javascript:void(0);', ['class' => 'btn btn-primary glyphicon glyphicon-pencil', 'title'=>'Edit','onclick'=>'showWhat();']); } ?>
meeting.js 中的showWhat()
函数如下所示:
// 在主题面板顶部显示消息函数显示什么(){ function showWhat() { if ($('#showWhat').hasClass( "hidden")) { $('#showWhat').removeClass("hidden"); $('#editWhat').addClass("hidden"); }else { $('#showWhat').addClass("hidden"); $('#editWhat').removeClass("hidden"); $('#meeting-subject').select(); } }; function cancelWhat() { showWhat(); }
这是它隐藏和显示的 /frontend/views/meeting/_form.php 的顶部。这是 input 和 textarea 字段出现的地方:
<?php use yii\helpers\Html; use yii\widgets\ActiveForm; use \kartik\typeahead\TypeaheadBasic; use common\components\MiscHelpers; /* @var $this yii\web\View */ /* @var $model frontend\models\Meeting */ /* @var $form yii\widgets\ActiveForm */ ?> <div class="meeting-form"> <?php $form = ActiveForm::begin(); ?> <div class="row"> <div class="col-md-6"> <?php echo $form->field($model, 'subject')->widget(TypeaheadBasic::classname(), [ 'data' => $subjects, 'options' => ['placeholder' => Yii::t('frontend','what\'s the subject of this meeting?'), 'id'=>'meeting-subject', //'class'=>'input-large form-control' ], 'pluginOptions' => ['highlight'=>true], ]); ?> </div> </div>
通过 Ajax 更新主题和会议详细信息
当用户更新会议表单时,会调用以下 Ajax:
// meeting subject panel function updateWhat(id) { // ajax submit subject and message $.ajax({ url: $('#url_prefix').val()+'/meeting/updatewhat', data: {id: id, subject: $('#meeting-subject').val(), message: $('#meeting-message').val() }, success: function(data) { $('#showWhat').text($('#meeting-subject').val()); showWhat(); } }); }
该函数actionUpdatewhat
在 MeetingController.php 中:
public function actionUpdatewhat($id,$subject='',$message='') { Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; if (!Meeting::isAttendee($id,Yii::$app->user->getId())) { return false; } $m=Meeting::findOne($id); $m->subject = $subject; $m->message = $message; $m->update(); return true; }
注意Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
哪个配置 Yii 方法返回 JSON,而不是 HTML。
此外,该Meeting:isAttendee()
功能是一种身份验证检查,以确保在更新会议数据之前登录用户是与会者。
到目前为止我学到了什么
如您所见,要对所有这些部分进行 ajaxify 处理需要相当多的代码。
挑战之一是人类试图同时在如此多的文件和两种不同的语言之间切换。PHP 和 JavaScript 有不同的做事方式。例如,在 PHP 中使用句点和 JavaScript 中的加号来连接字符串。偶尔尝试构建查询参数字符串的语言之间切换 r api可能会导致错误。
在我的基于 PHP 的 Ajax 函数中还需要进行密集的安全检查。在今天的教程中,您会看到它的开头,但在使代码完全生效之前,我必须添加更彻底的检查。
最后,我尝试重用符号和结构方法,以便所有代码都具有相似的组成和术语——尽管使用不同的数据元素。
发送会议记录
会议记录也是静态文本区域字段。然而,当添加一个注释时,可能存在需要在页面上更新的持续注释线程(例如,不仅仅是单个会议主题)。重要的是告诉用户我们将通知参与者有关该注释的信息。
例如,我在日程安排中取消了提交按钮 UX,以便快速高效地进行规划。新的 Meeting Planner 用户经常对此感到困惑,因此我添加了提醒,让他们知道我们会处理它。
通过 Ajax 编写注释
首先,有 _panel.php 用于会议记录。我预先构建了可以根据需要通过 jQuery 显示的隐藏错误警报。我计划在未来对此进行简化和标准化,包括让消息的本地化变得更容易。
在下面的示例中,noteMessage1
和noteMessage2
都被加载为隐藏。
<?php use yii\helpers\Html; use yii\bootstrap\Collapse; ?> <div id="noteMessage" class="alert-info alert fade in hidden"> <button type="button" class="close" data-dismiss="alert" aria-hidden="true">×</button> <span id="noteMessage1"> <?= Yii::t('frontend',"Thanks for your note. We'll automatically share it with other participants.")?></span> <span id="noteMessage2"> <?= Yii::t('frontend','Please be sure to type a note.')?></span> </div> <div class="panel panel-default"> <!-- Default panel contents --> <div class="panel-heading" role="tab" id="headingNote" > <div class="row"> <div class="col-lg-10 col-md-10 col-xs-10"><h4 class="meeting-view"> <a role="button" data-toggle="collapse" data-parent="#accordion" href="#collapseNote" aria-expanded="true" aria-controls="collapseNote"><?= Yii::t('frontend','Notes') ?></a></h4> <span class="hint-text"><?= Yii::t('frontend','send a message to others') ?></span> </div> <div class="col-lg-2 col-md-2 col-xs-2" > <div style="float:right;"> <?= Html::a('', 'javascript:void(0);', ['class' => 'btn btn-primary glyphicon glyphicon-plus', 'title'=>'Edit','onclick'=>'showNote();']); ?> </div> </div> </div> </div> <div id="collapseNote" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="headingNote"> <div class="panel-body nopadding"> <div id="editNote" class="hidden"> <?= $this->render('_form', [ 'model' => $model, ]) ?> </div> </div> <div id ="noteThread" class="nopadding"> <?= $this->render('_thread', [ 'model' => $model, 'noteProvider'=>$noteProvider, ]) ?> </div> </div> </div>
这是查找空白便笺、显示适当错误或通过 Ajax 提交内容以请求便笺线程更新并显示成功警报的 jQuery:
function updateNote(id) { note = $('#meeting-note').val(); if (note =='') { displayAlert('noteMessage','noteMessage2'); return false; } // ajax submit subject and message $.ajax({ url: $('#url_prefix').val()+'/meeting-note/updatenote', data: {id: id, note: note}, success: function(data) { $('#editNote').addClass("hidden"); $('#meeting-note').val(''); updateNoteThread(id); displayAlert('noteMessage','noteMessage1'); return true; } // to do - error display flash }); } function updateNoteThread(id) { // ajax submit subject and message $.ajax({ url: $('#url_prefix').val()+'/meeting-note/updatethread', data: {id: id}, type: 'GET', success: function(data){ $('#noteThread').html(data); // data['responseText'] }, error: function(error){ } }); }
其中一项待办事项是处理 Ajax 中的错误。做到这一点并不容易,并且需要一个相当详细的架构来支持这一点——现在,我已经继续前进,没有处理这种错误。
这displayAlert()
是我为所有各种面板及其警报消息重用和构建的 JavaScript 函数:
function displayAlert(alert_id,msg_id) { // which alert box i.e. which panel alert switch (alert_id) { case 'noteMessage': // which msg to display switch (msg_id) { case 'noteMessage1': $('#noteMessage1').removeClass('hidden'); // will share the note $('#noteMessage2').addClass('hidden'); $('#noteMessage').removeClass('hidden').addClass('alert-info').removeClass('alert-danger'); break; case 'noteMessage2': $('#noteMessage1').addClass('hidden'); $('#noteMessage2').removeClass('hidden'); // no note $('#noteMessage').removeClass('hidden').removeClass('alert-info').addClass('alert-danger'); break; } break; case 'participantMessage': // which msg to display $('#participantMessageTell').addClass('hidden'); // will share the note $('#participantMessageError').addClass('hidden'); $('#participantMessageOnlyOne').addClass("hidden"); $('#participantMessageNoEmail').addClass("hidden"); switch (msg_id) { case 'participantMessageTell': $('#participantMessageTell').removeClass('hidden'); // will share the note $('#participantMessage').removeClass('hidden').addClass('alert-info').removeClass('alert-danger'); break; case 'participantMessageError': $('#participantMessageError').removeClass("hidden"); $('#participantMessage').removeClass("hidden").removeClass('alert-info').addClass('alert-danger'); break; case 'participantMessageNoEmail': $('#participantMessageNoEmail').removeClass("hidden"); $('#participantMessage').removeClass("hidden").removeClass('alert-info').addClass('alert-danger'); break; case 'participantMessageOnlyOne': $('#participantMessageOnlyOne').removeClass("hidden"); $('#participantMessage').removeClass("hidden").removeClass('alert-info').addClass('alert-danger'); break; } break; case 'placeMessage': // which msg to display $('#placeMsg1').addClass('hidden'); // will share the note $('#placeMsg2').addClass('hidden'); // will share the note $('#placeMsg3').addClass('hidden'); // will share the note switch (msg_id) { case 'placeMsg1': $('#placeMsg1').removeClass('hidden'); // will share the note $('#placeMessage').removeClass('hidden').addClass('alert-info').removeClass('alert-danger'); break; case 'placeMsg2': $('#placeMsg2').removeClass('hidden'); // will share the note $('#placeMessage').removeClass('hidden').removeClass('alert-info').addClass('alert-danger'); break; } break; case 'timeMessage': // which msg to display $('#timeMsg1').addClass('hidden'); // will share the note $('#timeMsg2').addClass('hidden'); // will share the note //$('#timeMsg3').addClass('hidden'); // will share the note switch (msg_id) { case 'timeMsg1': $('#timeMsg1').removeClass('hidden'); // will share the note $('#timeMessage').removeClass('hidden').addClass('alert-info').removeClass('alert-danger'); break; case 'timeMsg2': $('#timeMsg2').removeClass('hidden'); // will share the note $('#timeMessage').removeClass('hidden').removeClass('alert-info').addClass('alert-danger'); break; } break; } }
更新笔记主题
当用户提交新笔记时,actionUpdatethread()
会通过 Ajax 调用 MeetingNoteController.php。这是PHP:
public function actionUpdatethread($id) { Yii::$app->response->format = \yii\web\Response::FORMAT_JSON; $m=Meeting::findOne($id); $noteProvider = new ActiveDataProvider([ 'query' => MeetingNote::find()->where(['meeting_id'=>$id]), 'sort'=> ['defaultOrder' => ['created_at'=>SORT_DESC]], ]); $result = $this->renderPartial('_thread', [ 'model' =>$m, 'noteProvider' => $noteProvider, ]); return $result; }
我有时尝试简单地返回最新的内容项(即最新的注释)并插入到先前的项之上。然而,事实证明这很麻烦,尤其是日期时间和地点显示在表格行中。
现在,我替换了整个更新的内容线程,并通过 Ajax 替换了整个面板。这是加载所有笔记的 _thread.php 部分,包括新的:
<?php use yii\widgets\ListView; use yii\helpers\Html; if ($timeProvider->count>0): ?> <table class="table"> <?= ListView::widget([ 'dataProvider' => $timeProvider, 'layout' => '{items}', 'itemView' => '_list', ]) ?> </table> <?php endif; ?>
我希望今天的学习和尝试就足够了。
我确实花了大约五到七个漫长的编码日,包括通宵完成这一集和即将到来的一集背后的所有代码。我已经好几年没有熬夜了。尽管如此,结果仍然令人鼓舞。
下一个是什么?
我希望了解 Yii 和 PHP 的 Ajax 开发基础知识会有所帮助。通过这个过程,我当然学到了很多东西,这些变化使得安排会议比以前更快、更容易。
- 背景
- 编辑会议主题
- 预加载所有javascript
- 加载主题面板
- 通过 Ajax 更新主题和会议详细信息
- 到目前为止我学到了什么
- 发送会议记录
- 通过 Ajax 编写注释
- 更新笔记主题
- 下一个是什么?