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

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


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

Различия

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

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

Both sides previous revision Предыдущая версия
Следущая версия
Предыдущая версия
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/20 19:24]
sergeyfast
eaze:samples:добавление_фотогалереи_в_vt_к_объекту [2011/09/21 11:30] (текущий)
sergeyfast
Строка 23: Строка 23:
 {{ :​eaze:​samples:​album.png?​ |}} {{ :​eaze:​samples:​album.png?​ |}}
  
-Вот что в итоге у нас должно получится (вид с отображением ошибок). ​+Вот что в итоге у нас должно получиться (вид с отображением ошибок). ​
 > У альбома есть название и описание (необязательное поле). ​ > У альбома есть название и описание (необязательное поле). ​
 > У фотографии есть маленькая и большая картинка,​ из необязательных полей - название. > У фотографии есть маленькая и большая картинка,​ из необязательных полей - название.
Строка 62: Строка 62:
 {increal:​tmpl://​vt/​entities/​photos.tmpl.php} {increal:​tmpl://​vt/​entities/​photos.tmpl.php}
 </​code>​ </​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_к_объекту.1316532276.txt.gz · Последние изменения: 2011/09/20 19:24 — sergeyfast