Skip to content

[WIP] Added initial support for processing custom components in templates #30

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

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/HtmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

use DOMDocument;
use DOMElement;
use DOMXPath;
use Exception;
use LibXMLError;

Expand All @@ -14,6 +15,12 @@
*/
class HtmlParser {

private array $components;

public function __construct( array $components = [] ) {
$this->components = $components;
}

/**
* Parse the given HTML string into a DOM document.
*/
Expand Down Expand Up @@ -46,9 +53,24 @@ public function parseHtml( string $html ): DOMDocument {
//TODO Throw an exception
}

$this->substituteComponents( $document );
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think doing component substitution in HtmlParser will run into issues once we want to pass props into them… I was imagining it would take place in Component::handleNode(), probably just before the iteration over the child nodes. (And it should probably throw an “unsupported” exception if there are child nodes, at least until we get around to supporting slots…)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah - that makes sense. I'm not attached to this approach - this was just a sketch to see what would be possible / how complicated support might turn out to be.


return $document;
}

private function substituteComponents( DOMDocument $document ) {
$xpath = new DOMXPath( $document );
foreach ( $this->components as $componentName => $template ) {
$entries = $xpath->query( '//' . $componentName );
foreach ( $entries as $entry ) {
$templateRoot = $this->getBodyElement( $this->parseHtml( $template ) )->firstChild;
$templateNode = $document->importNode( $templateRoot, true );
$entry->parentNode->append( $templateNode );
$entry->remove();
}
}
}

/**
* Get the root node of the template represented by the given document.
*/
Expand Down
8 changes: 7 additions & 1 deletion src/Templating.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@

class Templating {

private array $components;

public function __construct( array $componentTemplates = [] ) {
$this->components = $componentTemplates;
}

/**
* @param string $template
* @param array $data
Expand All @@ -12,7 +18,7 @@ class Templating {
* @return string
*/
public function render( $template, array $data, array $methods = [] ) {
$htmlParser = new HtmlParser();
$htmlParser = new HtmlParser( $this->components );
$document = $htmlParser->parseHtml( $template );
$rootNode = $htmlParser->getRootNode( $document );
$component = new Component( $rootNode, $methods );
Expand Down
34 changes: 32 additions & 2 deletions tests/integration/FixtureTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ class FixtureTest extends TestCase {
* @dataProvider provideFixtures
*/
public function testPhpRenderingEqualsVueJsRendering( $template, array $data, $expectedResult ) {
$templating = new Templating();
$components = $this->loadFixtureComponents();
$templating = new Templating( $components );
$methods = [
'message' => 'strval',
'directionality' => function () {
Expand All @@ -31,14 +32,39 @@ public function testPhpRenderingEqualsVueJsRendering( $template, array $data, $e
$this->assertEqualHtml( $expectedResult, $result );
}

public function loadFixtureComponents() {
$componentDir = __DIR__ . '/fixture/components';

$components = [];
/** @var DirectoryIterator $fileInfo */
foreach ( new DirectoryIterator( $componentDir ) as $fileInfo ) {
if ( $fileInfo->isDot() || $fileInfo->isDir() ) {
continue;
}

$document = new DOMDocument();
// Ignore all warnings issued by DOMDocument when parsing
// as soon as VueJs template is not actually a "valid" HTML
/** @noinspection UsageOfSilenceOperatorInspection */
// @codingStandardsIgnoreLine
@$document->loadHTMLFile( $fileInfo->getPathname() );

$componentName = $this->getAttribute( $document, 'template', 'component-name' );
$template = $this->getContents( $document, 'template' );

$components[$componentName] = $template;
}
return $components;
}

public function provideFixtures() {
$fixtureDir = __DIR__ . '/fixture';

$cases = [];

/** @var DirectoryIterator $fileInfo */
foreach ( new DirectoryIterator( $fixtureDir ) as $fileInfo ) {
if ( $fileInfo->isDot() ) {
if ( $fileInfo->isDot() || $fileInfo->isDir() ) {
continue;
}

Expand Down Expand Up @@ -67,6 +93,10 @@ public function provideFixtures() {
return $cases;
}

private function getAttribute( DOMDocument $document, $elementId, $attributeId ) {
return $document->getElementById( $elementId )->getAttribute( $attributeId );
}

/**
* @param DOMDocument $document
* @param string $elementId
Expand Down
15 changes: 15 additions & 0 deletions tests/integration/fixture/components/subcomponent.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<template id="template" component-name="subcomponent">
<div><p>Subcomponent!</p></div>
</template>

<script>
const { defineComponent } = require( 'vue' );

// @vue/component
module.exports = exports = defineComponent( {
name: 'SubComponent',
setup() {
return { };
}
} );
</script>
14 changes: 14 additions & 0 deletions tests/integration/fixture/spc_test.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<template id="template">
<div>
<p>Surrounding HTML</p>
<subcomponent>
</subcomponent></div>
</template>

<script id="data" type="application/json">
{"condition":true , "link":"<a href=\"URL\">link</a>"}
</script>
<div id="result">
<!-- generated by `npm run-script populate-fixtures` -->
<div><p>Surrounding HTML</p><div><p>Subcomponent!</p></div></div>
</div>
48 changes: 44 additions & 4 deletions tests/integration/populate_fixtures_with_expected_results.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ const { renderToString } = require( 'vue/server-renderer' );
const fs = require( 'fs' );

const fixtureDir = __dirname + '/fixture';
const componentDir = fixtureDir + '/components';
const generatedComment = '<!-- generated by `npm run-script populate-fixtures` -->';

const defaultMethods = {
Expand All @@ -13,6 +14,20 @@ const defaultMethods = {
}
};

const components = {};

readComponentDir().then( function ( files ) {
files.forEach( function ( fileName ) {
var filePath = componentDir + '/' + fileName;
readFile( filePath )
.then( extractComponentTemplate )
.then( function ( componentData ) {
console.log( 'loaded component ' + fileName + ' : ' + componentData.componentName );
components[componentData.componentName] = componentData.template;
} )
} );
} );

readFixtureDir().then( function ( files ) {
files.forEach( function ( fileName ) {
var filePath = fixtureDir + '/' + fileName;
Expand All @@ -27,10 +42,24 @@ readFixtureDir().then( function ( files ) {
} );
} );

function getTemplateHtml( templateElement ) {
return cheerio.load( templateElement.html() ).html( { decodeEntities: false } ).trim();
}

function extractComponentTemplate( html ) {
const $ = cheerio.load( html );
const templateRoot = $( '#template' );

const componentName = templateRoot.attr( 'component-name' );
console.log("Found component with name " + componentName);
const template = getTemplateHtml( templateRoot );
return { componentName: componentName, template };
}

function extractDataFromFixture( html ) {
const $ = cheerio.load( html );

const template = cheerio.load( $( '#template' ).html() ).html( { decodeEntities: false } ).trim();
const template = getTemplateHtml( $( '#template' ) );
const data = JSON.parse( $( '#data' ).html() );

return { template: template, data: data };
Expand All @@ -42,6 +71,9 @@ function renderTemplate( fixtureData ) {
data: () => fixtureData.data,
methods: defaultMethods
} );
for ( [ componentName, template ] of Object.entries(components) ) {
app.component( componentName, { template } );
};

return renderToString( app );
}
Expand Down Expand Up @@ -80,18 +112,26 @@ function saveFile( filePath, contents ) {
} );
}

function readFixtureDir() {
function readDirFiles( dirPath ) {
return new Promise( function ( resolve, reject ) {
fs.readdir( fixtureDir, function ( err, items ) {
fs.readdir( dirPath, { withFileTypes: true }, function ( err, items ) {
if ( err ) {
reject( err )
} else {
resolve( items );
resolve( items.filter((item) => item.isFile()).map((item) => item.name) );
}
} );
} );
}

function readComponentDir() {
return readDirFiles( componentDir );
}

function readFixtureDir() {
return readDirFiles( fixtureDir );
}

function saveResultToFile(filePath, renderResult) {
readFile( filePath ).then( function ( html ) {
const $ = cheerio.load( html );
Expand Down
Loading