Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Build an element in steps #4

Closed
MikeiLL opened this issue Dec 30, 2022 · 13 comments
Closed

Build an element in steps #4

MikeiLL opened this issue Dec 30, 2022 · 13 comments
Labels
documentation Improvements or additions to documentation

Comments

@MikeiLL
Copy link
Contributor

MikeiLL commented Dec 30, 2022

Hi. Discovering this through WPBP and it looks pretty sweet!

I'm wanting to build an element in steps and it looks like the Tag methods are the way to do that.

What I'm hoping you can tell me is how to add the Content, dynamically.

use DecodeLabs\Tagged as Html;

$anchor = Html::tag('a.timeOfDay-class');
if ($is_daytime){
    $tag->setDataAttribute('environment', 'sunlight');
} else {
    $tag->setDataAttribute('environment', 'starlight');
}
// Actually, how do I set the content now?
echo $anchor;
@betterthanclay
Copy link
Member

betterthanclay commented Dec 30, 2022

Hi Mike, glad you're finding our libraries and making use of them.

Ok, so what you're trying to do is essentially the bread and butter of the Tagged library.
There are 2 different ways it can be used, represented with 2 classes: Tag and Element.

A Tag exists to just represent the actual tag structure itself - name, attributes, etc, and doesn't hold content.
An Element extends Tag to add a content container to those structures.

So, where above you have instantiated a Tag with Html::tag(), the $anchor object doesn't really have any concept of inner content; the point of the raw Tag class existing at all is to allow you to write particularly custom spaghetti code in more convoluted scenarios, where you are echoing the content yourself:

$anchor = Html::tag('a.timeOfDay-class', [
    'attribute' => 'value'
]);

echo $anchor->open();
echo 'Some content from somewhere else';
echo $anchor->close();

The much more common approach is to use the Element class which wraps up all of the above. The Tagged frontage also has some really handy shortcuts to use it directly. You could achieve what you're aiming for above with the following:

echo Html::{'a.timeOfDay-class'}('My content', [
    'data-environment' => $is_daytime ? 'sunlight' : 'starlight
]);

Or, you could do it more programatically, depending on your code context:

$anchor = Html::a(null);
$anchor->addClass('timeOfDay-class');
$anchor->setBody('My content');

if ($is_daytime){
    $anchor->setDataAttribute('environment', 'sunlight');
} else {
    $anchor->setDataAttribute('environment', 'starlight');
}

echo $anchor;

Also note, that because the Element class is a Collection container, you can `append()', 'prepend()' to existing content (or use any of the other Sequence Collection methods)

Hope that helps, let me know if you need any more help.

@MikeiLL
Copy link
Contributor Author

MikeiLL commented Dec 30, 2022

Awesome. I wasn't familiar with Collections (and am still not quite there). I see that Element is an interface, but can you point me to where the setBody method of an Html instance is actually defined?

@betterthanclay
Copy link
Member

Hi Mike. The setBody method is defined in the ElementTrait trait in the Elemental package, here - https://github.com/decodelabs/elementary

Elemental defines a generic set of tag and element constructs, and Tagged is the HTML implementation of that interface. (Another package, Exemplar, also defines an XML implementation too)

ElementTrait defines a few helpers like setBody, etc, then the rest of the container controls are in the Collections package.

The Collections package contains a number of classes used for containing sets of things in different ways - you can think of a Sequence as a php array with only numerical indexes and the equivalent of the php array_* functions available on the object.

@MikeiLL
Copy link
Contributor Author

MikeiLL commented Jan 2, 2023

Still feeling a little slow on the uptake here, @betterthanclay and much appreciating your patience.

I want to build an HTML table from an array.

So I need something like this:

Outer array loop for headings: 
TABLE
    THEAD
      ROW
        TD
          "A heading"
        /TD
      /ROW
    /THEAD
  Inner array loop for data per column
    ROW
      TD
        "Some data"
      /TD
    /ROW
/TABLE

Does this looks like the approach the tool has in mind (so to speak):

$table = Html::{'table'}(null);
foreach ( $horizontal_schedule as $day => $classes ) :
	$thead = Html::{'thead'}( Html::{'tr'}(null) );
	$thead->append(Html::{'th'}( gmdate( get_option( 'date_format' ), strtotime( $day ) ) ));
	$thead->append(Html::{'th'}( 'Class Name' ));
	$table->append($thead);
	foreach ( $classes as $k => $class ) :
		$table->append(Html::{'tr'}(Html::{'td'}( $class->class_name )));
	endforeach;
endforeach;
$result->append($table);

@betterthanclay
Copy link
Member

betterthanclay commented Jan 3, 2023

Hi Mike.

I'm not completely sure what table structure you're trying to make here - are you trying to create multiple tables, one for each day in $horizontal_schedule, or is it supposed to be 1 table with a column for each day? The logic you're using in the code above doesn't make it entirely clear.

I'm going to take a guess that you're aiming for one table and this is roughly the structure you're looking for:

<table>
  <thead>
    <tr>
      <th>Day 1</th>
      <th>Day 2</th>
      <th>Day 3</th>
      <th>Day 4</th>
      ...
    </tr>
  <thead>
  <tbody>
    <tr>
      <td>Class 1 for day 1</td>
      <td>Class 1 for day 2</td>
      <td>Class 1 for day 3</td>
      <td>Class 1 for day 4</td>
      ...
    </tr>
    <tr>
      <td>Class 2 for day 1</td>
      <td>Class 2 for day 2</td>
      <td>Class 2 for day 3</td>
      <td>Class 2 for day 4</td>
      ...
    </tr>
    <tr>
      <td>Class 3 for day 1</td>
      <td>Class 3 for day 2</td>
      <td>Class 3 for day 3</td>
      <td>Class 3 for day 4</td>
      ...
    </tr>
  </tbody>
</table>

Which would look like this:

Day 1 Day 2 Day 3 Day 4
Class 1 for day 1 Class 1 for day 2 Class 1 for day 3 Class 1 for day 4
Class 2 for day 1 Class 2 for day 2 Class 2 for day 3 Class 2 for day 4
Class 3 for day 1 Class 3 for day 2 Class 3 for day 3 Class 3 for day 4

If that's the case, the code to generate it would ideally look like this.. it's a little more fiddly than usual because your data appears to be stored as columns not rows:

// Element body can be rendered in a generator closure using yield statements
echo Html::table(function() use($horizontal_schedule) {
    // Generate header
    // Use list macro to create elements (th) with a loop ($horizontal_schedule) in a container (nested, thead > tr)
    yield Html::list($horizontal_schedule, 'thead > tr', 'th', function($classes, $thElem, $day) {
        return  gmdate( get_option( 'date_format' ), strtotime( $day ) );
    });

    // Extract list of $classes keys to generate rows
    // Assumes all $classes lists are the same length
    $classes_keys = array_keys($horizontal_schedule[array_key_first($horizontal_schedule)]);

    // Generate body
    // Loop over keys (rows), create tbody container with child tr elements
    yield Html::list($classes_keys, 'tbody', 'tr', function($key) use($horizontal_schedule) {
        // Loop through days for each row
        foreach($horizontal_schedule as $day => $classes) {
            // Create cell for each class name
            yield Html::td($classes[$key]->class_name);
        }
    });
});

Alternatively, if you are just trying to create a single-column table for each day, then it's much simpler.. I'm just not sure how useful this actually is:

echo Html::elements($horizontal_schedule, 'table', function($classes, $table, $day) {
    yield Html::{'thead > tr th'}(gmdate( get_option( 'date_format' ), strtotime( $day ) ));

    yield Html::list($classes, 'tbody', 'tr > td', function($class) {
        return $class->class_name;
    });
});

Which creates:

Day 1
Class 1 for day 1
Class 2 for day 1
Class 3 for day 1
Day 2
Class 1 for day 2
Class 2 for day 2
Class 3 for day 2

etc..


Hope that helps - if you can let me know exactly what HTML you're trying to create, I can show you how to make it with Tagged and hopefully you'll get the hang of making your own structures from there.

@betterthanclay betterthanclay pinned this issue Jan 3, 2023
@MikeiLL
Copy link
Contributor Author

MikeiLL commented Jan 3, 2023

Really elegant and insightful, man. I love the utilization of generators.

The list method is challenging me a bit, and generally you are raising the bar in terms of my coding so thank you.

Perhaps I can contribute at least to the docs in some of this beautiful tool set.

I may want to include two HTML elements in each TH, which I see I can also do naively like this:

yield Html::list($horizontal_schedule, 'thead > tr', 'th', function($classes, $thElem, $day) {
    $timefromstring = strtotime($day);
    return Html::h3(gmdate( 'l', $timefromstring ))->append(Html::h5(gmdate( 'F jS', $timefromstring )));
});

Again, that feels like the hack and slay approach. What would a wizard do?

@betterthanclay
Copy link
Member

Hi Mike.

Ok, so for reference there are a handful of looping helper macros available:

list()

This lets you create a container element, loop over your iterable, and create child elements within the container:

Html::list($iterable, 'container-element', 'child-element', function($value, $childElement, $key) {
    // Generate content here
    yield Html::span(['Key: ', $key]);
    yield Html::span(['Value: ', $value]);
});

This would make something like:

<container-element>
  <child-element>
    <span>Key: 0</span>
    <span>Value: value 1</span>
  </child-element>
  <child-element>
    <span>Key: 1</span>
    <span>Value: value 2</span>
  </child-element>
  <child-element>
    <span>Key: 2</span>
    <span>Value: value 3</span>
  </child-element>
</container-element>

elements()

The same as list, but without the container:

Html::elements($iterable, 'child-element', function($value, $childElement, $key) {
    // Generate content here
    yield Html::span(['Key: ', $key]);
    yield Html::span(['Value: ', $value]);
});

Would make something like:

<child-element>
  <span>Key: 0</span>
  <span>Value: value 1</span>
</child-element>
<child-element>
  <span>Key: 1</span>
  <span>Value: value 2</span>
</child-element>
<child-element>
  <span>Key: 2</span>
  <span>Value: value 3</span>
</child-element>

uList() / oList()

Wrapper around list to create ul and ol elements:

Html::uList($iterable, function($value, $liElement, $key) {
    // Generate content here
    yield Html::span(['Key: ', $key]);
    yield Html::span(['Value: ', $value]);
});

Would make something like:

<ul>
  <li>
    <span>Key: 0</span>
    <span>Value: value 1</span>
  </li>
  <li>
    <span>Key: 1</span>
    <span>Value: value 2</span>
  </li>
  <li>
    <span>Key: 2</span>
    <span>Value: value 3</span>
  </li>
</ul>

In your example above, the append() call is adding the h5 into the h3 which you don't really want.
The neat solution would just be:

yield Html::list($horizontal_schedule, 'thead > tr', 'th', function($classes, $thElem, $day) {
    $timefromstring = strtotime($day);
    yield Html::h3(gmdate( 'l', $timefromstring ));
    yield Html::h5(gmdate( 'F jS', $timefromstring ));
});

However, we can also go one step further and use the date helper in Tagged like this:
(This should work, so long as $day represents some sort of string date value)

yield Html::list($horizontal_schedule, 'thead > tr', 'th', function($classes, $thElem, $day) {
    yield Html::h3(Html::$time->format('l', $day));
    yield Html::h5(Html::$time->format( 'F jS', $day));
});

@MikeiLL
Copy link
Contributor Author

MikeiLL commented Jan 4, 2023

yield Html::list($horizontal_schedule, 'thead > tr', 'th', function($classes, $thElem, $day) {
    $timefromstring = strtotime($day);
    yield Html::h3(gmdate( 'l', $timefromstring ));
    yield Html::h5(gmdate( 'F jS', $timefromstring ));
});

Cool! From Python (I now know, incorrectly) I had understood a yield to be like a return, where the second one wouldn't run.

FWIW, the ->append() approach does seem to be yielding consecutive elements:

return Html::h3( \gmdate( 'l', $timefromstring ) )->append( Html::h4( \gmdate( 'F jS', $timefromstring ) ) )

Producing

<th>
  <h3>Wednesday</h3>
  <h4>January 4th</h4>
</th>

@betterthanclay
Copy link
Member

betterthanclay commented Jan 5, 2023

Hi Mike.

Interesting - I'm not really sure how that can be the case as when you append, any items you pass it will go into the body collection which will always be rendered inside the tags. The closing tag is always the last thing to be rendered in any particular element. It may be the ->append() was actually the wrong side of some other parenthesis or something like that..

I've just tested to make sure:

echo Html::div(function () {
    return Html::h3('First')->append(Html::h4('Second'));
});

Results in the following (h4 inside h3):

<div>
  <h3>
    First<h4>Second</h4>
  </h3>
</div>

Anyway, for the most part, using append() isn't necessary unless you're doing something weird or awkward and you're almost always better off using generators and yield as it's by far the most memory efficient.

And yeah, it's worth wrapping your head around how generators generally work in PHP as they can be confusing unless you have the background to them. In the case of Tagged, it's set up so you can essentially use yield in the same way you would use echo, though the reality is that's not really what yield is conceptually intended for.. it just happened to fit this case nicely.

@betterthanclay betterthanclay added the documentation Improvements or additions to documentation label Jan 5, 2023
@MikeiLL
Copy link
Contributor Author

MikeiLL commented Jan 5, 2023

I bet the Chrome dev tools was "fixing" those nested heading tags for me. Should have checked the source HTML.

My (mentor and) colleague has a little lightweight JS library that works very similarly to Tagged: https://github.com/Rosuav/choc

Have a nice one, Tom.

@betterthanclay
Copy link
Member

Yeah, more than likely actually. Can make it a bit confusing in situations like this where you're not actually looking at what you're producing from the server.

Thanks for the link, I'll check out choc.
Let me know if you have any more issues / questions.

@MikeiLL
Copy link
Contributor Author

MikeiLL commented Jan 5, 2023

One thing I came across today that perhaps you would suggest an Html::list() solution for:

yield Html::{'select#location'}(function() {
	foreach(Engine\Credentials::$site_ids as $k => $locID){
		yield Html::option(Engine\Credentials::$site_names[$k], ['value' => $locID'']);
	}
});

Additionally, how do I add a keyless attribute like selected? The following isn't doing the trick:

$selected = ($locID == $_SESSION['region']) ? 'selected' : '';
yield Html::option(Engine\Credentials::$mbo_site_names[$k], ['value' => $locID, 'selected' => $selected]);

'Cause if I recall correctly, "selected" is the same as "selected=selected".

TY

@betterthanclay
Copy link
Member

Hi Mike.

Sure - this should do the job:

yield Html::list(Engine\Credentials::$site_ids, 'select#location', 'option', function($locID, $option, $k) {
    $option
        ->setAttribute('value', $locID)
        ->setAttribute('selected', $locID == $_SESSION['region']);

    return Engine\Credentials::$mbo_site_names[$k];
});

Keyless attributes are treated as boolean in HTML / JS, and Tagged does the same - use true or false to include or not include those attributes. In this case, the check against $_SESSION can be used directly.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
documentation Improvements or additions to documentation
Projects
None yet
Development

No branches or pull requests

2 participants