天天看點

第十天--使用Ajax表單修改資料

通過昨天對已知技術的回顧,我們已經熟悉互動的使用。顯示豐富格式的問題與清單,甚至是分頁,并不足以使一個程式鮮活。而askeet概念的核心就是允許任何注冊的使用者詢問一個新問題,而任何使用者可以回答已存在的問題。現在是我們實作的時候了。

添加一個新問題

我們在第七天所建立的側邊欄已經包含一個添加新問題的連結。他連結到question/add動作,這正是我們将要開發的。

限制到注冊使用者的通路

首先,隻有注冊使用者可以添加一個新問題。為了限制到question/add動作的通路,在askeet/apps/frontend/modules/question/config/目錄下建立一個security.yml檔案:

add:

  is_secure:   on

  credentials: subscriber

all:

  is_secure:   off

當一個非注冊使用者試着通路一個限制動作時,Symfony将其重定向到登陸動作。這個動作必須在程式設定中進行定義,在login_module與login_action關鍵字之後:

all:

  .actions:

    login_module:           user

    login_action:           login

關于動作通路的限制的更多資訊可以在Symfony一書的安全一章進行了更多的解釋。

addSuccess.php模闆

question/add動作将會同時顯示表單與處理表單。這就意味着要顯示表單,我們隻需要一個空動作。另外,如果表單驗證出錯,還會再次顯示表單:

public function executeAdd()

{

}

public function handleErrorAdd()

{

  return sfView::SUCCESS;

}

所有的動作都會輸出addSuccess.php模闆:

<?php use_helper('Validation') ?>

<?php echo form_tag('@add_question') ?>

  <fieldset>

  <div class="form-row">

    <?php echo form_error('title') ?>

    <label for="title">Question title:</label>

    <?php echo input_tag('title', $sf_params->get('title')) ?>

  </div>

  <div class="form-row">

    <?php echo form_error('body') ?>

    <label for="label">Your question in details:</label>

    <?php echo textarea_tag('body', $sf_params->get('body')) ?>

  </div>

  </fieldset>

  <div class="submit-row">

    <?php echo submit_tag('ask it') ?>

  </div>

</form>

title與body控件都有一個預設值(表單幫助器的第二個參數),其值由請求的表單參數進行定義。為什麼是這樣呢?因為我們要為表單添加一個驗證檔案。如果驗證失敗,表單就會再次顯示,而前面的使用者實體仍存在于請求參數中。他們可以用作表單元素的預設值。

前面的實體并不會因為表單驗證失敗而丢失。這是我們對于一個使用者友好程式的最低要求。

但是,為了達到這個目的,我們需要一個表單驗證檔案。

表單驗證

在question子產品下建立一個validate/目錄,并且添加一個add.yml驗證檔案:

methods:

  post:            [title, body]

names:

  title:

    required:      Yes

    required_msg:  You must give a title to your question

  body:

    required:      Yes

    required_msg:  You must provide a brief context for your question

    validators:    bodyValidator

bodyValidator:

    class:         sfStringValidator

    param:

      min:         10

      min_error:   Please, give some more details

如果我們要了解更多的關于表單驗證的資訊,我們回到第6天,或是閱讀Symfony一書的表單驗證一章。

處理表單送出

現在編輯question/add動作來處理表單送出:

public function executeAdd()

{

  if ($this->getRequest()->getMethod() == sfRequest::POST)

  {

    // create question

    $user = $this->getUser()->getSubscriber();

    $question = new Question();

    $question->setTitle($this->getRequestParameter('title'));

    $question->setBody($this->getRequestParameter('body'));

    $question->setUser($user);

    $question->save();

    $user->isInterestedIn($question);

    return $this->redirect('@question?stripped_title='.$question->getStrippedTitle());

  }

}

記住,->setTitle()方不同時也會設定stripped_title,而->setBody()方法同時也會設定html_body域,因為我們在Question.php子產品類中重寫了這兩個方法。建立問題的使用者将會聲明對其感興趣。這是為了避免出現沒有人對其感興趣的問題,為樣就太悲哀了。

動作的結束處包含了一個->redirect()方法來重定向到建立問題的詳細頁面。比起->forward()方法,其最大的優點在于,如果使用者以後重新整理詳細問題頁面,表單不會再次送出。另外,'back'按鈕也會按所希望的方式工作。這是一條通常的規則:我們不要使用->forward()方法來結束表單送出的處理動作。

最好是如果表單不是在POST模式,動作仍然可以顯示表單。其形為正如同我們在前面編寫的空動作,傳回sfView::SUCCESS,啟動addSuccess.php模闆。

不要忘記在User子產品中建立珍上isInterestedIn()方法:

public function isInterestedIn($question)

{

  $interest = new Interest();

  $interest->setQuestion($question);

  $interest->setUserId($this->getId());

  $interest->save();

}

作為一次重構,我們可以在user/interested動作中使用這個方法來替換完在同樣事情的代碼。現在可以進行測試了。使用一個測試使用者,我們可以添加一個問題。

添加一個新答案

答案的添加将會以一種略微不同的方式來實作。在這裡并沒有必要使用一個表單将使用者重定向到一個新頁面,而是再一次到要顯示答案的另一個頁面。是以新的答案表單應是Ajax形式,而且新答案應立即顯示在問題詳細頁面。

添加一個Ajax表單

用下面的代碼來更改modules/question/templates/showSuccess.php模闆的結束處:

...    

<div id="answers">

<?php foreach ($question->getAnswers() as $answer): ?>

  <div class="answer">

  <?php include_partial('answer/answer', array('answer' => $answer)) ?>

  </div>

<?php endforeach; ?>

<?php echo use_helper('User') ?>

<div class="answer" id="add_answer">

  <?php echo form_remote_tag(array(

    'url'      => '@add_answer',

    'update'   => array('success' => 'add_answer'),

    'loading'  => "Element.show('indicator')",

    'complete' => "Element.hide('indicator');".visual_effect('highlight', 'add_answer'),

  )) ?>

    <div class="form-row">

      <?php if ($sf_user->isAuthenticated()): ?>

        <?php echo $sf_user->getNickname() ?>

      <?php else: ?>

        <?php echo 'Anonymous Coward' ?>

        <?php echo link_to_login('login') ?>

      <?php endif; ?>

    </div>

    <div class="form-row">

      <label for="label">Your answer:</label>

      <?php echo textarea_tag('body', $sf_params->get('body')) ?>

    </div>

    <div class="submit-row">

      <?php echo input_hidden_tag('question_id', $question->getId()) ?>

      <?php echo submit_tag('answer it') ?>

    </div>

  </form>

</div>

</div>

重構

link_to_login()函數必須添加到UserHelper.php幫助器中:

function link_to_login($name, $uri = null)

{

  if ($uri && sfContext::getInstance()->getUser()->isAuthenticated())

  {

    return link_to($name, $uri);

  }

  else

  {

    return link_to_function($name, visual_effect('blind_down', 'login', array('duration' => 0.5)));

  }

}

這個函數所做的事情是我們在其他的User幫助器中已經看到過的:如果使用者已被授權,将會顯示一個到動作的連結,而如果沒有授權,這個連結指向Ajax登陸表單。是以在link_to_user_interested()和link_to_user_relevancy()函數中使用link_to_login()來代替link_to_function()函數。不要忘記在modules/sidebar/templatetes/defaultSuccess.php中到@add_question的連結。是的,這就是重構。

處理表單送出

雖然他仍是調用代碼片段,我們在這裡選擇的用來處理Ajax請求的方法與我們在第八天所描述的還是有一些略微的不同。這是因為我們希望表單送出的結果實際替換表單。這就是為什麼将form_remote_tag()幫助器中的更新參數指向表單本身的容器,而不是指向外層空間。_answer.php片段将會包含在答案添加動作的結果中,是以最終的結果看起來如同下面的樣子:

...

<div id="answers">

  <!-- Answer 1 -->

  <!-- Answer 2 -->

  <!-- Answer 3 -->

  ...

</div>

<div class="answer" id="add_answer">

  <!-- The new answer -->

</div>

也許我們已經猜到了form_remote_tag() javascript是如何工作的了:他通過一個XMLHttpRequest對象将表單送出給url參數中指定的動作。動作的結果替換更新參數中指定的元素。而且,與第八天中的link_to_remote()幫助器相類似,他會激活活動訓示器可見,并且依據請求的送出使其不可郵,而且在Ajax事務的結束處高亮顯示更新的部分。

在這裡我們要多說幾名關于與一個新問題相關聯的使用者。我們在前面提到,答案必須連結到一個使用者。如果使用者已經被驗證,那麼他的user_id就會用于新答案。否則,就會使用anonymous,除非使用者選擇登陸。位于GlobalHelper.php幫助器中的link_to_login()幫助器就會激活布局中的隐藏表單。我們可以遊覽askeet源友來檢視其代碼。

answer/add動作

作為Ajax表單的url參數的@add_answer規則指向answer/add動作:

add_answer:

  url:   /add_anwser

  param: { module: answer, action: add }

下面是動作的内容:

public function executeAdd()

{

  if ($this->getRequest()->getMethod() == sfRequest::POST)

  {

    if (!$this->getRequestParameter('body'))

    {

      return sfView::NONE;

    }

    $question = QuestionPeer::retrieveByPk($this->getRequestParameter('question_id'));

    $this->forward404Unless($question);

    // user or anonymous coward

    $user = $this->getUser()->isAuthenticated() ? $this->getUser()->getSubscriber() : UserPeer::retriveByNickname('anonymous');

    // create answer

    $this->answer = new Answer();

    $this->answer->setQuestion($question);

    $this->answer->setBody($this->getRequestParameter('body'));

    $this->answer->setUser($user);

    $this->answer->save();

    return sfView::SUCCESS;

  }

  $this->forward404();

}

 首先,如果這個動作不是在POST模式下調用的,這就意味着是某些人在浏覽器的位址欄中輸入的。動作并不是為這種請類型而設定的,是以在這種情況下會傳回一個404錯誤。

為了确定要設定為答案作者的使用者,動作會檢測目前使用者是否已被授權。如果不是這種情況,動作就會通地UserPeer類的new::retrieveByNickname()方法使用'Anonymous Coward'使用者。如果我們對這個方法還有疑惑,我們可以檢視其源代碼。

這樣,就準備好了所有事情來建立一個新問題,并且将請求發往addSuccess.php模闆。正如我們所希望的,這個模闆隻包含一行代碼:

<?php include_partial('answer', array('answer' => $answer)) ?>

我們同時需要在frontend/modules/answer/config/view.yml中禁止這個動作的布局:

addSuccess:

  has_layout: off

最後,如果使用者送出一個空答案,我們并不要進行存儲。是以就會略地資料處理部分,而動作并不會傳回任何内容,這将簡單的清除頁面。我們已經在Ajax表單中完成了資料處理,但是這應将表單自身放在另一個片段中,而現在并不值得這樣做。

測試

這樣就完成了?正是,現在Ajax表單已經可以使用了,幹淨,安全。我們現在可以進行測試,顯示一個問題的答案清單,并且可以添加一個新問題。這個頁面并不需要重新整理,而新答案顯示在前面的清單的底部。這很簡單,不是嗎?

明天見

繼續閱讀