Инструменты пользователя

Инструменты сайта


eaze:samples:добавление_фотогалереи_в_vt_к_объекту

Различия

Здесь показаны различия между двумя версиями данной страницы.

Ссылка на это сравнение

Both sides previous revision Предыдущая версия
Следущая версия
Предыдущая версия
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/20 19:20]
sergeyfast
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/21 11:30] (текущий)
sergeyfast
Строка 23: Строка 23:
 {{ :​eaze:​samples:​album.png?​ |}} {{ :​eaze:​samples:​album.png?​ |}}
  
-Вот что в итоге у нас должно получится (вид с отображением ошибок). ​+Вот что в итоге у нас должно получиться (вид с отображением ошибок). ​
 > У альбома есть название и описание (необязательное поле). ​ > У альбома есть название и описание (необязательное поле). ​
 > У фотографии есть маленькая и большая картинка,​ из необязательных полей - название. > У фотографии есть маленькая и большая картинка,​ из необязательных полей - название.
Строка 50: Строка 50:
 ===== data.tmpl.php ===== ===== data.tmpl.php =====
  
-В шаблоне data.tmpl.php+В шаблоне data.tmpl.php ​добавим новый таб "​Фотографии"​. 
 +<code php> 
 +<ul class="​tabs-list">​ 
 +    <​li><​a href="#​page-0">​{lang:​vt.common.commonInfo}</​a></​li>​ 
 +    <​li><​a href="#​page-1">​Фотографии</​a></​li>​ 
 +</​ul>​ 
 +</​code>​ 
 + 
 +И подключим его после ''<​nowiki><​div id="​page-0"​ class="​tab-page rows">​...</​div></​nowiki>''​ 
 +<​code>​ 
 +{increal:​tmpl://​vt/​entities/​photos.tmpl.php} 
 +</​code>​ 
 + 
 +На этом работа с файлом закончена. 
 + 
 +===== photos.tmpl.php ===== 
 +Данный шаблон будет работать с двумя переменными из Response: ''​$data''​ и ''​$albumErrors''​.  
 + 
 +В ''​$data''​ содержится подготовленный массив с альбомами и фотографиями,​ который мы будем отдавать в качестве источника данных для JQuery Templates. Можно было бы конечно сделать сразу ''​json_encode( $object->​albums )'',​ но так мы получим избыток данных. В подготовленной переменной такого нет. Во время создания шаблона в ''​var data = ..''​ лежал заранее известный массив,​ который потом стал основой для php-массива (с помощью заранее готового массива можно проще отлаживать шаблон). 
 + 
 +В ''​$albumErrors''​ находится массив с ошибками валидации альбомов и фотографий. Его нельзя положить в обычный массив ''​$errors'',​ т.к. у него своя сложная структура. 
 + 
 +<code php> 
 +<? /** @var array $data  */ ?> 
 +<? /** @var array $albumErrors */ ?> 
 +<? JsHelper::​PushFile( '​js://​vt/​entity-photos.js'​ ); ?> 
 +<div id="​page-1"​ class="​tab-page rows albums">​ 
 +    <div data-row="​photos"​ style="​display:​ none;"></​div>​ 
 +    <div class="​row">​ 
 +        <ul class="​actions">​ 
 +            <li class="​edit"><​a href="#"​ id="​add-album">​Добавить альбом</​a></​li>​ 
 +        </​ul>​ 
 +    </​div>​ 
 +</​div>​ 
 + 
 +<script type="​text/​javascript">​ 
 +    var data = <?= ObjectHelper::​ToJSON( $data ); ?>; 
 +    <? if ( !empty( $albumErrors ) ) { ?> 
 +    var albumErrors = <?= ObjectHelper::​ToJSON( $albumErrors )?> 
 +    <? } ?> 
 +</​script>​ 
 +</​code>​ 
 + 
 +Создадим шаблон ''​albumTemplate'',​ который будет инициализироваться строчкой (entity-photos.js) 
 +<code javascript>​$("#​albumTemplate"​).tmpl(data,​ { counter: albumCounter,​ photoCounter:​ photoCounter } ).appendTo("​.albums"​);</​code>​ 
 +  * ''​data''​ - наш источник данных. 
 +  * ''​albumCounter''​ - объект,​ с помощью которого мы будем держать текущий индекс альбома (''​photoCounter''​ - аналогично с фото). Данный объект передается в tmpl() в качестве параметра для рендеринга (доступен в $item в шаблоне). 
 +<code javascript>​ 
 +    /** 
 +     * Counters for album index 
 +     */ 
 +    var Counter = function() { 
 +        this.index = -1; 
 +    } 
 + 
 +    Counter.prototype.nextIndex = function() { 
 +        this.index ++; 
 +        return this.index;​ 
 +    } 
 + 
 +    var albumCounter = new Counter();​ 
 +    var photoCounter = new Counter();​ 
 +</​code>​ 
 + 
 +Опишем шаблон для создания строчки с альбомом. 
 +<code html> 
 +<script id="​albumTemplate"​ type="​text/​x-jquery-tmpl">​ 
 +    <div class="​row album sort" id="​album-${ $item.counter.nextIndex() }">​ 
 +        <input type="​hidden"​ name="​{$prefix}[albums][${ $item.counter.index }][entityAlbumId]"​ value="​${id}"​ /> 
 +        <br /> 
 +        <table class="​objects"​ style="​width:​auto;">​ 
 +            <​tbody>​ 
 +            <​tr>​ 
 +                <td class="​handle">​Альбом</​td>​ 
 +                <td class="​left"><​input type="​text"​ name="​{$prefix}[albums][${ $item.counter.index }][title]"​ value="​${title}"​ /></​td>​ 
 +                <td colspan="​2"​ class="​left"><​input type="​text"​ name="​{$prefix}[albums][${ $item.counter.index }][description]"​ value="​${description}"​ size="​60"​ style="​width:​ auto;" /></​td>​ 
 +                <td width="​10%">​ 
 +                    <ul class="​actions">​ 
 +                        <li class="​edit"><​a href="#"​ class="​add-photo"​ data-album-id="​${ $item.counter.index }">​Добавить</​a></​li>​ 
 +                        <li class="​delete"><​a href="#"​ class="​delete-album"​ title="​Удалить"​ data-album-id="​${ $item.counter.index }">​Удалить</​a></​li>​ 
 +                    </​ul>​ 
 +                </​td>​ 
 +            </​tr>​ 
 +            {{each(i, photo) photos}} 
 +                {{tmpl( photo , { counter: $item.photoCounter,​ albumIndex: $item.counter.index }) "#​photoTemplate"​}} 
 +            {{/each}} 
 +            </​tbody>​ 
 +        </​table>​ 
 +    </​div>​ 
 +</​script><​script>''</​script>​ 
 +</​code>​ 
 + 
 +  * ''​${ $item.counter.nextIndex() }''​ - пробелы между {} нужны для того, чтобы шаблонизатор в Eaze не подумал,​ что это переменная ''​{$item}''​. 
 +  * ''</​script><​script><​nowiki>''</​nowiki></​script>''​ - [[http://​youtrack.jetbrains.net/​issue/​IDEA-73686|workaround]] для IDE. 
 +  * ''​${id}''​ - получение свойства id из элемента массива data. 
 +  * ''<​nowiki>​{{each(i,​ photo) photos}}</​nowiki>''​ - цикл в шаблоне для отображения фотографий. ''​photos''​ - свойство из элемента массива data. 
 +  * ''<​nowiki>​{{tmpl( photo , { counter: $item.photoCounter,​ albumIndex: $item.counter.index }) "#​photoTemplate"​}}</​nowiki>''​ - вызов шаблона photoTemplate,​ передача дополнительных параметров для рендеринга. 
 +  * ''​{$prefix}''​ - стандартная переменная для data.tmpl.php (префикс объекта). 
 + 
 +Шаблон для отображения конкретной фотографии 
 +<code html> 
 +<script id="​photoTemplate"​ type="​text/​x-jquery-tmpl">​ 
 +    <tr class="​sort">​ 
 +        <​td><​input type="​hidden"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.nextIndex() }][entityPhotoId]"​ value="​${id}"​ /></​td>​ 
 +        <​td><​input type="​text"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][title]"​ value="​${title}"​ class="​title-${ $item.albumIndex }-${num}"​ size="​60"​ style="​width:​ auto;" /></​td>​ 
 +        <td class="​left"><​input type="​hidden"​ class="​vfsFile"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][smallImageId]"​ rel="​smallImageId-${ $item.albumIndex }-${num}"​ id="​photo-small-${ $item.counter.index }" vfs:​previewType="​image" ​ {{if smallImage}}value="​${ smallImage.id}"​ vfs:​src="​{web:​vfs://​}${ smallImage.src}"​ vfs:​name="​${ smallImage.name}"​{{/​if}}/></​td>​ 
 +        <td class="​left"><​input type="​hidden"​ class="​vfsFile"​ name="​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][bigImageId]"​ rel="​bigImageId-${ $item.albumIndex }-${num}"​ id="​photo-big-${ $item.counter.index }" vfs:​previewType="​image"​ {{if bigImage}}value="​${ bigImage.id}"​ vfs:​src="​{web:​vfs://​}${ bigImage.src}"​ vfs:​name="​${ bigImage.name}"​{{/​if}}/></​td>​ 
 +        <td width="​10%">​ 
 +            <ul class="​actions">​ 
 +                <li class="​delete"><​a class="​delete-photo"​ href="#"​ title="​Удалить">​Удалить</​a></​li>​ 
 +            </​ul>​ 
 +        </​td>​ 
 +    </​tr>​ 
 +</​script><​script>''</​script>​ 
 +</​code>​ 
 +  * ''​${num}''​ - это дополнительное свойство (индекс массива),​ который используется для поиска элемента при отображении ошибок (''​$item.counter.nextIndex()''​ имеет сквозную нумерацию,​ а ''​${num}''​ начинается с 0 для каждого альбома). 
 + 
 +На этом работа с photos.tmpl.php завершена. 
 + 
 +===== entity-photos.js ===== 
 +//​Данный файл не претендует на идеальность :)// 
 + 
 +Начинается данный файл с описания счетчиков для альбомов и фотографий (см. код выше). 
 + 
 +После этого опишем поведение кнопок добавления и удаления альбомов и фотографий 
 +<code javascript>​ 
 +    /** 
 +     * Album Controls 
 +     */ 
 +    $(function() { 
 +        $("#​add-album"​).live('​click',​ function(e) { 
 +            $("#​albumTemplate"​).tmpl(null,​ { counter: albumCounter } ).appendTo("​.albums"​);​ 
 +            rebindAlbumControls();​ 
 +            e.preventDefault();​ 
 +        }); 
 + 
 +        $("​.add-photo"​).live('​click',​ function(e) { 
 +            var id = $(this).data("​albumId"​);​ 
 +            $("#​photoTemplate"​).tmpl( null, { counter: photoCounter,​ albumIndex: id } ).appendTo("#​album-"​ + id + " .objects"​);​ 
 +            vfsSelector.Init();​ 
 +            e.preventDefault();​ 
 +        }); 
 + 
 +        $("​.delete-photo"​).live('​click',​ function(e) { 
 +            $(this).parent().parent().parent().parent().remove();​ 
 +            e.preventDefault();​ 
 +        }); 
 + 
 +        $("​.delete-album"​).live('​click',​ function(e) { 
 +            var id = $(this).data("​albumId"​);​ 
 +            $("#​album-"​ + id).remove();​ 
 +            e.preventDefault();​ 
 +        }); 
 +    }); 
 +</​code>​ 
 + 
 +''​rebindAlbumControls()''​ необходимо для переинициализации vfs и сортировок 
 +<code javascript>​ 
 +    function rebindAlbumControls() { 
 +        vfsSelector.Init();​ 
 + 
 +        $('​.objects'​).sortable({ 
 +            '​items':​ '​tr.sort'​ 
 +            , '​forceHelperSize':​ true 
 +            , '​forcePlaceholderSize':​ true 
 +            , '​handle':​ '​td:​first-child'​ 
 +        }); 
 + 
 +        $('​.albums'​).sortable({ 
 +            '​items':​ '​.album'​ 
 +            , '​forceHelperSize':​ true 
 +            , '​forcePlaceholderSize':​ true 
 +            , '​handle':​ '​.handle'​ 
 +        }); 
 +    } 
 +</​code>​ 
 + 
 +При загрузки страницы запустим рендеринг 
 +<code javascript>​ 
 +    $(function() { 
 +        $("#​albumTemplate"​).tmpl(data,​ { counter: albumCounter,​ photoCounter:​ photoCounter } ).appendTo("​.albums"​);​ 
 +        rebindAlbumControls();​ 
 +        displayAlbumErrors();​ 
 +    }); 
 +</​code>​ 
 + 
 +И отобразим ошибки 
 +<code javascript>​ 
 +    function displayAlbumErrors() { 
 +        if ( typeof( albumErrors ) == '​undefined'​ ) { 
 +            return; 
 +        } 
 + 
 +        $.each( albumErrors,​ function( albumIndex, album ) { 
 +            $.each( album, function( albumField, albumData ) { 
 +                if ( albumField == '​photos'​ ) { 
 +                    $.each( albumData, function(photoIndex,​ photoField ){ 
 +                        $.each( photoField, function( fieldName, fieldError ) { 
 +                            $( '​[rel='​ + fieldName + '​-'​ + albumIndex + '​-'​ + photoIndex + '​]'​ ).parent().append( ​ ' <span style="​color:​red;">​*</​span>'​ ); 
 +                        }); 
 +                    }); 
 +                } else { 
 +                    $("​input[name='​entity[albums]["​ + albumIndex + "​]["​+ albumField + "​]'​]"​).parent().append( ' <span style="​color:​red;">​*</​span>'​ ); 
 +                } 
 +            }); 
 +        }); 
 +    } 
 +</​code>​ 
 + 
 +Со клиентской частью мы закончили. 
 +Данного кода достаточно для управления альбомами без серверной части, только они не будут сохранятся и валидироваться,​ но в ''​$object''​ данные попадут,​ т.к. мы используем полные пути к свойствам объекта (например ''​{$prefix}[albums][${ $item.albumIndex }][photos][${ $item.counter.index }][smallImageId]''​ ) - GetFromRequest соберет объект так, как нужно. 
 + 
 +Попробуем модифицировать SaveEntityAction таким образом,​ чтобы он сохранял альбомы в базу. 
 + 
 +===== SaveEntityAction.php ===== 
 +Прежде всего мы должны помнить о том, что albums у объекта Entity - лист. ''​EntityFactory::​GetById( $id, array( BaseFactory::​WithLists => true ) )''​ выберет только альбомы,​ без фотографий. Фотографии будем выбрать вручную. 
 +Создадим метод ''​refillAlbumPhotos()'',​ который добавляет к альбомам фотографии. 
 +<code php> 
 +protected function refillAlbumPhotos( $object ) { 
 +    $photos = EntityPhotoFactory::​Get( array( '​entityId'​ => $object->​entityId ) ); 
 +    $photos = ArrayHelper::​Collapse( $photos, '​entityAlbumId'​ ); 
 + 
 +    if ( !empty( $photos ) ){ 
 +        foreach( $object->​albums as $album ) { 
 +            if ( isset( $photos[$album->​entityAlbumId] ) ) { 
 +                $album->​photos = $photos[$album->​entityAlbumId];​ 
 +            } 
 +        } 
 +    } 
 +
 +</​code>​ 
 +  * дополнительно в EntityPhotoFactory мы добавили поиск по entityId. 
 + 
 +Далее сделаем так, чтобы фотографии грузились только тогда, когда мы открыли объект для редактирования. 
 +<code php> 
 +/** 
 + * Set Json Albums Data to Template 
 + * @return void 
 + */ 
 +protected function beforeSave() { 
 +    if ( $this->​action != self::​UpdateAction && !empty( $this->​currentObject->​entityId ) ) { 
 +        $this->​refillAlbumPhotos( $this->​currentObject ); 
 +    } 
 + 
 +    Response::​setParameter( '​data',​ EntityAlbumUtility::​PrepareAlbumsData( $this->​currentObject ) ); 
 +
 +</​code>​ 
 +  * EntityAlbumUtility::​PrepareAlbumsData() - метод, который подготавливает переменную ''​data''​ для шаблона photos.tmpl.php. 
 + 
 +Теперь,​ если у объекта есть альбомы и фотографии,​ то мы уже увидим их в шаблоне :). 
 + 
 +Добавим валидацию альбомов и фотографий. 
 +<code php> 
 +protected function validate( $object ) { 
 +    $errors = parent::​$factory->​Validate( $object ); 
 + 
 +    $albumErrors = EntityAlbumUtility::​ValidateAlbums( $object->​albums ); 
 +    if ( !empty( $albumErrors ) ) { 
 +        $errors['​fields'​]['​photos'​]['​format'​] = '​format';​ 
 +        Response::​setParameter( '​albumErrors',​ $albumErrors ); 
 +    } 
 + 
 +    return $errors; 
 +
 +</​code>​ 
 +  * fields => photos поможет нам подсветить таб "​Фотографии",​ если на нем были ошибки (''<​div data-row="​**photos**"​...''​ в photos.tmpl.php) и не дать сохранить ошибочный объект в базу. 
 + 
 +Теперь можно перейти к сохранению. Для этого сначала нужно заполнить $originalObject фотографиями (в оригинальном объекте они нужны для того, чтобы удалить те фотографии,​ которые мы удалили с формы). 
 +<code php> 
 +protected function beforeAction() { 
 +    if ( !empty( $this->​originalObject ) ) { 
 +       ​$this->​refillAlbumPhotos( $this->​originalObject ); 
 +   } 
 +
 +</​code>​ 
 + 
 +add() и update() будут обернуты в транзакцию. 
 +<code php> 
 +/** 
 + * Add Object 
 + * 
 + * @param Entity $object 
 + * @return bool 
 + */ 
 +protected function add( $object ) { 
 +    ConnectionFactory::​BeginTransaction();​ 
 + 
 +    $result = parent::​$factory->​Add( $object, array( BaseFactory::​WithReturningKeys => true ) ); 
 +    $result = $result && EntityAlbumUtility::​SaveAlbums( $object, $this->​originalObject ); 
 + 
 +    ConnectionFactory::​CommitTransaction( $result ); 
 + 
 +    return $result; 
 +
 + 
 + 
 +/** 
 + * Update Object 
 + * 
 + * @param Entity $object 
 + * @return bool 
 + */ 
 +protected function update( $object ) { 
 +    ConnectionFactory::​BeginTransaction();​ 
 + 
 +    $result = parent::​$factory->​Update( $object ); 
 +    $result = $result && EntityAlbumUtility::​SaveAlbums( $object, $this->​originalObject ); 
 + 
 +    ConnectionFactory::​CommitTransaction( $result ); 
 +     
 +    return $result; 
 +
 +</​code>​ 
 + 
 +Последний штрих. При сохранении фоток мы не получаем их идентификаторы,​ из-за этого неправильно работает кнопка "​Применить"​ в режиме редактирование. Исправляется это путем переполучения фотографий только при успешном сохранении. 
 +<code php> 
 +protected function afterAction( $success ) { 
 +    if ( $this->​redirect == '​view'​ && $success ) { 
 +        $this->​refillAlbumPhotos( $this->​currentObject ); 
 +        Response::​setParameter( '​data',​ EntityAlbumUtility::​PrepareAlbumsData( $this->​currentObject ) ); 
 +    } 
 +
 +</​code>​ 
 + 
 +Не забудьте посмотреть код [[eaze:​samples:​добавление_фотогалереи_в_vt_к_объекту_EntityAlbumUtility.php|EntityAlbumUtility.php]]. 
 + 
 + 
 +====== Итог ====== 
 +Поставленной цели мы добились. Можно было бы конечно сделать массовый загрузчик на flash и потом и мышкой раскидать фотографии по альбомам,​ но для этого нужно написать ещё больше javascript'​а. 
 + 
 +Осталось рассмотреть плюсы и минусы подхода. 
 + 
 +===== Плюсы ===== 
 +  * Не нужно дополнительно обрабатывать получение связанных объектов из формы на PHP (albums, photos). 
 +  * Не нужно дублировать шаблон отображения album и photo сначала в PHP, потом на JS. Всего используется один шаблон. 
 +  * Минимальное количество кода в SaveEntityAction (в основном - только получение листов второго и последующих уровней). 
 + 
 +===== Минусы ===== 
 +  * GetFromRequest на втором уровне получает каждый объект через GetById (соответственно сколько файлов - столько запросов при сохранении). //​можно исправить,​ но сложновато//​ 
 +  * Если сохранить страницу при выключенном JS - то все фотографии удалятся (потому что не пришли с формы). //​можно исправить через дополнительную переменную,​ которая выставляется через JS// 
 +  * При сохранении страницы для не измененных данных каждый раз выполняется UPDATE. //​можно исправить путем добавления проверки на эквивалентность объектов//​ 
 + 
 +//Ещё раз повторюсь,​ что данный пример не претендует на идеальность и универсальность. Он пытается рассказать,​ как работать в VT со сложными объектами. Пожелания и дополнения приветствуются. ;)// 
 + 
 + 
 + 
 + 
 + 
  
  
eaze/samples/добавление_фотогалереи_в_vt_к_объекту.1316532050.txt.gz · Последние изменения: 2011/09/20 19:20 — sergeyfast