Jump to content
marco

Template Twig Replace

Recommended Posts

Hi folks,

as announced yesterday, I just published my new Template Twig Replace module on github.

The module lets you write Twig templates directly, making calls like $page->twig('my_template.twig') in an otherwise empty php template unnecessary.

It also seamlessly integrates with my other module Template Data Providers that can be found here and here.

Be sure to consult the README.md for further information.

Please leave your comments, opinions, and suggestions here in this forum or at github and I'll try to answer as quick as possible.

Honorable mentions go to porl and his Template Twig module for inspiring this little piece of code.

Regards,

Marco

  • Like 4

Share this post


Link to post
Share on other sites

Hi,

I just released the new minor version 1.0.4 of the TemplateTwigReplace module.

The module now lets you access the Twig_Environment instance used for rendereing pages and chunks via a public getter. So now you may customize the twig environment (e.g. by adding custom filters) to better meet your needs.

Please take a look at the modified readme.

Regards,

Marco

  • Like 1

Share this post


Link to post
Share on other sites

Just updated the Twig library that is shipped with the module to the current release v1.15.0.

  • Like 1

Share this post


Link to post
Share on other sites

Marco, do you have any pointers on how best to use Twig as a tool within the normal PW templating system? I want to have some of my PW template files prepare data, specify a path to a Twig template file and then invoke Twig to insert the data and return the result. I don't want Twig to be the default way of rendering pages.

Thanks.

Edit: I guess my question is really whether your module can be used in that way or should I just make something much more basic.

Answer!  :) 

I answered my own question. Porl's TemplateTwig module turned out to be perfect for what I'm doing. I just made one small change to point it to a newer version of Twig than the one distributed with the module.

This is part of a larger MVC-ish scheme, part of which is just like Apesia's Template Delegate approach using PW's TemplateFile class. In templates/markup/layouts/ I have the TemplateFile PHP file ('whatever.php') which, depending on what needs to be done, can prepare variables and echo them out directly or pass them in an array to Twig which will look for a similarly named file ('whatever.php.twig') of twig markup. Just to make things easier, the system behind all this passes that 'twig' filename to the TemplateFile class file so all it has to do is prepare an array of data and then: echo $page->twig->render($this->twig, $arr);
 

  • Like 1

Share this post


Link to post
Share on other sites

Sorry, dumb/newbie question,

I can't find how to use this module, should I rename all the templates to .twig and put the logic (selectors, expressions) between <?php ?> tags?

Thanks in advance

Share this post


Link to post
Share on other sites

FYI, Twig syntax has support for conditional expressions, loops, etc. Look for clues in the TemplateTwig README.md file which also mentions using the Template Data Providers module to prepare data for the template.

Share this post


Link to post
Share on other sites

Hey SteveB, Hey Manaus,

as Marco can't answer currently I'll try to answer your questions as far as I can.

@SteveB the answer is: No. As you've mentioned you can't just use PHP in Template Twig Replace. It is designed to replace the whole rendering Process and expects the input to be twig compatible. You could use Twig filters to achieve PHP execution though.

The way we (Marco works at the same company I do) use Twig Replace & Data Providers is somewhat similar to the delegate approach. We use some basic "wrapper" Templates and extend them (using blocks) with template based code.

Here is an example we actually use in a blog page from an actual project:

Base Template (_base_template.twig in /templates) which is extended later on (every %block acts as a placeholder which can be filled with stuff from templates or chunks):

<!DOCTYPE html>
<!--[if lt IE 9 ]><html class="no-js lt-ie9" lang="de"><![endif]-->
<!--[if IE 9 ]><html class="no-js ie9" lang="de"><![endif]-->
<!--[if !(IE)]><!--><html class="no-js modern" lang="de"><!--<![endif]-->
<html lang="de" xmlns:fb="http://ogp.me/ns/fb#">
    <head>
        {% include 'includes/htmlhead.twig' %}
        {% block htmlhead %} {% endblock htmlhead %}
    </head>
    <body>
        <div class="wrapper">
            {% include 'includes/header.twig' %}
            {% block stage %}{% endblock stage %}
            <main class="main">
                {% block mainContent %}{% endblock mainContent %}
            </main>
            {% include 'includes/footer.twig' %}
        </div>
        {% include 'includes/javascript.twig' %}
        {% block beforebody %} {% endblock beforebody %}
    </body>
</html>

Blog Page (blog.twig in /templates) which displays an overview of the last articles:

{% extends '_base-template.twig' %}

{% block stage %}
    <div class="stage row">
        <img class="stage__background" src="{{ config.urls.root }}dummy-data/stage_default.jpg">
        <div class="stage__inner large-12 large-offset-2 columns">
            <h1 class="stage__headline stage__headline--default headline">xxx</h1>
        </div>
    </div>
{% endblock %}

{%  block mainContent %}
    <main class="article row" role="main">
        <div class="large-offset-2 large-10 columns">

            {% for article in articles %}
                {{ page.renderChunk('includes/blogpost_preview.twig',article,'blog') }}
            {% endfor %}

            {{ page.renderChunk('includes/pager.twig',articles) }}

        </div>
        {% include 'includes/sidebar.twig' %}
        

    </main>



{% endblock mainContent%}

The Coressponding "DataProvider"/Controller (BlogPage.php in /templates/dataproviders):

<?php

/**
 * Class definition of BlogPage
 *
 * @version 1.0.2
 * @copyright Copyright (c) 2013, neuwaerts GmbH
 * @filesource
 */

/**
 * Class BlogPage
 */
class BlogPage extends \nw\DataProviders\PageDataProvider {

    public function populate() {

        // load articles
        $today = date('Y-m-d');
        $this->articles = wire('pages')->find('template=blog_article, blog_articledate<=' . $today . ', sort=-date, limit=5');
        $pages = wire('pages');
        $this->categories = $pages->get('/blog/kategorien/')->children();
		$this->authors = $pages->get('/blog/autoren/')->children();
    }
}

And (just to make it complete and show some more twig stuff) here is the chunk for displaying the blog post previews that is included in the template. The chunk gets the article as a param and has it's own controller where you can place logic (i.e. to find related articles), too. 

<article class="article__preview clearfix">
	<div class="article__content">
		<header class="article__header clearfix">
			<h2 class="article__headline"><a href="{{article.url}}">{{ article.headline ?: article.title }}</a></h2>
			<div class="article__meta">
				{% for author in article.blog_author %}
					{% if loop.length > 1 %}
					<span class="meta__author"><img class="article__avatar" src="{{ author.blog_authorimage.size(20,20).url }}" alt="{{ author }}">{{ author.title }}</span>,
					{% endif %}
					{% if loop.last %}
						<span class="meta__author"><img class="article__avatar" src="{{ author.blog_authorimage.size(20,20).url }}" alt="{{ author }}">{{ author.title }}</span>
					{% endif %}
				{% endfor %}
					 | <time datetime="{{ article.blog_articledate|date('Y-d-m', config.timezone) }} ">{{ article.blog_articledate|date('d. F Y', config.timezone) }}</time>
			</div>
		</header>
		{% if includeTemplate == 'blog' %}
				{% if article.blog_keyvisualsize == 'small' %}
					{% set size = 280 %}
				{% else  %}
					{% set size = 755 %}
				{% endif %}
				{% if article.blog_articlekeyvisual %}<img src="{{article.blog_articlekeyvisual.width(size).url}}" alt="{{article.blog_articlekeyvisual.description}}" class="article__keyvisual {{article.blog_keyvisualsize}}">{% endif %}
				<div class="article__text">
					{% if article.body|length > 350 %}
						{{ article.body[:350] }}...
					{% else %}
							{{ article.body }}
					{% endif %}
				</div>
		{% endif %}
		<div class="article__category">
			{% for category in article.blog_category %}
				{% if loop.length > 1 %}
				{{ category.title }}, 
				{% endif %}
				{% if loop.last %}
						<a class="category__link" href="{{ config.urls.root }}blog/kategorien/{{category.name}}">{{ category.title }}</a>
				{% endif %}
			{% endfor %}
			<div class="article__read-more right">
				<a class="read-more" href="{{article.url}}">Artikel lesen</a>
			</div>
		</div>
	</div>
</article>

So we're basically splitting everything in includes (DRY), template specific stuff and Controllers. That's the way we believe is the "cleanest" and best maintainable. If you'd like to output some default stuff (i.e. headline and body) you're free to add it to the base template and just overwrite the block that they are in on demand.

@Manaus: You don't HAVE to rename everything into .twig - but you CAN. If you like to do this you'll have to set this explicitly in site/config.php: $config->templateExtension = 'twig';

Enclosing everything in a <?php is not required (in fact it will output this as plaintext: <?php).

  • Like 1

Share this post


Link to post
Share on other sites

I'm sure this is a great module, I haven't tested it but thanks for your work.

I have two concerns with the module's approach though:

1.

The Twig templating engine should be stand alone/a seperate module, which is not autoloaded. The TemplateTwigReplace module then should load the Engine and work with it, this way other modules can use the Twig Engine by loading the stand alone module as well without running into any issues.

2. warning: extensive use of the word "logic" =)

I think the whole purpose of Twig is to sperate presentation from logic, so it should only be used for simple logic. Replacing the whole template system with Twig makes it necessary to write all logic (e.g. selecting data, merging data et al) in Twig templates which make them almost equally ugly as php templates with much logic in them.

I think a better way would be to use regular php templates as controllers, have them do some heavy logic lifting and then pass data to Twig views which should only be concerned with outputting/formatting strings and iterate through loops and so on.

PS: oh, and thanks, I was having problems with passed PW data to Twig so I looked at your code and discovered Page::$issetHas = true;. I then found out that Ryan implemented that feature after @porl had the same problems while coding his Twig module, so thanks to Ryan too =)

  • Like 1

Share this post


Link to post
Share on other sites

Hey owzim,

thanks for your input.

1.) I'm not sure about if it's a good idea to not include twig with the plugin. As pw is not using any dependency/package manager like composer there would always be the need of updating the twig pw-module when twig itself is updated.

2.) The Plugin was actually developed as an addition to "TemplateDataproviders" which adresses exactly the issues you've mentioned. Look at the source code from my last post: All the "heavy lifting " is done within the controllers.

Share this post


Link to post
Share on other sites

1.) I'm not sure about if it's a good idea to not include twig with the plugin. As pw is not using any dependency/package manager like composer there would always be the need of updating the twig pw-module when twig itself is updated.

Good point, have not thought of that. There should be an extra flag for the module info to specifiy the versions of the required modules. That would imply that modules should be provided in different versions, handled by ModulesManger perhaps. I think I might open an extra thread for that in the feature requests forum. Still, don't you see issues when another module uses Twig too? The only way to avoid issues would be to namespace the Twig classes. but thats extra work after each Twig update, right?

2.) The Plugin was actually developed as an addition to "TemplateDataproviders" which adresses exactly the issues you've mentioned. Look at the source code from my last post: All the "heavy lifting " is done within the controllers.

I saw that a couple of minutes after my post and wanted to edit it today, you beat me to it. I'll look into that, looks promising.

Share this post


Link to post
Share on other sites

I am wondering, if the prependTemplateFile is supported here?

I try to play around with this module over the weekend but no luck. It seems that it does not recognize the prependTemplateFile.

Share this post


Link to post
Share on other sites

Hi peterfoeng,

twig has another concept of extending templates with predefined code. It's called "extend" and is documented here.

Share this post


Link to post
Share on other sites

Awesome module, much thanks for that. One question though, how would one accomplish the following in twig?

(I’m trying to exclude the current page from it’s array of siblings)

Vanilla PHP:

foreach($page->siblings('id!='.$page->id){

This is my twig code:

{%  for sibling in page.siblings('id!=page.id') %}

I understand that page.id will not be rendered as the page it’s id, since it’s a string etc. But what is the proper way to accomplish this? I’d need to do some string concatenation within the selector there. I know I can create a dataprovider to provide a proper wirearray with actual siblings, but it seems a bit overkill.

Thanks in advance!

Share this post


Link to post
Share on other sites

Ok, this works:

{%  for sibling in page.siblings('id!=' ~ page.id) %}

Share this post


Link to post
Share on other sites

Hi peterfoeng,

twig has another concept of extending templates with predefined code. It's called "extend" and is documented here.

For markup generation I do use extends. But in another case I needed more logic which does not generate markup and therefore I wanted to use  

$config->prependTemplateFile

and this does not work.

I want to redirect the user depending on his browser language setting "Accept-Language".

I didn't want to create an own module (beforeHook) containing only a few lines of code which have to differ from project to project.

I added some code in the module to make that work. Maybe this is not the best way to solve this issue. 

Any hints would be appreciated :)

Share this post


Link to post
Share on other sites

Hi Bea, 

thanks a lot. I did see that the rendering didn't support the 2.5 attach/prepend settings but didn't have the time to implement it.

I've merged your pull-request (and restored the indention to tabs ;)!!!). Furthermore I've added a new config setting that lets you exclude templates from beeing rendered with twig. This is needed if you're using FormBuilder (and i can think of some more use cases).

Share this post


Link to post
Share on other sites

tzz tabs   ;)

I really like the new config option, I build my own contact form because I could not implement Form Template Processor.  Could not get to run the following code using twig:

$form = $modules->get('FormTemplateProcessor'); 
$form->template = $templates->get('my_contact_form_template'); // required
$form->requiredFields = array('fullname', 'email');
$form->email = 'your@email.com'; // optional, sends form as email
$form->parent = $page; // optional, saves form as page
echo $form->render(); // draw form or process submitted form

Share this post


Link to post
Share on other sites
*shameless plug*
We're using TemplateDataProviders for exactly this kind of jobs (preparing data before it's rendered). Maybe you should try it out, too.
In your case I'd use output buffer and store it inside a variable to output it using twig later on.
 
dataproviders/ContactPage.php
<?php

class ContactPage extends \nw\DataProviders\PageDataProvider {

	public function populate() {
		$modules = wire('modules');
		$templates = wire('templates');

		$form = $modules->get('FormTemplateProcessor'); 
		$form->template = $templates->get('my_contact_form_template'); // required
		$form->requiredFields = array('fullname', 'email');
		$form->email = 'your@email.com'; // optional, sends form as email
		$form->parent = $page; // optional, saves form as page

		ob_start();
		echo $form->render(); // draw form or process submitted form
		$this->formMarkup = ob_get_contents();
		ob_end_clean();
	}

}

contact.twig

{{ formMarkup }}
 
 
Alternately you could add the Form Object to the templates namespace and render it directly using the method call within twig:
 
dataproviders/ContactPage.php
class ContactPage extends \nw\DataProviders\PageDataProvider {

	public function populate() {
		$modules = wire('modules');
		$templates = wire('templates');

		$form = $modules->get('FormTemplateProcessor'); 
		$form->template = $templates->get('my_contact_form_template'); // required
		$form->requiredFields = array('fullname', 'email');
		$form->email = 'your@email.com'; // optional, sends form as email
		$form->parent = $page; // optional, saves form as page

		$this->contactForm = $form;
	}

}
contact.twig
{{ contactForm.render() }}

Share this post


Link to post
Share on other sites

thanks, I will try this  ;)

Tried to update to version 1.0.7 and got the following error:

Notice: Undefined index: ignoredTemplates in /../site/modules/TemplateTwigReplace/TemplateTwigReplace.module on line 194

The problem is that I could not submit the settings and there is no entry for the new config optionignoredTemplates atm. The second error message occurs because the entry admin is not saved as well so I got the following message (it tries to render the admin template using twig):

paths->adminTemplates . 'controller.php');

I added a simple if-condition and everything works as expected.

Share this post


Link to post
Share on other sites

Why not write this:

ob_start();
echo $form->render(); // draw form or process submitted form
$this->formMarkup = ob_get_contents();
ob_end_clean();

Like this:

$this->formMarkup = $form->render();

?

Why generate output, create an output buffer, to then assign the buffer to a variable, if you can assign the form output directly to the var?

Share this post


Link to post
Share on other sites
 

Why not write this:

ob_start();
echo $form->render(); // draw form or process submitted form
$this->formMarkup = ob_get_contents();
ob_end_clean();

Like this:

$this->formMarkup = $form->render();

?

Why generate output, create an output buffer, to then assign the buffer to a variable, if you can assign the form output directly to the var?

 

Yeah - you're right. That's why I've added the second option.


thanks, I will try this  ;)

Tried to update to version 1.0.7 and got the following error:

Notice: Undefined index: ignoredTemplates in /../site/modules/TemplateTwigReplace/TemplateTwigReplace.module on line 194

The problem is that I could not submit the settings and there is no entry for the new config optionignoredTemplates atm. The second error message occurs because the entry admin is not saved as well so I got the following message (it tries to render the admin template using twig):

paths->adminTemplates . 'controller.php');

I added a simple if-condition and everything works as expected.

I'll look into this issue asap. It was working in my test setup. I must have missed something. 

Share this post


Link to post
Share on other sites

I've just pushed a bugfix to github. The $ignoredTemplates variable wasn't populated when updating the module.

Share this post


Link to post
Share on other sites

Join the conversation

You can post now and register later. If you have an account, sign in now to post with your account.

Guest
Reply to this topic...

×   Pasted as rich text.   Paste as plain text instead

  Only 75 emoji are allowed.

×   Your link has been automatically embedded.   Display as a link instead

×   Your previous content has been restored.   Clear editor

×   You cannot paste images directly. Upload or insert images from URL.


  • Recently Browsing   0 members

    No registered users viewing this page.

  • Similar Content

    • By teppo
      This module tracks changes, additions, removals etc. of public (as in "not under admin") pages of your site. Like it's name says, it doesn't attempt to be a version control system or anything like that - just a log of what's happened.
      At the moment it's still a work in progress and will most likely be a victim of many ruthless this-won't-work-let's-try-that-instead cycles, but I believe I've nailed basic functionality well enough to post it here.. so, once again, I'll be happy to hear any comments you folks can provide
      https://modules.processwire.com/modules/process-changelog/
      https://github.com/teppokoivula/ProcessChangelog
      How does it work?
      Exactly like it's (sort of) predecessor, Process Changelog actually consists of two modules: Process Changelog and Process Changelog Hooks. Hooks module exists only to serve main module by hooking into various functions within Pages class, collecting data of performed operations, refining it and keeping up a log of events in it's own custom database table (process_changelog.) Visible part is managed by Process Changelog, which provides users a (relatively) pretty view of the contents of said log table.
      How do you use it?
      When installed this module adds new page called Changelog under Admin > Setup which provides you with a table view of collected data and basic filtering tools See attached screenshots to get a general idea about what that page should look like after a while.
      For detailed installation instructions etc. see README.md.
       


    • By Gadgetto
      Status update links (inside this thread) for SnipWire development will be always posted here:
      2019-08-08
      2019-06-15
      2019-06-02
      2019-05-25
      If you are interested, you can test the current state of development:
      https://github.com/gadgetto/SnipWire
      Please note that the software is not yet intended for use in a production system (alpha version).
      If you like, you can also submit feature requests and suggestions for improvement. I also accept pull requests.
      ---- INITIAL POST FROM 2019-05-25 ----
      I wanted to let you know that I am currently working on a new ProcessWire module that fully integrates the Snipcart Shopping Cart System into ProcessWire. (this is a customer project, so I had to postpone the development of my other module GroupMailer).
      The new module SnipWire offers full integration of the Snipcart Shopping Cart System into ProcessWire.
      Here are some highlights:
      simple setup with (optional) pre-installed templates, product fields, sample products (quasi a complete shop system to get started immediately) store dashboard with all data from the snipcart system (no change to the snipcart dashboard itself required) Integrated REST API for controlling and querying snipcart data webhooks to trigger events from Snipcart (new order, new customer, etc.) multi currency support self-defined/configurable tax rates etc. Development is already well advanced and I plan to release the module in the next 2-3 months.
      I'm not sure yet if this will be a "Pro" module or if it will be made available for free.
      I would be grateful for suggestions and hints!
      (please have a look at the screenshots to get an idea what I'm talking about)
       




    • By apeisa
      Update 31.7.2019: AdminBar is now maintained by @teppo. Modules directory entry has been updated, as well as the "grab the code" link below.
      ***
      Latest screencast: http://www.screencas...73-ab3ba1fea30c
      Grab the code: https://github.com/teppokoivula/AdminBar
      ***
      I put this Adminbar thingy (from here: http://processwire.c...topic,50.0.html) to modules section and to it's own topic.
      I recorded quick and messy screencast (really, my first screencast ever) to show what I have made so far. You can see it from here: http://www.screencas...18-1bc0d49841b4
      When the modal goes off, I click on the "dark side". I make it so fast on screencast, so it might seem a little bit confusing. Current way is, that you can edit, go back to see the site (without saving anything), continue editing and save. After that you still have the edit window, but if you click "dark side" after saving, then the whole page will be reloaded and you see new edits live.
      I am not sure if that is best way: there are some strengths in this thinking, but it is probably better that after saving there shouldn't be a possibility to continue editing. It might confuse because then if you make edits, click on dark side -> *page refresh* -> You lose your edits.
      ***
      When I get my "starting module" from Ryan, I will turn this into real module. Now I had to make some little tweaks to ProcessPageEdit.module (to keep modal after form submits). These probably won't hurt anything:
      if($this->redirectUrl) $this->session->redirect($this->redirectUrl); if(!empty($_GET['modal'])) $this->session->redirect("./?id={$this->page->id}&modal=true"); // NEW LINE else $this->session->redirect("./?id={$this->page->id}");   and...
      if(!empty($_GET['modal'])) { $form->attr('action', './?id=' . $this->id . '&modal=true'); } else { $form->attr('action', './?id=' . $this->id); // OLD LINE }  
    • By Mike Rockett
      Jumplinks for ProcessWire
      Release: 1.5.54
      Composer: rockett/jumplinks
      Jumplinks is an enhanced version of the original ProcessRedirects by Antti Peisa.
      The Process module manages your permanent and temporary redirects (we'll call these "jumplinks" from now on, unless in reference to redirects from another module), useful for when you're migrating over to ProcessWire from another system/platform. Each jumplink supports wildcards, shortening the time needed to create them.
      Unlike similar modules for other platforms, wildcards in Jumplinks are much easier to work with, as Regular Expressions are not fully exposed. Instead, parameters wrapped in curly braces are used - these are described in the documentation.
      Under Development: 2.0, to be powered by FastRoute
      As of version 1.5.0, Jumplinks requires at least ProcessWire 2.6.1 to run.
      View on GitLab
      Download via the Modules Directory
      Read the docs
      Features
      The most prominent features include:
      Basic jumplinks (from one fixed route to another) Parameter-based wildcards with "Smart" equivalents Mapping Collections (for converting ID-based routes to their named-equivalents without the need to create multiple jumplinks) Destination Selectors (for finding and redirecting to pages containing legacy location information) Timed Activation (activate and/or deactivate jumplinks at specific times) 404-Monitor (for creating jumplinks based on 404 hits) Additionally, the following features may come in handy:
      Stale jumplink management Legacy domain support for slow migrations An importer (from CSV or ProcessRedirects) Feedback & Feature Requests
      I’d love to know what you think of this module. Please provide some feedback on the module as a whole, or even regarding smaller things that make it whole. Also, please feel free to submit feature requests and their use-cases.
      Note: Features requested so far have been added to the to-do list, and will be added to 2.0, and not the current dev/master branches.
      Open Source

      Jumplinks is an open-source project, and is free to use. In fact, Jumplinks will always be open-source, and will always remain free to use. Forever. If you would like to support the development of Jumplinks, please consider making a small donation via PayPal.
      Enjoy! :)
×
×
  • Create New...