I am working on a personal project, building out some basic functionality to handle PHP components so they can be built in a similar way to components in React. I've encountered some strange behavior, where if I render a component inline using string interpolation, it appears before the place it is invoked in code. I am able to get the markup to display correctly by running each step as its own line of code, but I'd like to be able to include these components in strings. Why could this out-of-order rendering be happening? Any insight would be greatly appreciated.
I have tried to return the included components instead of echo them, but (maybe because of the double quotes used?) they do not render at all doing so. I have also concatenated the components together as strings. This does not work. If I write distinct lines of code and end with a semicolon after each one, I get the expected result.
This is what I would like to have working:
echo "
<section class='videoInfo'>
<h1>{$props[$video]->getProperty(VIDEO_PROP::TITLE)}</h1>
<div class='secondary'>
<span class='viewCount'>{$props[$video]->getProperty(VIDEO_PROP::VIEWS)} views</span>
{$loader::render(COMPONENT::OPINION_CONTROLS)}
{$loader::render(COMPONENT::VIDEO_DETAILS)}
</div>
</section>
";
I have also tried this, which results in the same mangled output as above:
echo "
<section class='videoInfo'>
<h1>{$props[$video]->getProperty(VIDEO_PROP::TITLE)}</h1>
<div class='secondary'>
<span class='viewCount'>{$props[$video]->getProperty(VIDEO_PROP::VIEWS)} views</span>" .
$loader::render(COMPONENT::OPINION_CONTROLS) .
$loader::render(COMPONENT::VIDEO_DETAILS) .
"</div>
</section>
";
This works correctly, but is more cumbersome to write:
echo "
<section class='videoInfo'>
<h1>{$props[$video]->getProperty(VIDEO_PROP::TITLE)}</h1>
<div class='secondary'>
<span class='viewCount'>{$props[$video]->getProperty(VIDEO_PROP::VIEWS)} views</span>";
$loader::render(COMPONENT::OPINION_CONTROLS);
$loader::render(COMPONENT::VIDEO_DETAILS);
echo "</div>
</section>
";
I expect the output HTML to reflect the echoed string. So the final output should look like:
<section class="videoInfo">
<h1>Second Test</h1>
<div class="secondary">
<span class="viewCount">86 views</span>
<!-- includes javascript for handling like/dislike buttons -->
<script src="assets/js/opinions.js"></script>
<div class="controls">
<button class="opinion far fa-thumbs-up" onclick="like(this, 60)">
<span class="text">0</span>
</button>
<button class="opinion far fa-thumbs-down" onclick="dislike(this, 60)">
<span class="text">0</span>
</button>
</div>
<!-- includes javascript for handling edit/subscribe buttons -->
<script src="assets/js/videoInteractions.js"></script>
<section class="details">
<nav>
<a href="profile.php?username=testman">
<img src="assets/images/generic_profile.png" class="profilePic">
</a>
</nav>
<div class="uploadInfo">
<span class="uploader">
<a href="profile.php?username=testman">
testman
</a>
</span>
<span class="uploadDate">
Published on Jun 4, 2019
</span>
</div>
<button class="subscribe " onclick="subscribe("chan123", "testman", this)">
<span class="text">Subscribe 0</span>
</button>
<section class="description">
Testing view incrementing
</section>
</section>
</div>
</section>
This is indeed the output I get if I run each step as a distinct line of code. If I use string interpolation or concatenation, however, the components load out of order. The loaded components in fact appear before the videoInfo section altogether!
<button class="opinion far fa-thumbs-up" onclick="like(this, 60)">
<span class="text">0</span>
</button>
<button class="opinion far fa-thumbs-down" onclick="dislike(this, 60)">
<span class="text">0</span>
</button>
<nav>
<a href="profile.php?username=testman">
<img src="assets/images/generic_profile.png" class="profilePic">
</a>
</nav>
<div class="uploadInfo">
<span class="uploader">
<a href="profile.php?username=testman">
testman
</a>
</span>
<span class="uploadDate">
Published on Jun 4, 2019
</span>
</div>
<button class="subscribe " onclick="subscribe("chan123", "testman", this)">
<span class="text">Subscribe 0</span>
</button>
<section class="description">
Testing view incrementing
</section>
<h1>Second Test</h1>
<div class="secondary">
<span class="viewCount">87 views</span>
</div>
The output components are themselves echoed strings. They look like this:
(opinionControls)
echo "
<!-- includes javascript for handling like/dislike buttons -->
<script src='assets/js/opinions.js'></script>
<div class='controls'>
$likeButton
$dislikeButton
</div>
";
(videoDetails)
echo "
<!-- includes javascript for handling edit/subscribe buttons -->
<script src='assets/js/videoInteractions.js'></script>
<section class='details'>
<nav>
$profileButton
</nav>
<div class='uploadInfo'>
<span class='uploader'>
<a href='profile.php?username=$uploader'>
$uploader
</a>
</span>
<span class='uploadDate'>
Published on $formattedDate
</span>
</div>
$interactButton
<section class='description'>
{$props[$video]->getProperty(VIDEO_PROP::DESCRIPTION)}
</section>
</section>
";
Do nested echoes somehow bubble out of the top level echo? I am trying to wrap my head around this behavior.
I have tried to adjust the nested components by having them return strings instead of doing an echo, but I have had no luck getting them to render at all this way. I have tried single quotes and double quotes around the returned strings. Like so:
return '
<!-- includes javascript for handling like/dislike buttons -->
<script src=\'assets/js/opinions.js\'></script>
<div class=\'controls\'>
$likeButton
$dislikeButton
</div>
';
and
return "
<section class='videoInfo'>
<h1>{$props[$video]->getProperty(VIDEO_PROP::TITLE)}</h1>
<div class='secondary'>
<span class='viewCount'>{$props[$video]->getProperty(VIDEO_PROP::VIEWS)} views</span>
{$loader::render(COMPONENT::OPINION_CONTROLS)}
{$loader::render(COMPONENT::VIDEO_DETAILS)}
</div>
</section>
";
The $loader::render() function is fairly straightforward. It calls a function which invokes include() on a component that has been primed a little earlier in the application. The relevant ComponentLoader functions follow:
// Load component into static primedComponents array as a key, with its value its props
public static function primeComponentWithProps(string $includesPath, array $props)
{
self::$primedComponents[$includesPath] = $props;
}
// Extract component and props from static array, then include it on the page
public static function render(string $includesPath)
{
$props = self::$primedComponents[$includesPath];
$args = [$includesPath, $props];
self::loadComponentWithProps(...$args);
// remove the component from the array of primed components once it has been rendered
unset(self::$primedComponents[$includesPath]);
}
/** used to import components that require props */
public static function loadComponentWithProps(string $includesPath, array $props)
{
// extract($props); // using extract() would allow props to be accessed directly by key names--but currently we are using a props array and accessing values by key to make it clearer what is going on in components which consume props
include($includesPath);
}