J3.x

Difference between revisions of "Developing an MVC Component/Adding Versioning"

From Joomla! Documentation

< J3.x:Developing an MVC Component
m (root record needs to have language set to * (All))
(Several markup changes. Words2Watch corrections.)
Line 6: Line 6:
 
<translate><!--T:2--> This tutorial is part of the [[S:MyLanguage/J3.2:Developing an MVC Component | Developing an MVC Component for Joomla! 3.2]] tutorial. You are encouraged to read the previous parts of the tutorial before reading this.</translate>
 
<translate><!--T:2--> This tutorial is part of the [[S:MyLanguage/J3.2:Developing an MVC Component | Developing an MVC Component for Joomla! 3.2]] tutorial. You are encouraged to read the previous parts of the tutorial before reading this.</translate>
  
<translate><!--T:3--> In this step we enable our helloworld component to maintain versions of records. As versioning is often associated with documents, we add a text 'description' field to our helloworld record.</translate>  
+
<translate><!--T:3--> In this step we enable our helloworld component to maintain versions of records. As versioning is often associated with documents, we add a text 'description' field to our helloworld record.</translate>
  
<translate><!--T:4--> There are 2 videos accompanying this tutorial step, covering the [https://youtu.be/0TWiH9PVciQ Versioning Overview] and the [https://youtu.be/4W3K5ozsg9w Joomla Observer pattern].</translate>
+
<translate><!--T:4--> There are two videos accompanying this tutorial step, covering the [https://youtu.be/0TWiH9PVciQ Versioning Overview] and the [https://youtu.be/4W3K5ozsg9w Joomla Observer pattern].</translate>
  
 
* <translate><!--T:5--> Versioning Overview</translate>
 
* <translate><!--T:5--> Versioning Overview</translate>
Line 18: Line 18:
 
== Introduction == <!--T:7-->
 
== Introduction == <!--T:7-->
 
</translate>
 
</translate>
<translate><!--T:8--> Joomla implements versioning in its core components by using an observer pattern (aka publish / subscribe pattern). The Joomla content history code which handles the storing of record versions subscribes to the <tt>onAfterStore</tt> events which are triggered whenever the JTable code performs an insert or update operation on a record. When the trigger fires then this content history versioning code runs, and it writes a copy of the data to its own database table (ucm_history). This copy includes a version number, plus the data of the record as a blob of name-value pairs (amongst other information).</translate>  
+
<translate><!--T:8--> Joomla implements versioning in its core components by using an observer pattern (aka publish / subscribe pattern). The Joomla content history code that handles the storing of record versions subscribes to the ''onAfterStore'' events that are triggered whenever the JTable code performs an insert or update operation on a record. When the trigger fires then this content history versioning code runs, and it writes a copy of the data to its own database table (''ucm_history''). This copy includes a version number, plus the data of the record as a blob of name-value pairs (amongst other information).</translate>
  
<translate><!--T:9--> To manage and restore versions there is a Versions button on the record edit form. The Joomla code behind this button runs the com_contenthistory component, which is responsible for displaying the versions stored (by reading the ucm_history records), and enabling the administrator to manage those versions, including initiating a restore of a previous version to be the current record.</translate>
+
<translate><!--T:9--> To manage and restore versions there is a Versions button on the record edit form. The Joomla code behind this button runs the ''com_contenthistory'' component, which is responsible for displaying the versions stored (by reading the ''ucm_history'' records), and enabling the Administrator to manage those versions, including initiating a restore of a previous version to be the current record.</translate>
  
<translate><!--T:10--> In order to display the record data appropriately, the com_contenthistory component uses configuration data which it looks to find in the content_types table.</translate>  
+
<translate><!--T:10--> In order to display the record data appropriately, the ''com_contenthistory'' component uses configuration data that it looks to find in the ''content_types'' table.</translate>
  
<translate><!--T:11--> Note that this mechanism doesn't support keeping old versions to enable a record to be restored if it is deleted accidentally. If you want this in your component, then you should follow the example of Joomla: whenever an admin "deletes" a record it just sets the status of the record to "Trashed", and the record is only really deleted when the admin "empties the trash". When the record is physically deleted in the database, the event <tt>onBeforeDelete</tt> is triggered, and on receiving this the Joomla content history versioning code deletes versions of the record in the ucm history table as well.</translate>  
+
<translate><!--T:11--> Note that this mechanism doesn't support keeping old versions to enable a record to be restored if it is deleted accidentally. If you want this in your component, then you should follow the example of Joomla: whenever an admin "deletes" a record it just sets the status of the record to "Trashed", and the record is only really deleted when the admin "empties the trash". When the record is physically deleted in the database, the event ''onBeforeDelete'' is triggered, and on receiving this the Joomla content history versioning code deletes versions of the record in the ''ucm_history'' table as well.</translate>
  
 
<translate>
 
<translate>
Line 31: Line 31:
 
<translate><!--T:13--> The Joomla documentation page at [[S:MyLanguage/Using Content History in your Component]] gives a list of the aspects required to build version history capability into a component. These aspects are listed below.</translate>
 
<translate><!--T:13--> The Joomla documentation page at [[S:MyLanguage/Using Content History in your Component]] gives a list of the aspects required to build version history capability into a component. These aspects are listed below.</translate>
  
# <translate><!--T:14--> We need to get the Joomla <tt>JTableObserverContenthistory</tt> code – this is the code which is responsible for writing the versioned copies to the ucm_history table – to handle record updates which occur with our component Helloworld table. We do this by calling a function <tt>JObserverMapper::addObserverClassToClass</tt> and passing as parameters the observer class <tt>JTableObserverContenthistory</tt> and the class to observe – our <tt>HelloWorldTableHelloWorld</tt> class. Our class inherits from JTable, and JTable publishes the <tt>onAfterStore</tt> and <tt>onBeforeDelete</tt> events which this observer will use to maintain our record versions in the ucm_history table.</translate>  
+
# <translate><!--T:14--> We need to get the Joomla ''JTableObserverContenthistory'' code – this is the code that is responsible for writing the versioned copies to the ''ucm_history'' table – to handle record updates that occur with our component Helloworld table. We do this by calling a function ''JObserverMapper::addObserverClassToClass'' and passing as parameters the observer class ''JTableObserverContenthistory'' and the class to observe – our ''HelloWorldTableHelloWorld'' class. Our class inherits from JTable, and JTable publishes the ''onAfterStore'' and ''onBeforeDelete'' events that this observer will use to maintain our record versions in the ''ucm_history'' table.</translate>
# <translate><!--T:15--> We add a Versions button on our edit form. This has standard Joomla code behind it, which results in a modal with an iframe being added to our edit page. When the Versions button is clicked the modal is displayed, and the iframe within it is populated from the com_contenthistory component code, based on the record's versions available in the ucm_history table.</translate>  
+
# <translate><!--T:15--> We add a Versions button on our edit form. This has standard Joomla code behind it, which results in a modal with an iframe being added to our edit page. When the ''Versions button'' is clicked the modal is displayed, and the iframe within it is populated from the ''com_contenthistory'' component code, based on the record's versions available in the ''ucm_history'' table.</translate>
# <translate><!--T:16--> We provide configuration data (which we insert into the content_types table keyed by a type_alias value of <tt>com_helloworld.helloworld</tt>) to the content history code for 2 purposes:</translate>  
+
# <translate><!--T:16--> We provide configuration data (which we insert into the ''content_types'' table keyed by a ''type_alias'' value of ''com_helloworld.helloworld'') to the content history code for two purposes:</translate>
#* <translate><!--T:17--> to specify under what circumstances a new version should be stored – for example, if a user just clicks on a record to edit it, but then cancels, we wouldn't want a new version to be created. But as this process involves updating the database record via the checkout process, we have to tell the code to ignore the case where just the checkout fields are changed.</translate>  
+
## <translate><!--T:17--> to specify under what circumstances a new version should be stored – for example, if a user just clicks on a record to edit it, but then cancels, we wouldn't want a new version to be created. But as this process involves updating the database record via the checkout process, we have to tell the code to ignore the case where just the checkout fields are changed.</translate>
#* <translate><!--T:18--> to enable the com_contenthistory code to display the data in the record with sensible labels.</translate>  
+
## <translate><!--T:18--> to enable the ''com_contenthistory'' code to display the data in the record with sensible labels.</translate>
# <translate><!--T:19--> We add a helloworld configuration parameter which allows the administrator to switch on / off versioning for com_helloworld. The content history versioning code (in <tt>JTableObserverContenthistory</tt>) looks for a com_helloworld <tt>save_history</tt> parameter to determine if it should create the record versions for the helloworld component.</translate>  
+
# <translate><!--T:19--> We add a helloworld configuration parameter that allows the Administrator to switch on/off versioning for ''com_helloworld''. The content history versioning code (in ''JTableObserverContenthistory'') looks for a ''com_helloworld'' ''save_history'' parameter to determine if it should create the record versions for the helloworld component.</translate>
# <translate><!--T:20--> We add a <tt>typeAlias</tt> variable to our model. This is just used as a check by Joomla when restoring a version of a helloworld record, ensuring that the typeAlias obtained via the history record matches the value in this <tt>typeAlias</tt> variable.</translate>
+
# <translate><!--T:20--> We add a ''typeAlias'' variable to our model. This is just used as a check by Joomla when restoring a version of a helloworld record, ensuring that the typeAlias obtained via the history record matches the value in this ''typeAlias'' variable.</translate>
# <translate><!--T:21--> We add a version note field to our edit form. A value in this field won't be stored in our helloworld record, but will be stored in the version copy in the ucm_history table, as a label associated with that version.</translate>  
+
# <translate><!--T:21--> We add a version note field to our edit form. A value in this field won't be stored in our helloworld record, but will be stored in the version copy in the ''ucm_history'' table, as a label associated with that version.</translate>
# <translate><!--T:22--> We add an editor field to the component. This uses an <tt>editor</tt> form field type, one of Joomla's [[standard_form_field_types|standard form field types]] within the admin form, and we output it on the front end as well.</translate>  
+
# <translate><!--T:22--> We add an editor field to the component. This uses an ''editor'' form field type, one of Joomla's [[standard_form_field_types|standard form field types]] within the admin form, and we output it on the Frontend as well.</translate>
  
<translate><!--T:23--> In addition, we enable version support for helloworld categories. In general the code associated with helloworld categories checks our save_history parameter to determine whether to store versions of categories, and whether to show the Versions button on the edit category form. However, we must also supply the configuration data for categories in the content_types table.</translate>  
+
<translate><!--T:23--> In addition, we enable version support for helloworld categories. In general the code associated with helloworld categories checks our save_history parameter to determine whether to store versions of categories, and whether to show the Versions button on the edit category form. However, we must also supply the configuration data for categories in the content_types table.</translate>
 
<translate>
 
<translate>
 
== Database and Install == <!--T:24-->
 
== Database and Install == <!--T:24-->
 
</translate>
 
</translate>
<translate><!--T:25--> We add the description field to the helloworld record, and our configuration data as records in the <tt>content_types</tt> table. A good explanation of this configuration data is at [[Using Content History in your Component#Add Labels to Pop-Up Windows]].</translate>
+
<translate><!--T:25--> We add the description field to the helloworld record, and our configuration data as records in the ''content_types'' table. A good explanation of this configuration data is at [[Using Content History in your Component#Add Labels to Pop-Up Windows]].</translate>
  
 
<span id="admin/sql/updates/mysql/0.0.27.sql">
 
<span id="admin/sql/updates/mysql/0.0.27.sql">
<tt>admin/sql/updates/mysql/0.0.27.sql</tt>
+
''admin/sql/updates/mysql/0.0.27.sql''
<source lang="sql">
+
<syntaxhighlight lang="sql">
 
ALTER TABLE `#__helloworld` ADD COLUMN `description` VARCHAR(4000) NOT NULL DEFAULT '' AFTER `greeting`;
 
ALTER TABLE `#__helloworld` ADD COLUMN `description` VARCHAR(4000) NOT NULL DEFAULT '' AFTER `greeting`;
  
INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)  
+
INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)
 
VALUES
 
VALUES
('Helloworld', 'com_helloworld.helloworld',  
+
('Helloworld', 'com_helloworld.helloworld',
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",  
+
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],  
+
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],
 
"ignoreChanges":["checked_out", "checked_out_time", "path"],
 
"ignoreChanges":["checked_out", "checked_out_time", "path"],
"convertToInt":[],  
+
"convertToInt":[],
 
"displayLookup":[
 
"displayLookup":[
 
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
Line 64: Line 64:
 
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
 
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
 
('Helloworld Category', 'com_helloworld.category',
 
('Helloworld Category', 'com_helloworld.category',
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",  
+
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],  
+
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],
 
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
 
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
"convertToInt":["publish_up", "publish_down"],  
+
"convertToInt":["publish_up", "publish_down"],
 
"displayLookup":[
 
"displayLookup":[
 
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
Line 73: Line 73:
 
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');
 
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
 
<span id="admin/sql/install.mysql.utf8.sql">
 
<span id="admin/sql/install.mysql.utf8.sql">
<tt>admin/sql/install.mysql.utf8.sql</tt>
+
''admin/sql/install.mysql.utf8.sql''
<source lang="sql" highlight="11,38-58">
+
<syntaxhighlight lang="sql" highlight="11,38-58">
 
DROP TABLE IF EXISTS `#__helloworld`;
 
DROP TABLE IF EXISTS `#__helloworld`;
  
Line 116: Line 116:
 
('Goodbye World!','goodbye-world','en-GB', 1, 1, 'goodbye-world', 3, 4, 0);
 
('Goodbye World!','goodbye-world','en-GB', 1, 1, 'goodbye-world', 3, 4, 0);
  
INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)  
+
INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)
 
VALUES
 
VALUES
('Helloworld', 'com_helloworld.helloworld',  
+
('Helloworld', 'com_helloworld.helloworld',
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",  
+
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],  
+
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],
 
"ignoreChanges":["checked_out", "checked_out_time", "path"],
 
"ignoreChanges":["checked_out", "checked_out_time", "path"],
"convertToInt":[],  
+
"convertToInt":[],
 
"displayLookup":[
 
"displayLookup":[
 
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
Line 128: Line 128:
 
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
 
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
 
('Helloworld Category', 'com_helloworld.category',
 
('Helloworld Category', 'com_helloworld.category',
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",  
+
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],  
+
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],
 
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
 
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
"convertToInt":["publish_up", "publish_down"],  
+
"convertToInt":["publish_up", "publish_down"],
 
"displayLookup":[
 
"displayLookup":[
 
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
Line 137: Line 137:
 
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
 
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');
 
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
<translate><!--T:26--> If the helloworld component is uninstalled then we should remove the configuration records from the content_types table and remove helloworld record versions from the ucm_history table.</translate>
+
<translate><!--T:26--> If the helloworld component is uninstalled then we should remove the configuration records from the content_types table and remove helloworld record versions from the ''ucm_history'' table.</translate>
  
 
<span id="admin/sql/uninstall.mysql.utf8.sql">
 
<span id="admin/sql/uninstall.mysql.utf8.sql">
<tt>admin/sql/uninstall.mysql.utf8.sql</tt>
+
''admin/sql/uninstall.mysql.utf8.sql''
<source lang="sql" highlight="2-4">
+
<syntaxhighlight lang="sql" highlight="2-4">
 
DROP TABLE IF EXISTS `#__helloworld`;
 
DROP TABLE IF EXISTS `#__helloworld`;
DELETE FROM `#__ucm_history` WHERE ucm_type_id in  
+
DELETE FROM `#__ucm_history` WHERE ucm_type_id in
 
(select type_id from `#__content_types` where type_alias in ('com_helloworld.helloworld','com_helloworld.category'));
 
(select type_id from `#__content_types` where type_alias in ('com_helloworld.helloworld','com_helloworld.category'));
 
DELETE FROM `#__content_types` WHERE type_alias in ('com_helloworld.helloworld','com_helloworld.category');
 
DELETE FROM `#__content_types` WHERE type_alias in ('com_helloworld.helloworld','com_helloworld.category');
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
<translate><!--T:27--> In the previous step we used the install script to modify the helloworld records in the database. We don't want that to recur this step, so we revert the install script to its basic form.</translate>  
+
<translate><!--T:27--> In the previous step we used the install script to modify the helloworld records in the database. We don't want that to recur in this step, so we revert the install script to its basic form.</translate>
  
 
<span id="script.php">
 
<span id="script.php">
<tt>script.php</tt>
+
''script.php''
<source lang="php" highlight="92-93">
+
<syntaxhighlight lang="php" highlight="92-93">
 
<?php
 
<?php
 
// No direct access to this file
 
// No direct access to this file
Line 190: Line 190:
 
     * @return void
 
     * @return void
 
     */
 
     */
     public function install($parent)  
+
     public function install($parent)
 
     {
 
     {
 
         $parent->getParent()->setRedirectURL('index.php?option=com_helloworld');
 
         $parent->getParent()->setRedirectURL('index.php?option=com_helloworld');
Line 202: Line 202:
 
     * @return void
 
     * @return void
 
     */
 
     */
     public function uninstall($parent)  
+
     public function uninstall($parent)
 
     {
 
     {
 
         echo '<p>' . JText::_('COM_HELLOWORLD_UNINSTALL_TEXT') . '</p>';
 
         echo '<p>' . JText::_('COM_HELLOWORLD_UNINSTALL_TEXT') . '</p>';
Line 214: Line 214:
 
     * @return void
 
     * @return void
 
     */
 
     */
     public function update($parent)  
+
     public function update($parent)
 
     {
 
     {
 
         echo '<p>' . JText::sprintf('COM_HELLOWORLD_UPDATE_TEXT', $parent->get('manifest')->version) . '</p>';
 
         echo '<p>' . JText::sprintf('COM_HELLOWORLD_UPDATE_TEXT', $parent->get('manifest')->version) . '</p>';
Line 231: Line 231:
 
     * @return void
 
     * @return void
 
     */
 
     */
     public function preflight($type, $parent)  
+
     public function preflight($type, $parent)
 
     {
 
     {
 
         echo '<p>' . JText::_('COM_HELLOWORLD_PREFLIGHT_' . $type . '_TEXT') . '</p>';
 
         echo '<p>' . JText::_('COM_HELLOWORLD_PREFLIGHT_' . $type . '_TEXT') . '</p>';
Line 247: Line 247:
 
     * @return void
 
     * @return void
 
     */
 
     */
     function postflight($type, $parent)  
+
     function postflight($type, $parent)
 
     {
 
     {
 
     }
 
     }
 
}
 
}
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
Line 260: Line 260:
  
 
<span id="admin/tables/helloworld.php">
 
<span id="admin/tables/helloworld.php">
<tt>admin/tables/helloworld.php</tt>
+
''admin/tables/helloworld.php''
<source lang="php" highlight="26">
+
<syntaxhighlight lang="php" highlight="26">
 
<?php
 
<?php
 
/**
 
/**
Line 307: Line 307:
 
$array['params'] = (string)$parameter;
 
$array['params'] = (string)$parameter;
 
}
 
}
       
+
 
 
         if (isset($array['imageinfo']) && is_array($array['imageinfo']))
 
         if (isset($array['imageinfo']) && is_array($array['imageinfo']))
 
{
 
{
Line 315: Line 315:
 
$array['image'] = (string)$parameter;
 
$array['image'] = (string)$parameter;
 
}
 
}
       
+
 
 
         // Bind the rules.
 
         // Bind the rules.
 
if (isset($array['rules']) && is_array($array['rules']))
 
if (isset($array['rules']) && is_array($array['rules']))
Line 322: Line 322:
 
$this->setRules($rules);
 
$this->setRules($rules);
 
}
 
}
       
+
 
 
if (isset($array['parent_id']))
 
if (isset($array['parent_id']))
 
{
 
{
Line 331: Line 331:
 
elseif (isset($array['helloworldordering']))
 
elseif (isset($array['helloworldordering']))
 
{
 
{
// when saving a record load() is called before bind() so the table instance will have properties which are the existing field values
+
// when saving a record load() is called before bind() so the table instance will have properties that are the existing field values
 
if ($this->parent_id == $array['parent_id'])
 
if ($this->parent_id == $array['parent_id'])
 
{
 
{
Line 365: Line 365:
 
return parent::bind($array, $ignore);
 
return parent::bind($array, $ignore);
 
}
 
}
   
+
 
 
     /**
 
     /**
 
* Method to compute the default name of the asset.
 
* Method to compute the default name of the asset.
Line 437: Line 437:
 
}
 
}
 
}
 
}
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
 
<translate>
 
<translate>
 
== Add Versions button to the Edit View == <!--T:30-->
 
== Add Versions button to the Edit View == <!--T:30-->
</translate>  
+
</translate>
  
<translate><!--T:31--> We check that our <tt>save_history</tt> config parameter is set to true before we display the Versions button.</translate>
+
<translate><!--T:31--> We check that our ''save_history'' config parameter is set to true before we display the Versions button.</translate>
  
 
<span id="admin/views/helloworld/view.html.php">
 
<span id="admin/views/helloworld/view.html.php">
<tt>admin/views/helloworld/view.html.php</tt>
+
''admin/views/helloworld/view.html.php''
<source lang="php" highlight="109-114">
+
<syntaxhighlight lang="php" highlight="109-114">
 
<?php
 
<?php
 
/**
 
/**
Line 491: Line 491:
 
         // What Access Permissions does this user have? What can (s)he do?
 
         // What Access Permissions does this user have? What can (s)he do?
 
$this->canDo = JHelperContent::getActions('com_helloworld', 'helloworld', $this->item->id);
 
$this->canDo = JHelperContent::getActions('com_helloworld', 'helloworld', $this->item->id);
       
+
 
 
// Check for errors.
 
// Check for errors.
 
if (count($errors = $this->get('Errors')))
 
if (count($errors = $this->get('Errors')))
Line 525: Line 525:
  
 
$isNew = ($this->item->id == 0);
 
$isNew = ($this->item->id == 0);
+
 
 
JToolBarHelper::title($isNew ? JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW')
 
JToolBarHelper::title($isNew ? JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW')
 
                            : JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT'), 'helloworld');
 
                            : JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT'), 'helloworld');
Line 532: Line 532:
 
{
 
{
 
// For new records, check the create permission.
 
// For new records, check the create permission.
if ($this->canDo->get('core.create'))  
+
if ($this->canDo->get('core.create'))
 
{
 
{
 
JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
 
JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
Line 548: Line 548:
 
JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
 
JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
 
JToolBarHelper::save('helloworld.save', 'JTOOLBAR_SAVE');
 
JToolBarHelper::save('helloworld.save', 'JTOOLBAR_SAVE');
+
 
 
// We can save this record, but check the create permission to see
 
// We can save this record, but check the create permission to see
 
// if we can return to make a new one.
 
// if we can return to make a new one.
if ($this->canDo->get('core.create'))  
+
if ($this->canDo->get('core.create'))
 
{
 
{
 
JToolBarHelper::custom('helloworld.save2new', 'save-new.png', 'save-new_f2.png',
 
JToolBarHelper::custom('helloworld.save2new', 'save-new.png', 'save-new_f2.png',
Line 558: Line 558:
 
$config = JFactory::getConfig();
 
$config = JFactory::getConfig();
 
$save_history = $config->get('save_history', true);
 
$save_history = $config->get('save_history', true);
if ($save_history)  
+
if ($save_history)
 
{
 
{
 
JToolbarHelper::versions('com_helloworld.helloworld', $this->item->id);
 
JToolbarHelper::versions('com_helloworld.helloworld', $this->item->id);
 
}
 
}
 
}
 
}
if ($this->canDo->get('core.create'))  
+
if ($this->canDo->get('core.create'))
 
{
 
{
 
JToolBarHelper::custom('helloworld.save2copy', 'save-copy.png', 'save-copy_f2.png',
 
JToolBarHelper::custom('helloworld.save2copy', 'save-copy.png', 'save-copy_f2.png',
Line 576: Line 576:
 
* @return void
 
* @return void
 
*/
 
*/
protected function setDocument()  
+
protected function setDocument()
 
{
 
{
 
$isNew = ($this->item->id < 1);
 
$isNew = ($this->item->id < 1);
Line 588: Line 588:
 
}
 
}
 
}
 
}
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
Line 595: Line 595:
 
</translate>
 
</translate>
  
<translate><!--T:33--> We add the <tt>save_history</tt> parameter together with a history_limit parameter which specifies the number of versions to keep.</translate>
+
<translate><!--T:33--> We add the ''save_history'' parameter together with a history_limit parameter that specifies the number of versions to keep.</translate>
  
 
<span id="admin/config.xml">
 
<span id="admin/config.xml">
<tt>admin/config.xml</tt>
+
''admin/config.xml''
<source lang="xml" highlight="38-59">
+
<syntaxhighlight lang="xml" highlight="38-59">
 
<?xml version="1.0" encoding="utf-8"?>
 
<?xml version="1.0" encoding="utf-8"?>
 
<config>
 
<config>
Line 677: Line 677:
 
</fieldset>
 
</fieldset>
 
</config>
 
</config>
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
Line 685: Line 685:
  
 
<span id="admin/models/helloworld.php">
 
<span id="admin/models/helloworld.php">
<tt>admin/models/helloworld.php</tt>
+
''admin/models/helloworld.php''
<source lang="php" highlight="25-26">
+
<syntaxhighlight lang="php" highlight="25-26">
 
<?php
 
<?php
 
/**
 
/**
Line 708: Line 708:
 
class HelloWorldModelHelloWorld extends JModelAdmin
 
class HelloWorldModelHelloWorld extends JModelAdmin
 
{
 
{
     // JModelAdmin needs to know this for storing the associations  
+
     // JModelAdmin needs to know this for storing the associations
 
protected $associationsContext = 'com_helloworld.item';
 
protected $associationsContext = 'com_helloworld.item';
   
+
 
 
// Contenthistory needs to know this for restoring previous versions
 
// Contenthistory needs to know this for restoring previous versions
 
public $typeAlias = 'com_helloworld.helloworld';
 
public $typeAlias = 'com_helloworld.helloworld';
+
 
 
/**
 
/**
 
* Method to override getItem to allow us to convert the JSON-encoded image information
 
* Method to override getItem to allow us to convert the JSON-encoded image information
Line 727: Line 727:
 
$item->imageinfo = $registry->toArray();
 
$item->imageinfo = $registry->toArray();
 
}
 
}
       
+
 
 
         // Load associated items
 
         // Load associated items
 
if (JLanguageAssociations::isEnabled())
 
if (JLanguageAssociations::isEnabled())
Line 743: Line 743:
 
}
 
}
 
}
 
}
return $item;  
+
return $item;
 
}
 
}
+
 
 
/**
 
/**
 
* Method to get a table object, load it if necessary.
 
* Method to get a table object, load it if necessary.
Line 791: Line 791:
 
return $form;
 
return $form;
 
}
 
}
   
+
 
 
     /**
 
     /**
 
* Method to preprocess the form to add the association fields dynamically
 
* Method to preprocess the form to add the association fields dynamically
Line 833: Line 833:
 
* @return string Script files
 
* @return string Script files
 
*/
 
*/
public function getScript()  
+
public function getScript()
 
{
 
{
 
return 'administrator/components/com_helloworld/models/forms/helloworld.js';
 
return 'administrator/components/com_helloworld/models/forms/helloworld.js';
 
}
 
}
+
 
 
/**
 
/**
 
* Method to get the data that should be injected in the form.
 
* Method to get the data that should be injected in the form.
Line 926: Line 926:
 
{
 
{
 
}
 
}
+
 
 
/**
 
/**
 
* Save the record reordering after a record is dragged to a new position in the helloworlds view
 
* Save the record reordering after a record is dragged to a new position in the helloworlds view
Line 945: Line 945:
 
}
 
}
 
}
 
}
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
 
<translate>
 
<translate>
Line 951: Line 951:
 
</translate>
 
</translate>
  
<translate><!--T:36--> We need to add these to our edit form xml definition and Joomla expects the version note field to be called "version_note". We put the version_note field within the "details" fieldset so that it will be output in the layout with the other fields in the <tt>renderFieldset('details')</tt> call. The description field we'll place separately on the right hand side of the page on the admin form, and we'll add it on the front end as well.</translate>  
+
<translate><!--T:36--> We need to add these to our edit form xml definition and Joomla expects the version note field to be called "version_note". We put the version_note field within the "details" fieldset so that it will be output in the layout with the other fields in the ''renderFieldset('details')'' call. The description field we'll place separately on the right side of the page on the admin form, and we'll add it on the Frontend as well.</translate>
  
 
<span id="admin/models/forms/helloworld.xml">
 
<span id="admin/models/forms/helloworld.xml">
<tt>admin/models/forms/helloworld.xml</tt>
+
''admin/models/forms/helloworld.xml''
<source lang="xml" highlight="90-97,99-105">
+
<syntaxhighlight lang="xml" highlight="90-97,99-105">
 
<?xml version="1.0" encoding="utf-8"?>
 
<?xml version="1.0" encoding="utf-8"?>
 
<form
 
<form
Line 979: Line 979:
 
default=""
 
default=""
 
/>
 
/>
<field  
+
<field
name="alias"  
+
name="alias"
type="text"  
+
type="text"
 
label="JFIELD_ALIAS_LABEL"
 
label="JFIELD_ALIAS_LABEL"
 
description="JFIELD_ALIAS_DESC"
 
description="JFIELD_ALIAS_DESC"
 
hint="JFIELD_ALIAS_PLACEHOLDER"
 
hint="JFIELD_ALIAS_PLACEHOLDER"
size="40"  
+
size="40"
 
/>
 
/>
 
         <field
 
         <field
Line 1,021: Line 1,021:
 
default="0.0"
 
default="0.0"
 
/>
 
/>
<field
+
<field
                 name="language"  
+
                 name="language"
         type="contentlanguage"  
+
         type="contentlanguage"
 
                 label="JFIELD_LANGUAGE_LABEL"
 
                 label="JFIELD_LANGUAGE_LABEL"
 
description="COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC"
 
description="COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC"
Line 1,049: Line 1,049:
 
label="JGLOBAL_FIELD_VERSION_NOTE_LABEL"
 
label="JGLOBAL_FIELD_VERSION_NOTE_LABEL"
 
description="JGLOBAL_FIELD_VERSION_NOTE_DESC"
 
description="JGLOBAL_FIELD_VERSION_NOTE_DESC"
class="inputbox"  
+
class="inputbox"
 
size="45"
 
size="45"
 
labelclass="control-label">
 
labelclass="control-label">
 
</field>
 
</field>
 
     </fieldset>
 
     </fieldset>
<field name="description"  
+
<field name="description"
 
type="editor"
 
type="editor"
label="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_LABEL"  
+
label="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_LABEL"
 
description="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_DESC"
 
description="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_DESC"
filter="JComponentHelper::filterText"  
+
filter="JComponentHelper::filterText"
buttons="true"  
+
buttons="true"
 
/>
 
/>
 
<fields name="imageinfo">
 
<fields name="imageinfo">
Line 1,123: Line 1,123:
 
     </fieldset>
 
     </fieldset>
 
</form>
 
</form>
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
 
<span id="admin/views/helloworld/tmpl/edit.php">
 
<span id="admin/views/helloworld/tmpl/edit.php">
<tt>admin/views/helloworld/tmpl/edit.php</tt>
+
''admin/views/helloworld/tmpl/edit.php''
<source lang="php" highlight="45,48-50">
+
<syntaxhighlight lang="php" highlight="45,48-50">
 
<?php
 
<?php
 
/**
 
/**
Line 1,142: Line 1,142:
 
JHtml::_('behavior.formvalidator');
 
JHtml::_('behavior.formvalidator');
  
// The following is to enable setting the permission's Calculated Setting  
+
// The following is to enable setting the permission's Calculated Setting
// when you change the permission's Setting.  
+
// when you change the permission's Setting.
 
// The core javascript code for initiating the Ajax request looks for a field
 
// The core javascript code for initiating the Ajax request looks for a field
 
// with id="jform_title" and sets its value as the 'title' parameter to send in the Ajax request
 
// with id="jform_title" and sets its value as the 'title' parameter to send in the Ajax request
Line 1,162: Line 1,162:
 
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&layout=edit' . $tmpl . '&id=' . (int) $this->item->id); ?>"
 
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&layout=edit' . $tmpl . '&id=' . (int) $this->item->id); ?>"
 
     method="post" name="adminForm" id="adminForm" class="form-validate">
 
     method="post" name="adminForm" id="adminForm" class="form-validate">
   
+
 
 
     <input id="jform_title" type="hidden" name="helloworld-message-title"/>
 
     <input id="jform_title" type="hidden" name="helloworld-message-title"/>
   
+
 
 
     <div class="form-horizontal">
 
     <div class="form-horizontal">
  
 
     <?php echo JHtml::_('bootstrap.startTabSet', 'myTab', array('active' => 'details')); ?>
 
     <?php echo JHtml::_('bootstrap.startTabSet', 'myTab', array('active' => 'details')); ?>
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'details',  
+
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'details',
 
         empty($this->item->id) ? JText::_('COM_HELLOWORLD_TAB_NEW_MESSAGE') : JText::_('COM_HELLOWORLD_TAB_EDIT_MESSAGE')); ?>
 
         empty($this->item->id) ? JText::_('COM_HELLOWORLD_TAB_NEW_MESSAGE') : JText::_('COM_HELLOWORLD_TAB_EDIT_MESSAGE')); ?>
 
         <fieldset class="adminform">
 
         <fieldset class="adminform">
Line 1,193: Line 1,193:
 
         </fieldset>
 
         </fieldset>
 
     <?php echo JHtml::_('bootstrap.endTab'); ?>
 
     <?php echo JHtml::_('bootstrap.endTab'); ?>
   
+
 
 
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'params', JText::_('COM_HELLOWORLD_TAB_PARAMS')); ?>
 
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'params', JText::_('COM_HELLOWORLD_TAB_PARAMS')); ?>
 
         <fieldset class="adminform">
 
         <fieldset class="adminform">
Line 1,217: Line 1,217:
 
         <?php echo JHtml::_('bootstrap.endTab'); ?>
 
         <?php echo JHtml::_('bootstrap.endTab'); ?>
 
     <?php endif; ?>
 
     <?php endif; ?>
   
+
 
 
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'permissions', JText::_('COM_HELLOWORLD_TAB_PERMISSIONS')); ?>
 
     <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'permissions', JText::_('COM_HELLOWORLD_TAB_PERMISSIONS')); ?>
 
         <fieldset class="adminform">
 
         <fieldset class="adminform">
Line 1,234: Line 1,234:
 
     <?php echo JHtml::_('form.token'); ?>
 
     <?php echo JHtml::_('form.token'); ?>
 
</form>
 
</form>
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
 
<translate>
 
<translate>
Line 1,241: Line 1,241:
  
 
<span id="site/models/helloworld.php">
 
<span id="site/models/helloworld.php">
<tt>site/models/helloworld.php</tt>
+
''site/models/helloworld.php''
<source lang="php" highlight="79">
+
<syntaxhighlight lang="php" highlight="79">
 
<?php
 
<?php
 
/**
 
/**
Line 1,315: Line 1,315:
 
public function getItem($id = null)
 
public function getItem($id = null)
 
{
 
{
if (!isset($this->item) || !is_null($id))  
+
if (!isset($this->item) || !is_null($id))
 
{
 
{
 
$id    = is_null($id) ? $this->getState('message.id') : $id;
 
$id    = is_null($id) ? $this->getState('message.id') : $id;
Line 1,333: Line 1,333:
  
 
$db->setQuery((string)$query);
 
$db->setQuery((string)$query);
+
 
if ($this->item = $db->loadObject())  
+
if ($this->item = $db->loadObject())
 
{
 
{
 
// Load the JSON string
 
// Load the JSON string
Line 1,361: Line 1,361:
 
public function getMapParams()
 
public function getMapParams()
 
{
 
{
if ($this->item)  
+
if ($this->item)
 
{
 
{
 
$url = HelloworldHelperRoute::getAjaxURL();
 
$url = HelloworldHelperRoute::getAjaxURL();
Line 1,371: Line 1,371:
 
'ajaxurl' => $url
 
'ajaxurl' => $url
 
);
 
);
return $this->mapParams;  
+
return $this->mapParams;
 
}
 
}
 
else
 
else
Line 1,381: Line 1,381:
 
public function getMapSearchResults($mapbounds)
 
public function getMapSearchResults($mapbounds)
 
{
 
{
try  
+
try
 
{
 
{
 
$db    = JFactory::getDbo();
 
$db    = JFactory::getDbo();
Line 1,387: Line 1,387:
 
$query->select('h.id, h.alias, h.catid, h.greeting, h.latitude, h.longitude')
 
$query->select('h.id, h.alias, h.catid, h.greeting, h.latitude, h.longitude')
 
  ->from('#__helloworld as h')
 
  ->from('#__helloworld as h')
  ->where('h.latitude > ' . $mapbounds['minlat'] .  
+
  ->where('h.latitude > ' . $mapbounds['minlat'] .
 
' AND h.latitude < ' . $mapbounds['maxlat'] .
 
' AND h.latitude < ' . $mapbounds['maxlat'] .
 
' AND h.longitude > ' . $mapbounds['minlng'] .
 
' AND h.longitude > ' . $mapbounds['minlng'] .
Line 1,399: Line 1,399:
  
 
$db->setQuery($query);
 
$db->setQuery($query);
$results = $db->loadObjectList();  
+
$results = $db->loadObjectList();
 
}
 
}
 
catch (Exception $e)
 
catch (Exception $e)
 
{
 
{
 
$msg = $e->getMessage();
 
$msg = $e->getMessage();
JFactory::getApplication()->enqueueMessage($msg, 'error');  
+
JFactory::getApplication()->enqueueMessage($msg, 'error');
 
$results = null;
 
$results = null;
 
}
 
}
Line 1,417: Line 1,417:
 
}
 
}
  
for ($i = 0; $i < count($results); $i++)  
+
for ($i = 0; $i < count($results); $i++)
 
{
 
{
$results[$i]->url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $results[$i]->id .  
+
$results[$i]->url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $results[$i]->id .
 
":" . $results[$i]->alias . '&catid=' . $results[$i]->catid . $query_lang);
 
":" . $results[$i]->alias . '&catid=' . $results[$i]->catid . $query_lang);
 
}
 
}
  
return $results;  
+
return $results;
 
}
 
}
  
Line 1,433: Line 1,433:
 
}
 
}
 
}
 
}
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
Line 1,439: Line 1,439:
  
 
<span id="site/views/helloworld/tmpl/default.php">
 
<span id="site/views/helloworld/tmpl/default.php">
<tt>site/views/helloworld/tmpl/default.php</tt>
+
''site/views/helloworld/tmpl/default.php''
<source lang="php" highlight="26">
+
<syntaxhighlight lang="php" highlight="26">
 
<?php
 
<?php
 
/**
 
/**
Line 1,488: Line 1,488:
 
<?php endif; ?>
 
<?php endif; ?>
  
<?php if ($this->children) :  
+
<?php if ($this->children) :
 
$baseLevel = $this->item->level; ?>
 
$baseLevel = $this->item->level; ?>
 
<h1><?php echo JText::_('COM_HELLOWORLD_CHILDREN') ?>
 
<h1><?php echo JText::_('COM_HELLOWORLD_CHILDREN') ?>
Line 1,512: Line 1,512:
 
     </div>
 
     </div>
 
</div>
 
</div>
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
  
Line 1,520: Line 1,520:
  
 
<span id="admin/language/en-GB/en-GB.com_helloworld.ini">
 
<span id="admin/language/en-GB/en-GB.com_helloworld.ini">
<tt>admin/language/en-GB/en-GB.com_helloworld.ini</tt>
+
''admin/language/en-GB/en-GB.com_helloworld.ini''
<source lang="text" highlight="28-29">
+
<syntaxhighlight lang="text" highlight="28-29">
 
; Joomla! Project
 
; Joomla! Project
 
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
 
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
Line 1,588: Line 1,588:
 
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
 
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
 
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
 
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on front end form"
+
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on Frontend form"
 
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
 
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on front end"
+
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on Frontend"
 
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
 
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
 
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
 
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
Line 1,629: Line 1,629:
 
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_1="%d record successfully checked in."
 
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_1="%d record successfully checked in."
 
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_MORE="%d records successfully checked in."
 
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_MORE="%d records successfully checked in."
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
 
<translate>
 
<translate>
Line 1,635: Line 1,635:
 
</translate>
 
</translate>
  
<translate><!--T:41--> Contents of your code directory. Each file link below takes you to the step in the tutorial which has the latest version of that source code file.</translate>
+
<translate><!--T:41--> Contents of your code directory. Each file link below takes you to the step in the tutorial that has the latest version of that source code file.</translate>
  
 
* ''[[S:MyLanguage/J3.2:Developing_an_MVC_Component/Adding Versioning#helloworld.xml|helloworld.xml]]''
 
* ''[[S:MyLanguage/J3.2:Developing_an_MVC_Component/Adding Versioning#helloworld.xml|helloworld.xml]]''
Line 1,750: Line 1,750:
  
 
<span id="helloworld.xml">
 
<span id="helloworld.xml">
<tt>helloworld.xml</tt>
+
''helloworld.xml''
<source lang="xml" highlight="13">
+
<syntaxhighlight lang="xml" highlight="13">
 
<?xml version="1.0" encoding="utf-8"?>
 
<?xml version="1.0" encoding="utf-8"?>
 
<extension type="component" version="3.0" method="upgrade">
 
<extension type="component" version="3.0" method="upgrade">
Line 1,850: Line 1,850:
  
 
</extension>
 
</extension>
</source>
+
</syntaxhighlight>
 
</span>
 
</span>
 
<translate>
 
<translate>
Line 1,857: Line 1,857:
 
*[[User:Robbiej|Robbie Jackson]]
 
*[[User:Robbiej|Robbie Jackson]]
  
<div class="row">  
+
<div class="row">
 
<div class="large-6 columns">{{Basic button|S:MyLanguage/J3.x:Developing_an_MVC_Component/Adding Levels|<translate><!--T:43--> Prev: Adding Levels</translate>|class=expand success}}</div>
 
<div class="large-6 columns">{{Basic button|S:MyLanguage/J3.x:Developing_an_MVC_Component/Adding Levels|<translate><!--T:43--> Prev: Adding Levels</translate>|class=expand success}}</div>
 
<div class="large-6 columns">{{Basic button|S:MyLanguage/J3.x:Developing_an_MVC_Component/Adding Tags|<translate><!--T:44--> Next: Adding Tags</translate>|class=expand}}</div>
 
<div class="large-6 columns">{{Basic button|S:MyLanguage/J3.x:Developing_an_MVC_Component/Adding Tags|<translate><!--T:44--> Next: Adding Tags</translate>|class=expand}}</div>

Revision as of 21:03, 13 December 2022

Other languages:
English • ‎español • ‎français • ‎中文(台灣)‎
Joomla! 
3.x
Tutorial
Developing an MVC Component



This is a multiple-article series of tutorials on how to develop a Model-View-Controller Component for Joomla! VersionJoomla 3.x.

Begin with the Introduction, and navigate the articles in this series by using the navigation button at the bottom or the box to the right (the Articles in this series).



This tutorial is part of the Developing an MVC Component for Joomla! 3.2 tutorial. You are encouraged to read the previous parts of the tutorial before reading this.

In this step we enable our helloworld component to maintain versions of records. As versioning is often associated with documents, we add a text 'description' field to our helloworld record.

There are two videos accompanying this tutorial step, covering the Versioning Overview and the Joomla Observer pattern.

  • Versioning Overview

  • Joomla Observer pattern

Introduction[edit]

Joomla implements versioning in its core components by using an observer pattern (aka publish / subscribe pattern). The Joomla content history code that handles the storing of record versions subscribes to the onAfterStore events that are triggered whenever the JTable code performs an insert or update operation on a record. When the trigger fires then this content history versioning code runs, and it writes a copy of the data to its own database table (ucm_history). This copy includes a version number, plus the data of the record as a blob of name-value pairs (amongst other information).

To manage and restore versions there is a Versions button on the record edit form. The Joomla code behind this button runs the com_contenthistory component, which is responsible for displaying the versions stored (by reading the ucm_history records), and enabling the Administrator to manage those versions, including initiating a restore of a previous version to be the current record.

In order to display the record data appropriately, the com_contenthistory component uses configuration data that it looks to find in the content_types table.

Note that this mechanism doesn't support keeping old versions to enable a record to be restored if it is deleted accidentally. If you want this in your component, then you should follow the example of Joomla: whenever an admin "deletes" a record it just sets the status of the record to "Trashed", and the record is only really deleted when the admin "empties the trash". When the record is physically deleted in the database, the event onBeforeDelete is triggered, and on receiving this the Joomla content history versioning code deletes versions of the record in the ucm_history table as well.

Approach[edit]

The Joomla documentation page at S:MyLanguage/Using Content History in your Component gives a list of the aspects required to build version history capability into a component. These aspects are listed below.

  1. We need to get the Joomla JTableObserverContenthistory code – this is the code that is responsible for writing the versioned copies to the ucm_history table – to handle record updates that occur with our component Helloworld table. We do this by calling a function JObserverMapper::addObserverClassToClass and passing as parameters the observer class JTableObserverContenthistory and the class to observe – our HelloWorldTableHelloWorld class. Our class inherits from JTable, and JTable publishes the onAfterStore and onBeforeDelete events that this observer will use to maintain our record versions in the ucm_history table.
  2. We add a Versions button on our edit form. This has standard Joomla code behind it, which results in a modal with an iframe being added to our edit page. When the Versions button is clicked the modal is displayed, and the iframe within it is populated from the com_contenthistory component code, based on the record's versions available in the ucm_history table.
  3. We provide configuration data (which we insert into the content_types table keyed by a type_alias value of com_helloworld.helloworld) to the content history code for two purposes:
    1. to specify under what circumstances a new version should be stored – for example, if a user just clicks on a record to edit it, but then cancels, we wouldn't want a new version to be created. But as this process involves updating the database record via the checkout process, we have to tell the code to ignore the case where just the checkout fields are changed.
    2. to enable the com_contenthistory code to display the data in the record with sensible labels.
  4. We add a helloworld configuration parameter that allows the Administrator to switch on/off versioning for com_helloworld. The content history versioning code (in JTableObserverContenthistory) looks for a com_helloworld save_history parameter to determine if it should create the record versions for the helloworld component.
  5. We add a typeAlias variable to our model. This is just used as a check by Joomla when restoring a version of a helloworld record, ensuring that the typeAlias obtained via the history record matches the value in this typeAlias variable.
  6. We add a version note field to our edit form. A value in this field won't be stored in our helloworld record, but will be stored in the version copy in the ucm_history table, as a label associated with that version.
  7. We add an editor field to the component. This uses an editor form field type, one of Joomla's standard form field types within the admin form, and we output it on the Frontend as well.

In addition, we enable version support for helloworld categories. In general the code associated with helloworld categories checks our save_history parameter to determine whether to store versions of categories, and whether to show the Versions button on the edit category form. However, we must also supply the configuration data for categories in the content_types table.

Database and Install[edit]

We add the description field to the helloworld record, and our configuration data as records in the content_types table. A good explanation of this configuration data is at Using Content History in your Component#Add Labels to Pop-Up Windows.

admin/sql/updates/mysql/0.0.27.sql

ALTER TABLE `#__helloworld` ADD COLUMN `description` VARCHAR(4000) NOT NULL DEFAULT '' AFTER `greeting`;

INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)
VALUES
('Helloworld', 'com_helloworld.helloworld',
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],
"ignoreChanges":["checked_out", "checked_out_time", "path"],
"convertToInt":[],
"displayLookup":[
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"parent_id","targetTable":"#__helloworld","targetColumn":"id","displayColumn":"greeting"},
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
('Helloworld Category', 'com_helloworld.category',
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
"convertToInt":["publish_up", "publish_down"],
"displayLookup":[
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"access","targetTable":"#__viewlevels","targetColumn":"id","displayColumn":"title"},
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');

admin/sql/install.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;

CREATE TABLE `#__helloworld` (
	`id`       INT(11)     NOT NULL AUTO_INCREMENT,
	`asset_id` INT(10)     NOT NULL DEFAULT '0',
	`created`  DATETIME    NOT NULL DEFAULT '0000-00-00 00:00:00',
	`created_by`  INT(10) UNSIGNED NOT NULL DEFAULT '0',
	`checked_out` INT(10) NOT NULL DEFAULT '0',
	`checked_out_time` DATETIME NOT NULL DEFAULT '0000-00-00 00:00:00',
	`greeting` VARCHAR(25) NOT NULL,
	`description` VARCHAR(4000) NOT NULL DEFAULT '',
	`alias`  VARCHAR(40)  NOT NULL DEFAULT '',
	`language`  CHAR(7)  NOT NULL DEFAULT '*',
	`parent_id`	int(10)    NOT NULL DEFAULT '1',
	`level`	int(10)    NOT NULL DEFAULT '0',
	`path`	VARCHAR(400)    NOT NULL DEFAULT '',
	`lft`	int(11)    NOT NULL DEFAULT '0',
	`rgt`	int(11)    NOT NULL DEFAULT '0',
	`published` tinyint(4) NOT NULL DEFAULT '1',
	`catid`	    int(11)    NOT NULL DEFAULT '0',
	`params`   VARCHAR(1024) NOT NULL DEFAULT '',
	`image`   VARCHAR(1024) NOT NULL DEFAULT '',
	`latitude` DECIMAL(9,7) NOT NULL DEFAULT 0.0,
	`longitude` DECIMAL(10,7) NOT NULL DEFAULT 0.0,
	PRIMARY KEY (`id`)
)
	ENGINE =MyISAM
	AUTO_INCREMENT =0
	DEFAULT CHARSET =utf8;

CREATE UNIQUE INDEX `aliasindex` ON `#__helloworld` (`alias`, `catid`);

INSERT INTO `#__helloworld` (`greeting`,`alias`,`language`, `parent_id`, `level`, `path`, `lft`, `rgt`, `published`) VALUES
('helloworld root','helloworld-root-alias','*', 0, 0, '', 0, 5, 1),
('Hello World!','hello-world','en-GB', 1, 1, 'hello-world', 1, 2, 0),
('Goodbye World!','goodbye-world','en-GB', 1, 1, 'goodbye-world', 3, 4, 0);

INSERT INTO `#__content_types` (`type_title`, `type_alias`, `content_history_options`)
VALUES
('Helloworld', 'com_helloworld.helloworld',
'{"formFile":"administrator\\/components\\/com_helloworld\\/models\\/forms\\/helloworld.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path"],
"ignoreChanges":["checked_out", "checked_out_time", "path"],
"convertToInt":[],
"displayLookup":[
{"sourceColumn":"created_by","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"parent_id","targetTable":"#__helloworld","targetColumn":"id","displayColumn":"greeting"},
{"sourceColumn":"catid","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}'),
('Helloworld Category', 'com_helloworld.category',
'{"formFile":"administrator\\/components\\/com_categories\\/models\\/forms\\/category.xml",
"hideFields":["asset_id","checked_out","checked_out_time","version","lft","rgt","level","path","extension"],
"ignoreChanges":["modified_user_id", "modified_time", "checked_out", "checked_out_time", "version", "hits", "path"],
"convertToInt":["publish_up", "publish_down"],
"displayLookup":[
{"sourceColumn":"created_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"access","targetTable":"#__viewlevels","targetColumn":"id","displayColumn":"title"},
{"sourceColumn":"modified_user_id","targetTable":"#__users","targetColumn":"id","displayColumn":"name"},
{"sourceColumn":"parent_id","targetTable":"#__categories","targetColumn":"id","displayColumn":"title"}]}');

If the helloworld component is uninstalled then we should remove the configuration records from the content_types table and remove helloworld record versions from the ucm_history table.

admin/sql/uninstall.mysql.utf8.sql

DROP TABLE IF EXISTS `#__helloworld`;
DELETE FROM `#__ucm_history` WHERE ucm_type_id in
	(select type_id from `#__content_types` where type_alias in ('com_helloworld.helloworld','com_helloworld.category'));
DELETE FROM `#__content_types` WHERE type_alias in ('com_helloworld.helloworld','com_helloworld.category');

In the previous step we used the install script to modify the helloworld records in the database. We don't want that to recur in this step, so we revert the install script to its basic form.

script.php

<?php
// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * Script file of HelloWorld component.
 *
 * The name of this class is dependent on the component being installed.
 * The class name should have the component's name, directly followed by
 * the text InstallerScript (ex:. com_helloWorldInstallerScript).
 *
 * This class will be called by Joomla!'s installer, if specified in your component's
 * manifest file, and is used for custom automation actions in its installation process.
 *
 * In order to use this automation script, you should reference it in your component's
 * manifest file as follows:
 * <scriptfile>script.php</scriptfile>
 *
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
class com_helloWorldInstallerScript
{
    /**
     * This method is called after a component is installed.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function install($parent)
    {
        $parent->getParent()->setRedirectURL('index.php?option=com_helloworld');
    }

    /**
     * This method is called after a component is uninstalled.
     *
     * @param  \stdClass $parent - Parent object calling this method.
     *
     * @return void
     */
    public function uninstall($parent)
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_UNINSTALL_TEXT') . '</p>';
    }

    /**
     * This method is called after a component is updated.
     *
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function update($parent)
    {
        echo '<p>' . JText::sprintf('COM_HELLOWORLD_UPDATE_TEXT', $parent->get('manifest')->version) . '</p>';
    }

    /**
     * Runs just before any installation action is preformed on the component.
     * Verifications and pre-requisites should run in this function.
     *
     * @param  string    $type   - Type of PreFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    public function preflight($type, $parent)
    {
        echo '<p>' . JText::_('COM_HELLOWORLD_PREFLIGHT_' . $type . '_TEXT') . '</p>';
    }

    /**
     * Runs right after any installation action is preformed on the component.
     *
     * @param  string    $type   - Type of PostFlight action. Possible values are:
     *                           - * install
     *                           - * update
     *                           - * discover_install
     * @param  \stdClass $parent - Parent object calling object.
     *
     * @return void
     */
    function postflight($type, $parent)
    {
    }
}

Setup Content History Observer[edit]

We need to tell the Content History code to observe our helloworld table class, and the most logical place to put this code is in our table constructor.

admin/tables/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */
// No direct access
defined('_JEXEC') or die('Restricted access');

/**
 * Hello Table class
 *
 * @since  0.0.1
 */
class HelloWorldTableHelloWorld extends JTableNested
{
	/**
	 * Constructor
	 *
	 * @param   JDatabaseDriver  &$db  A database connector object
	 */
	function __construct(&$db)
	{
		JObserverMapper::addObserverClassToClass('JTableObserverContenthistory', 'HelloWorldTableHelloWorld', array('typeAlias' => 'com_helloworld.helloworld'));
		parent::__construct('#__helloworld', 'id', $db);
	}
	/**
	 * Overloaded bind function
	 *
	 * @param       array           named array
	 * @return      null|string     null is operation was satisfactory, otherwise returns an error
	 * @see JTable:bind
	 * @since 1.5
	 */
	public function bind($array, $ignore = '')
	{
		if (isset($array['params']) && is_array($array['params']))
		{
			// Convert the params field to a string.
			$parameter = new JRegistry;
			$parameter->loadArray($array['params']);
			$array['params'] = (string)$parameter;
		}

        if (isset($array['imageinfo']) && is_array($array['imageinfo']))
		{
			// Convert the imageinfo array to a string.
			$parameter = new JRegistry;
			$parameter->loadArray($array['imageinfo']);
			$array['image'] = (string)$parameter;
		}

        // Bind the rules.
		if (isset($array['rules']) && is_array($array['rules']))
		{
			$rules = new JAccessRules($array['rules']);
			$this->setRules($rules);
		}

		if (isset($array['parent_id']))
		{
			if (!isset($array['id']) || $array['id'] == 0)
			{   // new record
				$this->setLocation($array['parent_id'], 'last-child');
			}
			elseif (isset($array['helloworldordering']))
			{
				// when saving a record load() is called before bind() so the table instance will have properties that are the existing field values
				if ($this->parent_id == $array['parent_id'])
				{
					// If first is chosen make the item the first child of the selected parent.
					if ($array['helloworldordering'] == -1)
					{
						$this->setLocation($array['parent_id'], 'first-child');
					}
					// If last is chosen make it the last child of the selected parent.
					elseif ($array['helloworldordering'] == -2)
					{
						$this->setLocation($array['parent_id'], 'last-child');
					}
					// Don't try to put an item after itself. All other ones put after the selected item.
					elseif ($array['helloworldordering'] && $this->id != $array['helloworldordering'])
					{
						$this->setLocation($array['helloworldordering'], 'after');
					}
					// Just leave it where it is if no change is made.
					elseif ($array['helloworldordering'] && $this->id == $array['helloworldordering'])
					{
						unset($array['helloworldordering']);
					}
				}
				// Set the new parent id if parent id not matched and put in last position
				else
				{
					$this->setLocation($array['parent_id'], 'last-child');
				}
			}
		}

		return parent::bind($array, $ignore);
	}

    /**
	 * Method to compute the default name of the asset.
	 * The default name is in the form `table_name.id`
	 * where id is the value of the primary key of the table.
	 *
	 * @return	string
	 * @since	2.5
	 */
	protected function _getAssetName()
	{
		$k = $this->_tbl_key;
		return 'com_helloworld.helloworld.'.(int) $this->$k;
	}
	/**
	 * Method to return the title to use for the asset table.
	 *
	 * @return	string
	 * @since	2.5
	 */
	protected function _getAssetTitle()
	{
		return $this->greeting;
	}
	/**
	 * Method to get the asset-parent-id of the item
	 *
	 * @return	int
	 */
	protected function _getAssetParentId(JTable $table = NULL, $id = NULL)
	{
		// We will retrieve the parent-asset from the Asset-table
		$assetParent = JTable::getInstance('Asset');
		// Default: if no asset-parent can be found we take the global asset
		$assetParentId = $assetParent->getRootId();

		// Find the parent-asset
		if (($this->catid)&& !empty($this->catid))
		{
			// The item has a category as asset-parent
			$assetParent->loadByName('com_helloworld.category.' . (int) $this->catid);
		}
		else
		{
			// The item has the component as asset-parent
			$assetParent->loadByName('com_helloworld');
		}

		// Return the found asset-parent-id
		if ($assetParent->id)
		{
			$assetParentId=$assetParent->id;
		}
		return $assetParentId;
	}

	public function check()
	{
		$this->alias = trim($this->alias);
		if (empty($this->alias))
		{
			$this->alias = $this->greeting;
		}
		$this->alias = JFilterOutput::stringURLSafe($this->alias);
		return true;
	}

	public function delete($pk = null, $children = false)
	{
		return parent::delete($pk, $children);
	}
}

Add Versions button to the Edit View[edit]

We check that our save_history config parameter is set to true before we display the Versions button.

admin/views/helloworld/view.html.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

/**
 * HelloWorld View
 *
 * @since  0.0.1
 */
class HelloWorldViewHelloWorld extends JViewLegacy
{
	/**
	 * View form
	 *
	 * @var         form
	 */
	protected $form = null;
    protected $canDo;

	/**
	 * Display the Hello World view
	 *
	 * @param   string  $tpl  The name of the template file to parse; automatically searches through the template paths.
	 *
	 * @return  void
	 */
	public function display($tpl = null)
	{
		// Get the Data
		$this->form = $this->get('Form');
		$this->item = $this->get('Item');
        $this->script = $this->get('Script');

        // What Access Permissions does this user have? What can (s)he do?
		$this->canDo = JHelperContent::getActions('com_helloworld', 'helloworld', $this->item->id);

		// Check for errors.
		if (count($errors = $this->get('Errors')))
		{
			JError::raiseError(500, implode('<br />', $errors));

			return false;
		}

		// Set the toolbar
		$this->addToolBar();

		// Display the template
		parent::display($tpl);

		// Set the document
		$this->setDocument();
	}

	/**
	 * Add the page title and toolbar.
	 *
	 * @return  void
	 *
	 * @since   1.6
	 */
	protected function addToolBar()
	{
		$input = JFactory::getApplication()->input;

		// Hide Joomla Administrator Main menu
		$input->set('hidemainmenu', true);

		$isNew = ($this->item->id == 0);

		JToolBarHelper::title($isNew ? JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW')
		                             : JText::_('COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT'), 'helloworld');
		// Build the actions for new and existing records.
		if ($isNew)
		{
			// For new records, check the create permission.
			if ($this->canDo->get('core.create'))
			{
				JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
				JToolBarHelper::save('helloworld.save', 'JTOOLBAR_SAVE');
				JToolBarHelper::custom('helloworld.save2new', 'save-new.png', 'save-new_f2.png',
				                       'JTOOLBAR_SAVE_AND_NEW', false);
			}
			JToolBarHelper::cancel('helloworld.cancel', 'JTOOLBAR_CANCEL');
		}
		else
		{
			if ($this->canDo->get('core.edit'))
			{
				// We can save the new record
				JToolBarHelper::apply('helloworld.apply', 'JTOOLBAR_APPLY');
				JToolBarHelper::save('helloworld.save', 'JTOOLBAR_SAVE');

				// We can save this record, but check the create permission to see
				// if we can return to make a new one.
				if ($this->canDo->get('core.create'))
				{
					JToolBarHelper::custom('helloworld.save2new', 'save-new.png', 'save-new_f2.png',
					                       'JTOOLBAR_SAVE_AND_NEW', false);
				}
				$config = JFactory::getConfig();
				$save_history = $config->get('save_history', true);
				if ($save_history)
				{
					JToolbarHelper::versions('com_helloworld.helloworld', $this->item->id);
				}
			}
			if ($this->canDo->get('core.create'))
			{
				JToolBarHelper::custom('helloworld.save2copy', 'save-copy.png', 'save-copy_f2.png',
				                       'JTOOLBAR_SAVE_AS_COPY', false);
			}
			JToolBarHelper::cancel('helloworld.cancel', 'JTOOLBAR_CLOSE');
		}
	}
	/**
	 * Method to set up the document properties
	 *
	 * @return void
	 */
	protected function setDocument()
	{
		$isNew = ($this->item->id < 1);
		$document = JFactory::getDocument();
		$document->setTitle($isNew ? JText::_('COM_HELLOWORLD_HELLOWORLD_CREATING') :
                JText::_('COM_HELLOWORLD_HELLOWORLD_EDITING'));
        $document->addScript(JURI::root() . $this->script);
		$document->addScript(JURI::root() . "/administrator/components/com_helloworld"
		                                  . "/views/helloworld/submitbutton.js");
		JText::script('COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE');
	}
}

Add Configuration Parameters[edit]

We add the save_history parameter together with a history_limit parameter that specifies the number of versions to keep.

admin/config.xml

<?xml version="1.0" encoding="utf-8"?>
<config>
	<fieldset
		name="greetings"
		label="COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL"
		description="COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC"
	>
		<field
			name="show_category"
			type="radio"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
			default="0"
		>
			<option value="0">JHIDE</option>
			<option value="1">JSHOW</option>
		</field>
        <field
			name="captcha"
			type="plugins"
            folder="captcha"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC"
			default="0"
			filter="cmd"
		>
            <option value="">JOPTION_USE_DEFAULT</option>
			<option value="0">JOPTION_DO_NOT_USE</option>
		</field>
        <field
			name="user_to_email"
			type="user"
			label="COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL"
			description="COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC"
			default="0"
		>
		</field>
		<field
			name="save_history"
			type="radio"
			class="btn-group btn-group-yesno"
			default="1"
			label="JGLOBAL_SAVE_HISTORY_OPTIONS_LABEL"
			description="JGLOBAL_SAVE_HISTORY_OPTIONS_DESC"
		>
			<option
				value="0">JNO</option>
			<option
				value="1">JYES</option>
		</field>
		<field
			name="history_limit"
			type="text"
			filter="integer"
			label="JGLOBAL_HISTORY_LIMIT_OPTIONS_LABEL"
			description="JGLOBAL_HISTORY_LIMIT_OPTIONS_DESC"
			default="5"
		>
		</field>
	</fieldset>
    <fieldset
		name="permissions"
		label="JCONFIG_PERMISSIONS_LABEL"
		description="JCONFIG_PERMISSIONS_DESC"
	>
		<field
			name="rules"
			type="rules"
			label="JCONFIG_PERMISSIONS_LABEL"
			class="inputbox"
			validate="rules"
			filter="rules"
			component="com_helloworld"
			section="component"
		/>
	</fieldset>
</config>

Add typeAlias to the Model[edit]

admin/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

use Joomla\Registry\Registry;

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelAdmin
{
    // JModelAdmin needs to know this for storing the associations
	protected $associationsContext = 'com_helloworld.item';

	// Contenthistory needs to know this for restoring previous versions
	public $typeAlias = 'com_helloworld.helloworld';

	/**
	 * Method to override getItem to allow us to convert the JSON-encoded image information
	 * in the database record into an array for subsequent prefilling of the edit form
     * We also use this method to prefill the associations
	 */
	public function getItem($pk = null)
	{
		$item = parent::getItem($pk);
		if ($item AND property_exists($item, 'image'))
		{
			$registry = new Registry($item->image);
			$item->imageinfo = $registry->toArray();
		}

        // Load associated items
		if (JLanguageAssociations::isEnabled())
		{
			$item->associations = array();

			if ($item->id != null)
			{
				$associations = JLanguageAssociations::getAssociations('com_helloworld', '#__helloworld', 'com_helloworld.item', (int)$item->id);

				foreach ($associations as $tag => $association)
				{
					$item->associations[$tag] = $association->id;
				}
			}
		}
		return $item;
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Method to get the record form.
	 *
	 * @param   array    $data      Data for the form.
	 * @param   boolean  $loadData  True if the form is to load its own data (default case), false if not.
	 *
	 * @return  mixed    A JForm object on success, false on failure
	 *
	 * @since   1.6
	 */
	public function getForm($data = array(), $loadData = true)
	{
		// Get the form.
		$form = $this->loadForm(
			'com_helloworld.helloworld',
			'helloworld',
			array(
				'control' => 'jform',
				'load_data' => $loadData
			)
		);

		if (empty($form))
		{
			return false;
		}

		return $form;
	}

    /**
	 * Method to preprocess the form to add the association fields dynamically
	 *
	 * @return     none
	 */
	protected function preprocessForm(JForm $form, $data, $group = 'helloworld')
	{
		// Association content items
		if (JLanguageAssociations::isEnabled())
		{
			$languages = JLanguageHelper::getContentLanguages(false, true, null, 'ordering', 'asc');

			if (count($languages) > 1)
			{
				$addform = new SimpleXMLElement('<form />');
				$fields = $addform->addChild('fields');
				$fields->addAttribute('name', 'associations');
				$fieldset = $fields->addChild('fieldset');
				$fieldset->addAttribute('name', 'item_associations');

				foreach ($languages as $language)
				{
					$field = $fieldset->addChild('field');
					$field->addAttribute('name', $language->lang_code);
					$field->addAttribute('type', 'modal_helloworld');
					$field->addAttribute('language', $language->lang_code);
					$field->addAttribute('label', $language->title);
					$field->addAttribute('translate_label', 'false');
				}

				$form->load($addform, false);
			}
		}
		parent::preprocessForm($form, $data, $group);
	}

	/**
	 * Method to get the script to be included on the form
	 *
	 * @return string	Script files
	 */
	public function getScript()
	{
		return 'administrator/components/com_helloworld/models/forms/helloworld.js';
	}

	/**
	 * Method to get the data that should be injected in the form.
	 *
	 * @return  mixed  The data for the form.
	 *
	 * @since   1.6
	 */
	protected function loadFormData()
	{
		// Check the session for previously entered form data.
		$data = JFactory::getApplication()->getUserState(
			'com_helloworld.edit.helloworld.data',
			array()
		);

		if (empty($data))
		{
			$data = $this->getItem();
		}

		return $data;
	}
	/**
	 * Method to override the JModelAdmin save() function to handle Save as Copy correctly
	 *
	 * @param   The helloworld record data submitted from the form.
	 *
	 * @return  parent::save() return value
	 */
	public function save($data)
	{
		$input = JFactory::getApplication()->input;

		JLoader::register('CategoriesHelper', JPATH_ADMINISTRATOR . '/components/com_categories/helpers/categories.php');

		// Validate the category id
		// validateCategoryId() returns 0 if the catid can't be found
		if ((int) $data['catid'] > 0)
		{
			$data['catid'] = CategoriesHelper::validateCategoryId($data['catid'], 'com_helloworld');
		}

		// Alter the greeting and alias for save as copy
		if ($input->get('task') == 'save2copy')
		{
			$origTable = clone $this->getTable();
			$origTable->load($input->getInt('id'));

			if ($data['greeting'] == $origTable->greeting)
			{
				list($greeting, $alias) = $this->generateNewTitle($data['catid'], $data['alias'], $data['greeting']);
				$data['greeting'] = $greeting;
				$data['alias'] = $alias;
			}
			else
			{
				if ($data['alias'] == $origTable->alias)
				{
					$data['alias'] = '';
				}
			}
			// standard Joomla practice is to set the new record as unpublished
			$data['published'] = 0;
		}

		$result = parent::save($data);
		if ($result)
		{
			$this->getTable()->rebuild(1);
		}
		return $result;
	}
	/**
	 * Method to check if it's OK to delete a message. Overrides JModelAdmin::canDelete
	 */
	protected function canDelete($record)
	{
		if( !empty( $record->id ) )
		{
			return JFactory::getUser()->authorise( "core.delete", "com_helloworld.helloworld." . $record->id );
		}
	}
	/**
	 * Prepare a helloworld record for saving in the database
	 */
	protected function prepareTable($table)
	{
	}

	/**
	 * Save the record reordering after a record is dragged to a new position in the helloworlds view
	 */
	public function saveorder($idArray = null, $lft_array = null)
	{
		// Get an instance of the table object.
		$table = $this->getTable();

		if (!$table->saveorder($idArray, $lft_array))
		{
			$this->setError($table->getError());

			return false;
		}

		return true;
	}
}

Add Version Note and Description fields[edit]

We need to add these to our edit form xml definition and Joomla expects the version note field to be called "version_note". We put the version_note field within the "details" fieldset so that it will be output in the layout with the other fields in the renderFieldset('details') call. The description field we'll place separately on the right side of the page on the admin form, and we'll add it on the Frontend as well.

admin/models/forms/helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<form
				addrulepath="/administrator/components/com_helloworld/models/rules"
>
	<fieldset
				name="details"
				label="COM_HELLOWORLD_HELLOWORLD_DETAILS"
	>
		<field
				name="id"
				type="hidden"
				/>
                <field
				name="greeting"
				type="text"
				label="COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_GREETING_DESC"
				size="40"
				class="inputbox validate-greeting"
				validate="greeting"
				required="true"
				default=""
				/>
		<field
				name="alias"
				type="text"
				label="JFIELD_ALIAS_LABEL"
				description="JFIELD_ALIAS_DESC"
				hint="JFIELD_ALIAS_PLACEHOLDER"
				size="40"
				/>
        <field
				name="catid"
				type="category"
				extension="com_helloworld"
				class="inputbox"
				default=""
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC"
				required="true"
		>
			<option value="0">JOPTION_SELECT_CATEGORY</option>
		</field>
		<field
				name="latitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC"
				min="-90.0"
				max="90.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
				name="longitude"
				type="number"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC"
				min="-180.0"
				max="180.0"
				class="inputbox"
				required="true"
				default="0.0"
				/>
		<field
                name="language"
        		type="contentlanguage"
                label="JFIELD_LANGUAGE_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC"
		>
			<option value="*">JALL</option>
		</field>
		<field
				name="parent_id"
				type="helloworldparent"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC"
				default="1"
				filter="int">
		</field>
		<field
				name="helloworldordering"
				type="helloworldordering"
				label="JFIELD_ORDERING_LABEL"
				description="JFIELD_ORDERING_DESC"
				filter="int"
				size="1">
		</field>
		<field 	name="version_note"
				type="text"
				label="JGLOBAL_FIELD_VERSION_NOTE_LABEL"
				description="JGLOBAL_FIELD_VERSION_NOTE_DESC"
				class="inputbox"
				size="45"
				labelclass="control-label">
		</field>
    </fieldset>
		<field 	name="description"
				type="editor"
				label="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_LABEL"
				description="COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_DESC"
				filter="JComponentHelper::filterText"
				buttons="true"
		/>
	<fields name="imageinfo">
		<fieldset
				name="image-info"
				label="COM_HELLOWORLD_IMAGE_FIELDS"
		>
			<field
                name="image"
                type="media"
                preview="tooltip"
                label="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL"
                description="COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC" />
            <field name="alt"
                type="text"
                label="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL"
                description="COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC"
                size="30"/>
            <field name="caption"
                type="text"
                label="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL"
                description="COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC"
                size="30"/>
		</fieldset>
	</fields>
	<fields name="params">
		<fieldset
				name="params"
				label="JGLOBAL_FIELDSET_DISPLAY_OPTIONS"
		>
			<field
					name="show_category"
					type="list"
					label="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL"
					description="COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC"
					default=""
			>
				<option value="">JGLOBAL_USE_GLOBAL</option>
				<option value="0">JHIDE</option>
				<option value="1">JSHOW</option>
			</field>
		</fieldset>
	</fields>
	<fieldset
			name="accesscontrol"
			label="COM_HELLOWORLD_FIELDSET_RULES"
	>
    	<field
				name="asset_id"
				type="hidden"
				filter="unset"
				/>
    	<field
				name="rules"
				type="rules"
				label="COM_HELLOWORLD_FIELD_RULES_LABEL"
				filter="rules"
				validate="rules"
				class="inputbox"
				component="com_helloworld"
				section="message"
				/>
    </fieldset>
</form>

admin/views/helloworld/tmpl/edit.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2015 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access
defined('_JEXEC') or die('Restricted access');
JHtml::_('behavior.formvalidator');

// The following is to enable setting the permission's Calculated Setting
// when you change the permission's Setting.
// The core javascript code for initiating the Ajax request looks for a field
// with id="jform_title" and sets its value as the 'title' parameter to send in the Ajax request
JFactory::getDocument()->addScriptDeclaration('
	jQuery(document).ready(function() {
        greeting = jQuery("#jform_greeting").val();
		jQuery("#jform_title").val(greeting);
	});
');

// Required for proper display of fields generated by com_associations
JHtml::_('formbehavior.chosen', 'select');

// if &tmpl=component used on first invocation, ensure it's on subsequent ones too
$input = JFactory::getApplication()->input;
$tmpl = $input->getCmd('tmpl', '') === 'component' ? '&tmpl=component' : '';
?>
<form action="<?php echo JRoute::_('index.php?option=com_helloworld&layout=edit' . $tmpl . '&id=' . (int) $this->item->id); ?>"
    method="post" name="adminForm" id="adminForm" class="form-validate">

    <input id="jform_title" type="hidden" name="helloworld-message-title"/>

    <div class="form-horizontal">

    <?php echo JHtml::_('bootstrap.startTabSet', 'myTab', array('active' => 'details')); ?>
    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'details',
        empty($this->item->id) ? JText::_('COM_HELLOWORLD_TAB_NEW_MESSAGE') : JText::_('COM_HELLOWORLD_TAB_EDIT_MESSAGE')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_DETAILS') ?></legend>
            <div class="row-fluid">
                <div class="span3">
                    <?php echo $this->form->renderFieldset('details');  ?>
                </div>
                <div class="span9">
                    <?php echo $this->form->getInput('description');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'image', JText::_('COM_HELLOWORLD_TAB_IMAGE')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_IMAGE') ?></legend>
            <div class="row-fluid">
                <div class="span6">
                    <?php echo $this->form->renderFieldset('image-info');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'params', JText::_('COM_HELLOWORLD_TAB_PARAMS')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PARAMS') ?></legend>
            <div class="row-fluid">
                <div class="span6">
                    <?php echo $this->form->renderFieldset('params');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>

    <?php if (JLanguageAssociations::isEnabled()) : ?>
        <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'associations', JText::_('COM_HELLOWORLD_TAB_ASSOCIATIONS')); ?>
            <fieldset class="adminform">
                <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_ASSOCIATIONS') ?></legend>
                <div class="row-fluid">
                    <div class="span12">
                        <?php echo JLayoutHelper::render('joomla.edit.associations', $this);  ?>
                    </div>
                </div>
            </fieldset>
        <?php echo JHtml::_('bootstrap.endTab'); ?>
    <?php endif; ?>

    <?php echo JHtml::_('bootstrap.addTab', 'myTab', 'permissions', JText::_('COM_HELLOWORLD_TAB_PERMISSIONS')); ?>
        <fieldset class="adminform">
            <legend><?php echo JText::_('COM_HELLOWORLD_LEGEND_PERMISSIONS') ?></legend>
            <div class="row-fluid">
                <div class="span12">
                    <?php echo $this->form->renderFieldset('accesscontrol');  ?>
                </div>
            </div>
        </fieldset>
    <?php echo JHtml::_('bootstrap.endTab'); ?>
    <?php echo JHtml::_('bootstrap.endTabSet'); ?>

    </div>
    <input type="hidden" name="task" value="helloworld.edit" />
    <?php echo JHtml::_('form.token'); ?>
</form>

Include the description field in the query within the model.

site/models/helloworld.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');

JLoader::register('HelloworldHelperRoute', JPATH_ROOT . '/components/com_helloworld/helpers/route.php');

/**
 * HelloWorld Model
 *
 * @since  0.0.1
 */
class HelloWorldModelHelloWorld extends JModelItem
{
	/**
	 * @var object item
	 */
	protected $item;

	/**
	 * Method to auto-populate the model state.
	 *
	 * This method should only be called once per instantiation and is designed
	 * to be called on the first call to the getState() method unless the model
	 * configuration flag to ignore the request is set.
	 *
	 * Note. Calling getState in this method will result in recursion.
	 *
	 * @return	void
	 * @since	2.5
	 */
	protected function populateState()
	{
		// Get the message id
		$jinput = JFactory::getApplication()->input;
		$id     = $jinput->get('id', 1, 'INT');
		$this->setState('message.id', $id);

		// Load the parameters.
		$this->setState('params', JFactory::getApplication()->getParams());
		parent::populateState();
	}

	/**
	 * Method to get a table object, load it if necessary.
	 *
	 * @param   string  $type    The table name. Optional.
	 * @param   string  $prefix  The class prefix. Optional.
	 * @param   array   $config  Configuration array for model. Optional.
	 *
	 * @return  JTable  A JTable object
	 *
	 * @since   1.6
	 */
	public function getTable($type = 'HelloWorld', $prefix = 'HelloWorldTable', $config = array())
	{
		return JTable::getInstance($type, $prefix, $config);
	}

	/**
	 * Get the message
	 * @return object The message to be displayed to the user
	 */
	public function getItem($id = null)
	{
		if (!isset($this->item) || !is_null($id))
		{
			$id    = is_null($id) ? $this->getState('message.id') : $id;
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.greeting, h.params, h.image as image, c.title as category, h.latitude as latitude, h.longitude as longitude,
						h.id as id, h.alias as alias, h.catid as catid, h.parent_id as parent_id, h.level as level, h.description as description')
				  ->from('#__helloworld as h')
				  ->leftJoin('#__categories as c ON h.catid=c.id')
				  ->where('h.id=' . (int)$id);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery((string)$query);

			if ($this->item = $db->loadObject())
			{
				// Load the JSON string
				$params = new JRegistry;
				$params->loadString($this->item->params, 'JSON');
				$this->item->params = $params;

				// Merge global params with item params
				$params = clone $this->getState('params');
				$params->merge($this->item->params);
				$this->item->params = $params;

				// Convert the JSON-encoded image info into an array
				$image = new JRegistry;
				$image->loadString($this->item->image, 'JSON');
				$this->item->imageDetails = $image;
			}
			else
			{
				throw new Exception('Helloworld id not found', 404);
			}
		}
		return $this->item;
	}

	public function getMapParams()
	{
		if ($this->item)
		{
			$url = HelloworldHelperRoute::getAjaxURL();
			$this->mapParams = array(
				'latitude' => $this->item->latitude,
				'longitude' => $this->item->longitude,
				'zoom' => 10,
				'greeting' => $this->item->greeting,
				'ajaxurl' => $url
			);
			return $this->mapParams;
		}
		else
		{
			throw new Exception('No helloworld details available for map', 500);
		}
	}

	public function getMapSearchResults($mapbounds)
	{
		try
		{
			$db    = JFactory::getDbo();
			$query = $db->getQuery(true);
			$query->select('h.id, h.alias, h.catid, h.greeting, h.latitude, h.longitude')
			   ->from('#__helloworld as h')
			   ->where('h.latitude > ' . $mapbounds['minlat'] .
				' AND h.latitude < ' . $mapbounds['maxlat'] .
				' AND h.longitude > ' . $mapbounds['minlng'] .
				' AND h.longitude < ' . $mapbounds['maxlng']);

			if (JLanguageMultilang::isEnabled())
			{
				$lang = JFactory::getLanguage()->getTag();
				$query->where('h.language IN ("*","' . $lang . '")');
			}

			$db->setQuery($query);
			$results = $db->loadObjectList();
		}
		catch (Exception $e)
		{
			$msg = $e->getMessage();
			JFactory::getApplication()->enqueueMessage($msg, 'error');
			$results = null;
		}

		if (JLanguageMultilang::isEnabled())
		{
			$query_lang = "&lang={$lang}";
		}
		else
		{
			$query_lang = "";
		}

		for ($i = 0; $i < count($results); $i++)
		{
			$results[$i]->url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $results[$i]->id .
				":" . $results[$i]->alias . '&catid=' . $results[$i]->catid . $query_lang);
		}

		return $results;
	}

	public function getChildren($id)
	{
		$table = $this->getTable();
		$children = $table->getTree($id);
		return $children;
	}
}

Output the description field in the layout.

site/views/helloworld/tmpl/default.php

<?php
/**
 * @package     Joomla.Administrator
 * @subpackage  com_helloworld
 *
 * @copyright   Copyright (C) 2005 - 2018 Open Source Matters, Inc. All rights reserved.
 * @license     GNU General Public License version 2 or later; see LICENSE.txt
 */

// No direct access to this file
defined('_JEXEC') or die('Restricted access');
$lang = JFactory::getLanguage()->getTag();
if (JLanguageMultilang::isEnabled() && $lang)
{
    $query_lang = "&lang={$lang}";
}
else
{
    $query_lang = "";
}
?>
<h1><?php echo $this->item->greeting.(($this->item->category and $this->item->params->get('show_category'))
                                      ? (' ('.$this->item->category.')') : ''); ?>
</h1>
<?php
    echo $this->item->description;
    $src = $this->item->imageDetails['image'];
    if ($src)
    {
        $html = '<figure>
                    <img src="%s" alt="%s" >
                    <figcaption>%s</figcaption>
                </figure>';
        $alt = $this->item->imageDetails['alt'];
        $caption = $this->item->imageDetails['caption'];
        echo sprintf($html, $src, $alt, $caption);
    } ?>

<?php if ($this->parentItem->id > 1) : ?>
	<h1><?php echo JText::_('COM_HELLOWORLD_PARENT') ?>
	</h1>
	<h3>
		<?php $url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $this->parentItem->id . ':' . $this->parentItem->alias . '&catid=' . $this->parentItem->catid . $query_lang); ?>
		<a href="<?php echo $url; ?>"><?php echo $this->parentItem->greeting; ?></a>
	</h3>
<?php endif; ?>

<?php if ($this->children) :
		$baseLevel = $this->item->level; ?>
		<h1><?php echo JText::_('COM_HELLOWORLD_CHILDREN') ?>
		</h1>
		<?php foreach ($this->children as $i => $child) : ?>
			<h3>
				<?php $prefix = JLayoutHelper::render('joomla.html.treeprefix', array('level' => $child->level - $baseLevel)); ?>
				<?php echo $prefix; ?>
				<?php $url = JRoute::_('index.php?option=com_helloworld&view=helloworld&id=' . $child->id . ':' . $child->alias . '&catid=' . $child->catid . $query_lang); ?>
				<a href="<?php echo $url; ?>"><?php echo $child->greeting; ?></a>
			</h3>
	<?php endforeach; ?>
<?php endif; ?>

<div id="map" class="map"></div>
<div class="map-callout map-callout-bottom" id="greeting-container"></div>
<div id="searchmap">
    <?php echo '<input id="token" type="hidden" name="' . JSession::getFormToken() . '" value="1" />'; ?>
    <button type="button" class="btn btn-primary" onclick="searchHere();">
        <?php echo JText::_('COM_HELLOWORLD_SEARCH_HERE_BUTTON') ?>
    </button>
    <div id="searchresults">
    </div>
</div>

Updated Language Strings[edit]

admin/language/en-GB/en-GB.com_helloworld.ini

; Joomla! Project
; Copyright (C) 2005 - 2018 Open Source Matters. All rights reserved.
; License GNU General Public License version 2 or later; see LICENSE.txt, see LICENSE.php
; Note : All ini files need to be saved as UTF-8

COM_HELLOWORLD_ADMINISTRATION="HelloWorld - Administration"
COM_HELLOWORLD_ADMINISTRATION_CATEGORIES="HelloWorld - Categories"
COM_HELLOWORLD_NUM="#"
COM_HELLOWORLD_HELLOWORLDS_FILTER="Filters"
COM_HELLOWORLD_AUTHOR="Author"
COM_HELLOWORLD_LANGUAGE="Language"
COM_HELLOWORLD_CREATED_DATE="Created"
COM_HELLOWORLD_PUBLISHED="Published"
COM_HELLOWORLD_HELLOWORLDS_NAME="Name"
COM_HELLOWORLD_HELLOWORLDS_POSITION="Position"
COM_HELLOWORLD_HELLOWORLDS_IMAGE="Image"
COM_HELLOWORLD_HELLOWORLDS_ASSOCIATIONS="Associations"
COM_HELLOWORLD_ID="Id"

COM_HELLOWORLD_HELLOWORLD_CREATING="HelloWorld - Creating"
COM_HELLOWORLD_HELLOWORLD_DETAILS="Details"
COM_HELLOWORLD_HELLOWORLD_EDITING="HelloWorld - Editing"
COM_HELLOWORLD_HELLOWORLD_ERROR_UNACCEPTABLE="Some values are unacceptable"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_DESC="The category the messages belongs to"
COM_HELLOWORLD_HELLOWORLD_FIELD_CATID_LABEL="Category"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_DESC="This message will be displayed"
COM_HELLOWORLD_HELLOWORLD_FIELD_GREETING_LABEL="Message"
COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_DESC="Message description"
COM_HELLOWORLD_HELLOWORLD_FIELD_DESCRIPTION_LABEL="Description"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_LABEL="Show category"
COM_HELLOWORLD_HELLOWORLD_FIELD_SHOW_CATEGORY_DESC="If set to Show, the title of the message&rsquo;s category will show."
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_LABEL="Latitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LATITUDE_DESC="Enter the position latitude, between -90 and +90 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_LABEL="Longitude"
COM_HELLOWORLD_HELLOWORLD_FIELD_LONGITUDE_DESC="Enter the position longitude, between -180 and +180 degrees"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_LABEL="Parent"
COM_HELLOWORLD_HELLOWORLD_FIELD_PARENT_DESC="Select the parent record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_FIRST="-- First record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_VALUE_LAST="-- Last record"
COM_HELLOWORLD_ITEM_FIELD_ORDERING_TEXT="Ordering will be available after saving."
COM_HELLOWORLD_HELLOWORLD_FIELD_LANGUAGE_DESC="Select the appropriate language"
COM_HELLOWORLD_IMAGE_FIELDS="Image details"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_LABEL="Select image"
COM_HELLOWORLD_HELLOWORLD_FIELD_IMAGE_DESC="Select an image from the library, or upload a new one"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_LABEL="Alt text"
COM_HELLOWORLD_HELLOWORLD_FIELD_ALT_DESC="Alternative text (if image cannot be displayed)"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_LABEL="Caption"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTION_DESC="Provide a caption for the image"
COM_HELLOWORLD_HELLOWORLD_HEADING_GREETING="Greeting"
COM_HELLOWORLD_HELLOWORLD_HEADING_ID="Id"
COM_HELLOWORLD_MANAGER_HELLOWORLD_EDIT="HelloWorld manager: Edit Message"
COM_HELLOWORLD_MANAGER_HELLOWORLD_NEW="HelloWorld manager: New Message"
COM_HELLOWORLD_MANAGER_HELLOWORLDS="HelloWorld manager"
COM_HELLOWORLD_EDIT_HELLOWORLD="Edit message"
COM_HELLOWORLD_N_ITEMS_DELETED_1="One message deleted"
COM_HELLOWORLD_N_ITEMS_DELETED_MORE="%d messages deleted"
COM_HELLOWORLD_N_ITEMS_PUBLISHED="%d message(s) published"
COM_HELLOWORLD_N_ITEMS_UNPUBLISHED="%d message(s) unpublished"
COM_HELLOWORLD_HELLOWORLD_GREETING_LABEL="Greeting"
COM_HELLOWORLD_HELLOWORLD_GREETING_DESC="Add Hello World Greeting"
COM_HELLOWORLD_SUBMENU_MESSAGES="Messages"
COM_HELLOWORLD_SUBMENU_CATEGORIES="Categories"
COM_HELLOWORLD_CONFIGURATION="HelloWorld Configuration"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_LABEL="Messages settings"
COM_HELLOWORLD_CONFIG_GREETING_SETTINGS_DESC="Settings that will be applied to all messages by default"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_LABEL="Captcha"
COM_HELLOWORLD_HELLOWORLD_FIELD_CAPTCHA_DESC="Select Captcha to use on Frontend form"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_LABEL="User to email"
COM_HELLOWORLD_HELLOWORLD_FIELD_USER_TO_EMAIL_DESC="Select user to email when a new message is entered on Frontend"
COM_HELLOWORLD_FIELDSET_RULES="Message Permissions"
COM_HELLOWORLD_FIELD_RULES_LABEL="Permissions"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to edit this message?"
COM_HELLOWORLD_ACCESS_DELETE_DESC="Is this group allowed to delete this message?"
COM_HELLOWORLD_TAB_NEW_MESSAGE="New Message"
COM_HELLOWORLD_TAB_EDIT_MESSAGE="Message Details"
COM_HELLOWORLD_TAB_PARAMS="Parameters"
COM_HELLOWORLD_TAB_ASSOCIATIONS="Associations"
COM_HELLOWORLD_TAB_PERMISSIONS="Permissions"
COM_HELLOWORLD_TAB_IMAGE="Image"
COM_HELLOWORLD_LEGEND_DETAILS="Message Details"
COM_HELLOWORLD_LEGEND_PARAMS="Message Parameters"
COM_HELLOWORLD_LEGEND_ASSOCIATIONS="Message Associations"
COM_HELLOWORLD_LEGEND_PERMISSIONS="Message Permissions"
COM_HELLOWORLD_LEGEND_IMAGE="Image info"
; Column ordering in the Helloworlds view
COM_HELLOWORLD_ORDERING_ASC="Ordering ascending"
COM_HELLOWORLD_ORDERING_DESC="Ordering descending"
COM_HELLOWORLD_GREETING_ASC="Greeting ascending"
COM_HELLOWORLD_GREETING_DESC="Greeting descending"
COM_HELLOWORLD_AUTHOR_ASC="Author ascending"
COM_HELLOWORLD_AUTHOR_DESC="Author descending"
COM_HELLOWORLD_CREATED_ASC="Creation date ascending"
COM_HELLOWORLD_CREATED_DESC="Creation date descending"
COM_HELLOWORLD_PUBLISHED_ASC="Unpublished first"
COM_HELLOWORLD_PUBLISHED_DESC="Published first"
COM_HELLOWORLD_LANGUAGE_ASC="Language ascending"
COM_HELLOWORLD_LANGUAGE_DESC="Language descending"
COM_HELLOWORLD_ASSOCIATION_ASC="Association ascending"
COM_HELLOWORLD_ASSOCIATION_DESC="Association descending"
; Helloworld menuitem - selecting a greeting via modal
COM_HELLOWORLD_MENUITEM_SELECT_MODAL_TITLE="Select greeting"
COM_HELLOWORLD_MENUITEM_SELECT_HELLOWORLD="Select"
COM_HELLOWORLD_MENUITEM_SELECT_BUTTON_TOOLTIP="Select a helloworld greeting"
; Checking in records
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_0="No records checked in."
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_1="%d record successfully checked in."
COM_HELLOWORLD_N_ITEMS_CHECKED_IN_MORE="%d records successfully checked in."

Packaging the Component[edit]

Contents of your code directory. Each file link below takes you to the step in the tutorial that has the latest version of that source code file.

helloworld.xml

<?xml version="1.0" encoding="utf-8"?>
<extension type="component" version="3.0" method="upgrade">

	<name>COM_HELLOWORLD</name>
	<!-- The following elements are optional and free of formatting constraints -->
	<creationDate>January 2018</creationDate>
	<author>John Doe</author>
	<authorEmail>john.doe@example.org</authorEmail>
	<authorUrl>http://www.example.org</authorUrl>
	<copyright>Copyright Info</copyright>
	<license>License Info</license>
	<!--  The version string is recorded in the components table -->
	<version>0.0.27</version>
	<!-- The description is optional and defaults to the name -->
	<description>COM_HELLOWORLD_DESCRIPTION</description>

	<!-- Runs on install/uninstall/update; New in 2.5 -->
	<scriptfile>script.php</scriptfile>

	<install> <!-- Runs on install -->
		<sql>
			<file driver="mysql" charset="utf8">sql/install.mysql.utf8.sql</file>
		</sql>
	</install>
	<uninstall> <!-- Runs on uninstall -->
		<sql>
			<file driver="mysql" charset="utf8">sql/uninstall.mysql.utf8.sql</file>
		</sql>
	</uninstall>
	<update> <!-- Runs on update; New since J2.5 -->
		<schemas>
			<schemapath type="mysql">sql/updates/mysql</schemapath>
		</schemas>
	</update>

	<!-- Site Main File Copy Section -->
	<!-- Note the folder attribute: This attribute describes the folder
		to copy FROM in the package to install therefore files copied
		in this section are copied from /site/ in the package -->
	<files folder="site">
		<filename>index.html</filename>
		<filename>helloworld.php</filename>
		<filename>controller.php</filename>
		<filename>router.php</filename>
		<folder>controllers</folder>
		<folder>views</folder>
		<folder>models</folder>
		<folder>helpers</folder>
	</files>

		<languages folder="site/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
		</languages>

	<media destination="com_helloworld" folder="media">
		<filename>index.html</filename>
		<folder>images</folder>
		<folder>js</folder>
		<folder>css</folder>
	</media>

	<administration>
		<!-- Administration Menu Section -->
		<menu link='index.php?option=com_helloworld' img="../media/com_helloworld/images/tux-16x16.png">COM_HELLOWORLD_MENU</menu>
		<!-- Administration Main File Copy Section -->
		<!-- Note the folder attribute: This attribute describes the folder
			to copy FROM in the package to install therefore files copied
			in this section are copied from /admin/ in the package -->
		<files folder="admin">
			<!-- Admin Main File Copy Section -->
			<filename>index.html</filename>
			<filename>config.xml</filename>
			<filename>helloworld.php</filename>
			<filename>controller.php</filename>
			<filename>access.xml</filename>
			<!-- SQL files section -->
			<folder>sql</folder>
			<!-- tables files section -->
			<folder>tables</folder>
			<!-- models files section -->
			<folder>models</folder>
			<!-- views files section -->
			<folder>views</folder>
			<!-- controllers files section -->
			<folder>controllers</folder>
			<!-- helpers files section -->
			<folder>helpers</folder>
		</files>
		<languages folder="admin/language">
			<language tag="en-GB">en-GB/en-GB.com_helloworld.ini</language>
			<language tag="en-GB">en-GB/en-GB.com_helloworld.sys.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.ini</language>
			<language tag="fr-FR">fr-FR/fr-FR.com_helloworld.sys.ini</language>
		</languages>
	</administration>

</extension>

Contributors[edit]