7

I'm trying to do something very different for a SilverStripe site: on several subpages are tables of data, and these tables each have their own set of column headers, and some tables have more columns than others. I want to avoid building out tables in the Rich Text Editor as that is prone to a lot of mistakes and it's a hassle to maintain over time.

What I would like to do is create a DataObject that allows for a nth number of columns and an nth number of corresponding rows. This way I can call a loop (or possibly two) inside the template where I have the HTML table structure already in place. The content managers have full control over which columns are in the tables for any give subpage, and they don't have to worry about maintaining the HTML table setup.

I've had a couple of ideas that don't produce the results I want without a) making the UI experience too complex for content managers and b) not being able to properly link the columns with the rows.

I have thought of creating a DataObject for Table Headers and one for Table Rows, but then I'm stumped on how to combine them in such a way that would make sense, especially since there could be any number of columns.

Would anyone have any suggestions on to approach this?

UPDATE: Ok, I have something going for the TableRowItem data object that may work, and is close to working. However, the issue is this now: How do I save the field values to the database when I am creating them basically on the fly? As it is now, the only field that saves to the database is the PDF file upload field, everything else is erased upon hitting "create."

<?php

class TruckBodyPdfTableRowItem extends DataObject {

    private static $db = array(
    );

    // One-to-one relationship with gallery page
    private static $has_one = array(
        'TablePage'=> 'Page',
        'TableColumnSet' => 'TableColumnSet',
        'PDF' => 'File',
    );


    // tidy up the CMS by not showing these fields
    public function getCMSFields() {
        $fields = parent::getCMSFields();
        $fields->removeFieldFromTab("Root.Main","TablePageID");
        $fields->removeFieldFromTab("Root.Main","TableColumnSetID");
        $fields->removeFieldFromTab("Root.Main","SortOrder");
        $fields->addFieldsToTab("Root.Main", $this->getMyColumnOptions());

        return $fields;
    }
    public function getMyColumnOptions()
    {
        $columnArray = [];
        $Columns = DataObject::get('TableColumnSet');

        foreach($Columns as $Column){
          $columnArray[] = TextField::create($Column->TableColumnHeader);
        }

        return $columnArray;
    }

    // Tell the datagrid what fields to show in the table
    private static $summary_fields = array(
    );

    public function canEdit() {
        return true;
    }

    public function canDelete() {
        return true;
    }

    public function canCreate(){
        return true;
    }

    public function canPublish(){
        return true;
    }

    public function canView(){
        return true;
    }
}

But those are the tricky parts: Figuring out how to map values from one DataObject into labels for another, and then auto-generating an nth number of rows based on how many columns have been created.

<?php

class TablePage extends Page
{
    private static $db = array(
        'H1' => 'varchar(250)',
    );

    private static $has_many = array(
        'TableRowItems' => 'TableRowItem',
        'TableColumnSets' => 'TableColumnSet'
    );

    private static $has_one = array(

    );

    public function getCMSFields()
    {
        $fields = parent::getCMSFields();

        $fields->addFieldToTab("Root.Main", new TextField("H1"), "Content");

        $gridFieldConfig = GridFieldConfig_RecordEditor::create();

        $gridFieldConfig->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
            // field from drawer class => label in UI
            'TableColumnHeader' => 'Table Column Header'
        ));

        $gridfield = new GridField(
            "TableColumnSets",
            "Table Column Sets",
            $this->TableColumnSets(),
            $gridFieldConfig
        );

        $fields->addFieldToTab('Root.Specs Table', $gridfield);




        $gridFieldConfig2 = GridFieldConfig_RecordEditor::create();

        $gridFieldConfig2->getComponentByType('GridFieldDataColumns')->setDisplayFields(array(
            // field from drawer class => label in UI
            'TableRowValue' => 'Table Row Value'
        ));

        $gridfield2 = new GridField(
            "TableRowItems",
            "Table Row Items",
            $this->TableRowItems(),
            $gridFieldConfig2
        );
        $fields->addFieldToTab('Root.Specs Table', $gridfield2);



        return $fields;
    }
}

class TablePage_Controller extends Page_Controller
{
    private static $allowed_actions = array(

    );

    public function init()
    {
        parent::init();
        // You can include any CSS or JS required by your project here.
        // See: http://doc.silverstripe.org/framework/en/reference/requirements
    }
}

Here are the classes TableColumnSet and TableRowValue. I figured, there would be one set of column headers associated with an nth number of rows, so I figured there would be a $has_many relationship between the two classes, in that a TableColumnSet could have many TableRowValues, but there would only be one TableColumnSet for all the TableRowValues. I was hoping to associate the TableRowValues to the TableColumnSet values using a dropdown with all the column headers created but that just sounds like a bad idea. Having to manually associate every field in a row to the column headers seems tedious and potentially difficult content managers.

<?php

class TableColumnSet extends DataObject {

    private static $db = array(
        'SortOrder' => 'Int',
        'TableColumnHeader'=>'varchar(250)'
    );

    // One-to-one relationship with gallery page
    private static $has_one = array(
        'TablePage'=> 'Page'
    );

    private static $has_many = array(
        'TableRowItems' => 'TableRowItem'
    );

    // tidy up the CMS by not showing these fields
    public function getCMSFields() {
        $fields = parent::getCMSFields();
        $fields->removeFieldFromTab("Root.Main","TablePageID");

        $fields->removeFieldFromTab("Root.Main","SortOrder");

        return $fields;
    }

    // Tell the datagrid what fields to show in the table
    private static $summary_fields = array(
        'TableColumnHeader' => 'Table Column Header',
    );

    public function canEdit() {
        return true;
    }

    public function canDelete() {
        return true;
    }

    public function canCreate(){
        return true;
    }

    public function canPublish(){
        return true;
    }

    public function canView(){
        return true;
    }
}

I feel like may be on to something here, at least in regards to the relationship between the column headers and rows? I'm not sure, though.

halfer
  • 18,701
  • 13
  • 79
  • 158
Dejsa Cocan
  • 1,418
  • 2
  • 16
  • 45
  • Have you taken a look at [silverstripe/registry](https://github.com/silverstripe/silverstripe-registry)? It might help you here. – scrowler May 15 '17 at 20:58
  • That seems more like an admin model setup, and I really don't want to create multiple admin models for multiple subpages. I'd prefer to keep these tables on the page level, and not as admin model data. – Dejsa Cocan May 16 '17 at 14:48
  • 1
    I don't have the time to write a proper answer or even properly read your question right now. But I had this sort of requirement several times in the past and have come up with several different solutions. Such as postgres json storage, tables like those you have right now and, something that also works with MySQL, a DBField that uses json_encode: https://packagist.org/packages/zauberfisch/silverstripe-serialized-dataobject - This is used in my PageBuilder/Contentblock module. I might have more time the coming days, feel free to poke me in IRC and remind me to write a proper answer. – Zauberfisch May 17 '17 at 15:45
  • I've made something like that with just CSV in a textarea - customer wanted to be able to past CSV. In the end I liked this, because of its simplicity and CMS-editing is fast since you do not have to search, find, edit, save each row separately. – munomono May 17 '17 at 17:59
  • @munomono I think my concern with that approach, if I understand you correctly, is I would be replying on the client to understand proper CSV format, and I can't vouch for how tech-savvy they are. Plus, the data already exists on the client's current site in HTML tables -- I don't think there are any CSV for Excel files to reference. – Dejsa Cocan May 17 '17 at 18:04
  • @gallifrey1212 Excel e.g. can import a html table and export to csv. So if you provide a proper workflow for your client it might work. When you client knows to handle excel he can even take a current file and modify it for a new page. You might also be able to parse an excel file with phpexcel. So the client just needs to upload the file and you save it as json or csv or even rendered html in a hidden db field. There are many possibilities to solve this problem. – wmk May 18 '17 at 06:49
  • 1
    Hi gallifrey1212. Unfortunately this post cannot be left to stand like this, since an answer without a question is of no use to future readers. It also creates a great deal of work for volunteers, since my experience of asking posters to set it out in a useful fashion so it can be helpful in the future usually goes unheeded. Thus, for all of your future questions, please (a) don't add [solved] or [updated] title hacks; (b) add small updates to the end of the question only; (c) don't rewrite the question as you make progress; (d) post the solution as an answer; – halfer May 25 '17 at 20:06
  • 1
    (e) if your updates are getting too unwieldy or are substantially different from the original question, ask a new question. – halfer May 25 '17 at 20:06
  • 2
    Update: I've rolled back to the last state in which this post was still a question. If you could take the answer part [from the revision history](https://stackoverflow.com/posts/43980343/revisions) and post it as an answer, that would be great. – halfer May 25 '17 at 20:18

1 Answers1

-1

I might be off base here, since I have no experience with SilverStripe. But... my PHP / HTML table solution might apply here:

<?php

// parse your table data into this structure
$tableData = array(
    "rowOne" => array(
        "columnName" => "columnValue1",
        "colName" => "colValue1"
        // .....
    ),
    "rowTwo" => array(
        "columnName" => "columnValue2",
        "colName" => "colValue2"
        // .....
    )
);

// now loop through the array with a printHeader parameter
$tableHTML = array(
    "<table>"
);
$tableHead = array(
    "<thead>"
);
$tableBody = array(
    "<tbody>"
);
$printHeader = true;

foreach ($tableData as $row) {
    foreach ($row as $column => $value) {
        $tableRow = "<tr>";
        if ($printHeader) {
            $tableHead[] = "<th>".$column."</th>";
        }
        $tableRow .= "<td>".$value."</td>";
    }
    $tableBody[] = $tableRow."</tr>";
    // after the first row, set printHeader to false and close the <thead>
    $printHeader = false;
    $tableHead[] = "</thead>";
}

 // implode table header to string with linebreaks
$tableHead = implode(PHP_EOL, $tableHead);

// close table <tbody> & implode to string with linebreaks
$tableBody[] = "</tbody>";
$tableBody = implode(PHP_EOL, $tableBody);

// add all table elements together
$tableHTML[] = $tableHead;
$tableHTML[] = $tableBody;
$tableHTML[] = "</table>";
// implode table array to string
$tableHTML = implode(PHP_EOL, $tableHTML);

// print or write anywhere
echo($tableHTML);

?>

The array structure for all the steps in the loop are to keep the default server memory cleaner to remove old data. If you concat ($var .= "string";) everything as strings all the references will stay stored in memory and bog down the server when displaying large tables. I hope this is of some help

G4Hu
  • 198
  • 2
  • 16