I've created a plugin that can add different content (text, map, etc.). I have registered Snippet in the plugin.
Plugin.php
...
public function registerComponents() {
return [
'Author\Contents\Components\Text' => 'textContent'
];
}
public function registerPageSnippets() {
return [
'Author\Contents\Components\Text' => 'textContent'
];
}
...
I created a text type component that created two properties (contentID [selector of existing text content], showTitle [selector, true or false]).
components/Text.php
...
public function defineProperties() {
return [
"contentId" => [
"title" => "Select content",
"type" => "dropdown",
"placeholder" => "Please select a content..."
],
"showTitle" => [
"title" => "Show content title",
"type" => "dropdown"
]
];
}
public function getContentIdOptions() {
return Texts::orderBy("title")->lists("title", "id");
}
public function getShowTitleOptions() {
return [
"hide" => "No",
"show" => "Yes"
];
}
...
public function onRender() {
$this->contentData = $this->page["contentData"] = Texts::find($this->property("contentId"));
foreach ($this->getProperties() as $key => $value) {
$this->page[$key] = $value;
}
}
components/text/default.htm
{{ showTitle }}
{{ __SELF__ }}
<div class="row">
<div class="col-12">
{{ contentData.content|raw }}
</div>
</div>
When I try to insert a snippet to a page (in Static Pages plugin), it works perfectly. However, if I change the code snippet settings (for example, I change the showTitle property value from true to false), the content does not appear in the front-end page. If I change anything compared to the created state, the content will disappear. Even if that property is not included in the code.
If I do not modify the property used when inserting, it generates html.
However, if I change, I see this in the inspector:
<figure data-component="Author\Contents\Components\Text" data-inspector-id="inspectorid-984837527907" data-property-contentid="6" data-property-showtitle="show" data-snippet="textContent"> </figure>
Interestingly, the snippet of the Rjgallery plugin does not work, it does not load the content either.
EDIT
If I wait for a few minutes after the change, the changes will take effect. Probably a cache... How to take shorter time?
What is the reason of this?
Related
I'm sure this is very simple but it's proving just a bit beyond me at the moment.
I have made a plugin that I would like to use for displaying galleries which is working fine. However, trying to add the options of the galleries that I have created in my component is proving to be difficult.
When I add the component to a page, I have now got the option to choose all the galleries that I created but displaying the gallery based upon which one I selected is what I have been unsuccessful in doing.
Any help would be greatly appreciated!
I'm sure this is very simple but it's proving just a bit beyond me at the moment.
I have made a plugin that I would like to use for displaying galleries which is working fine. However, trying to add the options of the galleries that I have created in my component is proving to be difficult.
When I add the component to a page, I have now got the option to choose all the galleries that I created but displaying the gallery based upon which one I selected is what I have been unsuccessful in doing.
Any help would be greatly appreciated!
Components/Gallery.php:
use Cms\Classes\ComponentBase;
use MartinSmith\Gallerys\Models\Gallery as GalleryModel;
class gallerys extends ComponentBase
{
public $gallery;
public function componentDetails(){
return [
'name' => 'Frontend Gallery',
'description' => 'A gallery for you webpage'
];
}
public function defineProperties() {
$lists = $this->getLists();
return [
'galleryName' => [
'title' => 'Gallery',
'type' => 'dropdown',
'placeholder' => 'Select Gallery',
'options' => $lists
]
];
}
public function getLists() {
$agreements = GalleryModel::all()->pluck('name', 'id');
return $agreements->toArray();
}
public function getList() {
$agreement = GalleryModel::where('id', $this->property('galleryName'))->get();
return $agreement->first();
}
}
Components/gallery/default.htm:
{% set gallerys = __SELF__.gallery %}
{% for gallery in gallerys %}
<div class="container-fluid px-0">
<div class="gallery">
<div class="row">
{% for image in gallery.fullImage %}
<div class="col-md-4 px-0 home-galleryImg">
<a href="{{ image.path }}">
<div class="gallery-imgOverlay">
<p>{{ image.title }}</p>
<h5>{{ image.description }}</h5>
</div>
<img class="img-fluid" src="{{ image.thumb(650,auto) }}" alt="{{ thumbnail.description }}">
</a>
</div>
{% endfor %}
</div>
</div>
</div>
{% endfor %}
See screenshot
I solved this for myself by creating a function that returns the "name" and indexed by the 'id' using the laravel pluck method. pluck('name', 'id') The first argument selects the column to use as the value and the second argument selects the column to use as a key. Note* the toArray() method I don't think the options field can take collections.
public function getLists() {
$agreements = Agreements::all()->pluck('agrnum', 'id');
return $agreements->toArray();
}
//returns
array:3 [▼
2 => "DLE-2"
4 => "DLE-1"
5 => "DLE-3"
]
Now in my properties area I call the function $list = $this->getList();
public function defineProperties() {
$lists = $this->getLists();
return [
'getList' => [
'title' => 'List',
'type' => 'dropdown',
'placeholder' => 'Select List',
'options' => $lists
]
];
}
After that you can proceed to do a Lists::where('id', $this->property('getList')); or something of that sort in a function to show the selected list or in your case gallery.
My results:
The CMS Page Backend from component
public function defineProperties() {
$lists = $this->getLists();
return [
'getList' => [
'title' => 'List',
'type' => 'dropdown',
'placeholder' => 'Select List',
'options' => $lists
]
];
}
public function getLists() {
$agreements = Agreements::all()->pluck('agrnum', 'id');
return $agreements->toArray();
}
public function getList() {
$agreement = Agreements::where('id', $this->property('getList'))->get();
return $agreement->first();
}
The Webpage from default.htm in the component template folder
{{ d(__SELF__.getList) }}
Also if I do {{ d(__SELF__.property('getList')) }} it shows me the value is "5".
I built a form with a selectbox (EntityType) with a big amount of choices (about 50 000) :
->add(
'destination', EntityType::class, array(
'label' => 'à',
'multiple' => false,
'required' => false,
'class' => Stop::class,
'attr' => array(
'class' => 'form-control',
)
)
);
I am facing a big performance issue : dozens of seconds before the list is displayed when I click on it.
I guess the solution would be to initially only load a few elements (e.g. a hundred), and then use Ajax to request the DB when the user starts typing (I am using a select2 box with search field).
Problem is that I cannot figure out the most efficient way to do it through Symfony.
I have seen that the choice_loader functionality could do it, but there is no detailed documentation available : https://symfony.com/blog/new-in-symfony-3-2-lazy-loading-of-form-choices
Would be great if somebody can help on this,
Thanks for your support,
When I face this kind of trouble, I use another approach.
If the select option will have more than 20 entries, so I change it to a Input Text with autocomplete.
The Steps are:
Install a good autocomplete Javascript lib like jquery-typeahead
I like to use Wepack Encore in Symfony. With Webpack, Npm and Yarn the installation is easy like
yarn add jquery-typeahead --dev
You would need to run yarn run encore dev after installation.
Create a new FieldType for your form to replace the EntityType
Lets suppose that we need to create a register form with city field. The default behaviour will use EntityType and show a Select Option with all cities.
To change it to autocomplete lets create another FieldType.
<?php
// src/Form/Type/AutocompleteCityType.php
namespace App\Form\Type;
use Doctrine\ORM\EntityManagerInterface;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\OptionsResolver\OptionsResolver;
class AutocompleteCityType extends AbstractType
{
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults(array(
'attr' => ['autocomplete' => 'off']
));
}
public function getParent()
{
return SearchType::class;
}
}
NOTE: On the code above, I am extending SearchType::class that is a Input Type Search (HTML 5).
Our new field type can be used on our forms but it is just another string field. Won't work correctly to replace EntityType. We need to convert this string to an Entity. So we need a DataTransformer
Create a City to String DataTransformer
<?php
// src/Form/DataTransformer/CityToStringTransformer.php
namespace App\Form\DataTransformer;
use App\Entity\City; // Pay attention to use your Entities correctly
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
class CityToStringTransformer implements DataTransformerInterface
{
private $entityManager;
public function __construct(EntityManagerInterface $entityManager)
{
$this->entityManager = $entityManager;
}
/**
* Transforms an object (City) to a string.
*
* #param City|null $city
* #return string
*/
public function transform($city)
{
if (null === $city) {
return '';
}
return $city->getSomethingUnique();
}
/**
* Transforms a string to an object (city).
*
* #param string $somethingUnique
* #return City|null
* #throws TransformationFailedException if object (city) is not found.
*/
public function reverseTransform($somethingUnique)
{
// empty City? It's optional, so that's ok
if (!$somethingUnique) {
return;
}
$city = $this->entityManager
->getRepository(City::class)
->findByThatSomethingUnique($somethingUnique);
if (null === $city) {
// causes a validation error
// this message is not shown to the user
// see the invalid_message option
throw new TransformationFailedException(sprintf(
'The city "%s" cannot be found!',
$somethingUnique
));
}
return $city;
}
}
Note: The string must be some kind of unique key to work correctly and need to be good to show on Autocomplete and fill the field (Like [CityCode] CityName). The DataTransformation cannot return more than one result on findByThatSomethingUnique() method.
Note 2 : For exemple, $city->getSomethingUnique() cant be $city->getId()."-".$city->getName() and ->findByThatSomethingUnique($somethingUnique) can be ->findOneById(explode("-", $somethingUnique)[0])
Almost done. We can use both classes on our FormType to replace EntityType.
Using on the FormType
// src/Form/MyFormType.php
// (...) Other declarations(...)
use App\Form\DataTransformer\ContactToStringTransformer;
use App\Form\Type\AutocompleteContactType;
class MyFormType extends AbstractType
{
private $cityTransformer;
public function __construct(CityToStringTransformer $cityTransformer)
{
$this->cityTransformer = $cityTransformer;
}
public function buildForm(FormBuilderInterface $builder, array $options)
{
/* #var $myEntity MyEntity */
$myEntity = $builder->getData();
$builder
->add('city', AutocompleteCityType::class, [
'label' => 'Custom City Label',
])
// (...) Other fields (...)
;
$builder->get('city')
->addModelTransformer($this->cityTransformer);
}
With the code until here, the form will be shown correctly, but the typeahead must be configured properly.
You can create a new twig block type for this new field type. The code below must reside in the your custom form_theme
The Twig block
{% block autocomplete_city_widget %}
{% spaceless %}
<div class="typeahead__container">
<div class="typeahead__field">
<div class="typeahead__query">
{{ form_widget(form) }}
</div>
</div>
</div>
{% endspaceless %}
{% endblock %}
NOTE: The Twig code above is related to jquery-typeahead and only works with a field called AutocompleteCityType. If you install another lib or change the name of FieldType class, change it properly. Also pay attention to form names that change the block name to be rendered.
The last thing is to write the javascript for typeahead get the entries.
The Typeahead Javascript
jQuery.typeahead({
input: "#myForm_city", // Related to FormName and Autocomplete Field Name
minLength: 1,
maxItem: 20,
order: "asc",
dynamic: true,
delay: 500,
backdrop: { "background-color": "#eeeeee" },
template: "<small style='color:#999;'>{{ '[{{citycode}}] {{cityname}}' }}</small>", // remember that this is a Twig template...
emptyTemplate: "No results for typed string",
source: {
city: {
display: ["citycode", "cityname"],
ajax: function (query) {
return {
type: "POST",
url: '{{ path('controller_with_city_list_json_response') }}',
path: "city",
data: {
"q": "{{ '{{query}}' }}",
"length" : "40",
},
callback: {
done: function (res) {
var d = {};
d.city = [];
jQuery(res.data).each(function(index, value) {
d.city.push(value.city);
});
return d;
}
}
}
}
}
},
callback: {
onClickAfter: function (node, a, item, event) {
event.preventDefault();
jQuery(node).val("[" + item.citycode + "] " + item.cityname);
}
},
debug: false
});
NOTE: The Typeahead code above want a json response in format
{"data":
[
{"city":
{"cityname":"City Name X", "citycode": "NXNX"}
},
{"city":
{"cityname":"City Name Y", "citycode": "NYNY"}
}
]
}
I use an ActiveForm in Yii2 for my SearchModel. After click on search button the form fields remember previous values but SorterDropdown is refreshed.
<?php echo SorterDropdown::widget(['sort' => $dataProvider->sort,
'label' => $model->sortedBy($dataProvider->sort->attributes),])
?>
SorterDropdown is just a wrapper of ButtonDropdown.
How can I forse the SorterDropdown to remember sort order (and show it) after the form submition?
class SorterDropdown extends LinkSorter
{
public $label;
protected function renderSortLinks()
{
$attributes = empty($this->attributes) ? array_keys($this->sort->attributes) : $this->attributes;
$links = [];
foreach ($attributes as $name) {
$links[] = Html::tag('li', $this->sort->link($name, ['tabindex' => '-1']));
}
if (empty($this->label))
$this->label = 'Sort';
return \yii\bootstrap\ButtonDropdown::widget([
'encodeLabel' => false,
'label' => $this->label,
'dropdown' => [
'items' => $links,
],
]);
}
You should add some class to li element, which indicate active state of it and special style in CSS file.
I build my form template according the documentation. It seemed everything was fine until I get fields errors. Now I have two problems:
How can I change the class name of the forms fields when they get error?
Solution:
$this->loadHelper('Form', [
'templates' => 'your_template_file',
'errorClass' => 'your-class',
]);
How can I set escape => false in the error-message from cakephp, when the field get error? Because I have icon within that div, such as
<div class="error-message"><i class="fa fa-times"></i> My error</div>
Well, I got part of th solution. To escape HTML I could put $this->Form->error('field', null, ['escape' => false]); in all fields, but it´s a hard manually task. I´d like to keep escape with default of all fields errors. I could edit the FormHelper.php class. However, I think that is not good idea.
My form template is:
'formStart' => '<form {{attrs}} class="form-horizontal" novalidate>',
'inputContainer' => '{{content}}',
'input' => '<input type="{{type}}" name="{{name}}" {{attrs}} class="form-control"/>',
'checkbox' => '<input type="checkbox" value="{{value}}" name="{{name}}" {{attrs}}/>',
'textareaContainerError' => '{{content}}',
'textarea' => '<textarea name="{{name}}" {{attrs}} class="form-control"></textarea>',
'select' => '<select name="{{name}}" {{attrs}} class="form-control">{{content}}</select>',
'button' => '<button {{attrs}} class="btn btn-primary">{{text}}</button>',
'nestingLabel' => '{{input}}',
'formGroup' => '{{input}}',
to the second part of the question: you can extend FormHelper like in code below, so that escape will be set to false by default
// extended FormHelper, this goes in src/View/Helper
namespace App\View\Helper;
use Cake\View\Helper;
class MyFormHelper extends Helper\FormHelper
{
public function error($field, $text = null, array $options = [])
{
if (!isset($options['escape'])) {
$options['escape'] = false;
}
return parent::error($field, $text, $options);
}
}
next create alias for this helper in AppController.php
public $helpers = [
'Form' => ['className' => 'MyForm']
];
this also allows you to add more customization of your own and at any time, you can go back to default implementation of FormHelper, just remove that alias from AppController.php.
For those who wants an 'easy solution' to escape error message on some fields, you cant simply set escape options to false :
<?= $this->Form->input('email', [
"label" => "Email",
"error" => [
"escape" => false
]
]) ?>
I'm new to Laravel, and I'm being dumb on this for sure, cause i've read the documentation and i've searched all over google but i'm not getting how to go over this. I have a M:M relationship between galleries and artist. Inside each gallery edit page I have a form to update the name and url for the gallery that is working fine. In the same page I have 2 other select forms, one for adding artists to the gallery and another to remove artists, that need to update a pivot table called "galleries_artists". I created 2 custom methods for both these forms called "postAdd" and "postRemove" but I can't put them to work regardless of what I try.
Below is the code i have so far. Hope somebody can help me understand the dumb mistakes i'm making.
Model - Artist.php
class Artist extends Eloquent {
protected $fillable = array('name');
public static $rules = array(
'name'=>'required|min:2'
);
public function galeries() {
return $this->belongsToMany('Gallery', 'galeries_artists', 'artist_id', 'gallery_id', 'stand_id');
}
}
Model - Gallery.php
class Gallery extends Eloquent {
protected $fillable = array('name', 'stand_id', 'url');
public static $rules = array(
'stand_id'=>'required|integer'
);
public function stand() {
return $this->belongsTo('Stand');
}
public function artist() {
return $this->belongsToMany('Artist', 'galleries_artists', 'gallery_id', 'artist_id', 'stand_id');
}
}
Controller - GalleriesController.php
public function postAdd($id, $aid) {
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$add_artist = Input::get();
$add_artist->galleries()->attach(Input::get('add_artist'));
$add_artist->save();
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist added successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
public function postRemove($id, $aid) {
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$remove_artist = Input::get();
$remove_artist->galleries()->detach(Input::get('remove_artist'));
$remove_artist->save();
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist removed successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
edit.blade.php
Add Form
{{ Form::open(array('class' => '', 'method' => 'put', 'action'=> array('GalleriesController#postAdd', $gallery->id , $add_artist->id ))) }}
<div class="form-group">
{{ Form::label('Add Artist:') }}
{{ Form::select('add_artist', $other_artists_name, null, array('class'=>'form-control')) }}
</div>
{{ Form::button('Add Artist', array('type' => 'submit', 'class'=>'btn btn-primary')) }}
{{ Form::close() }}
edit.blade.php
Remove Form
{{ Form::open(array('class' => '', 'method' => 'put', 'action'=>array('GalleriesController#postRemove', $id , 'aid'))) }}
<div class="form-group">
{{ Form::label('Remove Artist:') }}
{{ Form::select('remove_artist', $gallery_artists_name, null, array('class'=>'form-control')) }}
</div>
{{ Form::button('Remove Artist', array('type' => 'submit', 'class'=>'btn btn-danger')) }}
{{ Form::close() }}
Routes.php
Route::post('admin/galleries/{galleries}/add/{aid}', 'GalleriesController#postAdd');
Route::post('admin/galleries/{galleries}/remove/{aid}', 'GalleriesController#postRemove');
Route::resource('admin/galleries', 'GalleriesController');
I've been doing so many changes to the code that a lot of things might be mixed up. Sorry if that's the case.
You are making it pretty difficult. Here is what I did, but with checkboxes, which allowed me to cut down on the number of functions and forms I needed to work with. I've skipped the validation, but what I do have was tested and seems to work fine.
Swapping out the checkboxes for a select shouldn't be too much additional work, but I'd suggest going with a multi-select in that case, because again, it would be much simpler to work with for you and much easier to use from the user's standpoint. Let me know if I should modify my answer if it has to be selects.
Controller
class ArtController extends BaseController {
public function getIndex($artist_id)
{
// Get our artist with associated galleries
$artist = Artist::find($artist_id);
$artist->load('galleries');
// Get all galleries to populate our checkboxes
$galleries = Gallery::all();
// Show form
return View::make('art.gallery_update_form')->with('artist', $artist)->with('galleries', $galleries);
}
public function postIndex($artist_id)
{
// Grab our artist
$artist = Artist::find($artist_id);
// Sync the galleries. If no galleries were chosen, send it an empty array. Sync will perform both write and delete operations for you in one shot. Very handy.
$artist->galleries()->sync(Input::get('galleries', array()));
// Reshow the form
return $this->getIndex($artist_id);
}
}
View
#section('content')
{{ Form::open() }}
<!-- Get a list of our ID's so we can check/uncheck the checkboxes as we go -->
<?php $artist_galleries = $artist->galleries->lists('id'); ?>
<!-- Create checkbox for each gallery -->
#foreach($galleries as $gallery)
{{ Form::label($gallery->id, $gallery->name) }}
<!-- 3rd parameter is where the magic happens, it's looking in our list of galleries
we created a few lines up for the id to see if the artist belongs to that gallery or not -->
{{ Form::checkbox('galleries[]', $gallery->id, in_array($gallery->id, $artist_galleries), array('id' => $gallery->id)) }}
<br />
#endforeach
{{ Form::submit() }}
{{ Form::close() }}
#stop
Routes
Route::get('art/{artist_id}', array('uses' => 'ArtController#getIndex'));
Route::post('art/{artist_id}', array('uses' => 'ArtController#postIndex'));
Edit
Just getting your postAdd() method to work, all that should be required is something like this...
public function postAdd($id, $aid)
{
$input = array_except(Input::all(), '_method');
$v = Validator::make(Input::all(), Artist::$rules);
if ($v->passes()) {
$gallery = Gallery::find($id);
$gallery->artists()->attach($aid);
return Redirect::route('admin.galleries.edit')
->with('message', 'Artist added successfully.');
}
return Redirect::route('admin.galleries.edit')
->with('message', 'Something went wrong')
->withErrors($v)
->withInput();
}
I may be a little confused on the purpose of this function. As I wrote it, it will attach a selected artist to a gallery. I'm not sure of the purpose of using input. It looks as though you may also be attempting to save a new artist as though you wish your users to be able to create a new artist and assign that artist to a gallery when the artist is created? If you are passing in the artist id and the gallery id, that should be all you need and there is no need for the Input class.
Usually though, you'd be only passing in the gallery id which you would have generated a link for and it would be in the URI and the artist would be passed in via the form, in which case you would need to use Input::get('add_artist') and that would keep your URI much cleaner as well.
The postRemove() function would be the exact same, except you'd want to use detach() instead. This is all assuming of course all the rest of the functionality which is responsible for passing in the gallery id and artist id are working as well as the relationships themselves you've already setup in your models.