DrupalTwig / sandwich

Sandwich demo module.
27 stars 3 forks source link

Theming & Render Array Questions #2

Open Mark-L6n opened 9 years ago

Mark-L6n commented 9 years ago

Thanks for the talk/slides/demo module! I have a couple of questions:

1) On the theming, I'm not clear what gets passed to what. 3 files (~.module,~Controller.php, ~.twig.html) have similar variables (attributes, name, bread, cheese, veggies, protein, condiments). Do the elements in ~Controller.php call [#theme]_theme() in ~.module, with the rest of the top-level elements of the render array passed in as arguments? Is the array that is returned by [#theme]_theme() passed into ~.twig.html? Or: Is the array that is returned by [#theme]_theme() some sort of 'interface' template through which arguments are passed from the elements in ~Controller.php to ~.twig.html?

2) public function RendererInterface::render says:

Renderable arrays have two kinds of key/value pairs: properties and children. Properties have keys starting with '#' and their values influence how the array will be rendered. Children are all elements whose keys do not start with a '#'. Their values should be renderable arrays themselves, which will be rendered during the rendering of the parent array.

That doesn't seem to match the #attributes part of the example. Is it still unclear/fuzzy what begins with a # and what doesn't, as it was in D7?

$best_sandboxwich = [ '#theme' => 'sandboxwich', '#name' => $this->t('Chickado'), '#attributes' => [ 'id' => 'best-sandboxwich', 'style' => 'float: left;', 'class' => ['left', 'clearfix'], ], '#bread' => $this->t('Sourdough'), '#cheese' => $this->t('Gruyère'), '#veggies' => [ $this->t('Avocado'), $this->t('Red onion'), $this->t('Romaine'), ], '#protein' => $this->t('Chicken'), '#condiments' => [ $this->t('Mayo'), $this->t('Dijon'), ], ];

3) Since we need to create render arrays, it would be good to show the basics of how to create them and what are needed. (as in a Q I asked at Basic Composition of Render Arrays ).

joelpittet commented 9 years ago

1) @Mark-L6n thanks for the question, I think you are the first to ask any questions at all.

.module is where you define the hook_theme() which is how you tell the theme system what the template file is called and what variables it should expect.

Controller.php is where you build up the variables you will be sending into the template.

html.twig is where those variables get marked up for presentation.

In other systems it's like MVC where the hook_theme() is kind of like a ViewModel (ASP.net reference), a Model specifically designed for the View (the twig template file in our case). Not sure if that helps but it helps me conceptualize it.

2) Yes that is a bit confusing isn't it. '#' when building up a #theme renderable array is directly tied to the variables that get passed to the template. For example: if I make a renderable array like ['#theme' => 'sandwich', '#anything' => 'something']; my sandwich.html.twig will have a variable 'anything' that I can print out as {{ anything }}. I don't need to define it in the hook_theme, but when I do define a hook theme if I expect people to know it is expected or to provide a default I'd better put it in there or it's a bit mystery meat.

3) I think maybe Scott and I can try and weave that into our presentation so maybe we can help clarify for others. Honestly I still am confused why we have #type and #theme, but the answer is mostly historical baggage and we need to look at merging efforts in D9. We are quite aware that renderable arrays are confusing and do plan on replacing them with objects in the future to give them a better formal definition.

For D8 we were trying to be consistent and avoid early rendering of the renderable arrays so that they are easy to manipulate the data or properties, whereas in D7 sometimes by the time they got to the template they were already strings and not easily to change without fancy regular expressions. So now we can make changes in the template.

I'll look at also following up on the Issue Queue, thanks for posting that question there too.

Mark-L6n commented 9 years ago

Thanks for the quick and thorough answers! Also, it has really been useful to have this demo module to look at. 1) That's a good analogy with MVC. I guess I just needed to know that the variable names in all 3 need to match each other. Perhaps part of what confused me trying to figure this out is that it's not apparent why .module needs to define those variables (couldn't twig.html just accept the variables sent over by the Controller?) Good to know "that's just the way it's done". 2) Thanks, thinking of anything prefixed with # as being a variable (that can contain a value or an array) that will be passed to twig is easier to grasp than 'property' vs. 'child render array'. And it looks like the children of a #property don't also need a # prefix, is that correct? 3) Yes, that's too bad there wasn't enough time to convert render arrays into objects for D8. Objects seem perfect for this, since each #type could be an inherited class. As for right now, though, render arrays would be more easily usable if only they were defined and documented better. Back at that issue in the issue queue, I just posted an example of #type and #theme working together, even though in theory there should just be one of them, so their interaction doesn't seem clear-cut as of now.

joelpittet commented 9 years ago
  1. Twig can accept the variables you pass in they don't need to be defined, but it's more of a contract saying that the template should expect at least those variables. It's a way to tell someone using your template what to expect coming in, otherwise it would be quite the guessing game and lots of {{ dump(_context) }} to see what you are getting.
  2. The way I mentally look at it is the # are properties (if it was an object) and everything without the # is data, though they are quite often in D7 and D8 called children and the element_children() function of D7 will get those children. I believe you are correct but may depend on the value (like if that value accepts yet another render array the properties would need more # prefixes), #prefix ironically may be one of those cases:P.
  3. We have one object in D8 that is printable and that is #attributes (it's turned into an Attribute object right before passing it to the template if it isn't already one). It's a decent example of the direction we'd like to go with Renderable Objects. But check out what @c4rl is doing https://github.com/c4rl/renderapi-demo @LionsAd is correct in that you can override a #type's #theme property, but they all have default #theme properties to dictate how the variables will be rendered to a string. And yes I agree wholeheartedly, there should be only one, if #theme was #template, and $variables were always instead of this weird $variables['#element'] property from #type, things would make a bit more sense and easier to explain. Was probably a good idea at the time...
Mark-L6n commented 9 years ago

Hey Joel—for 1) above, in testing, it seems that Twig doesn't receive any variables if they weren't defined in the template in .module. For example, if you go into sandwich.module and comment out 'cheese' => '', (and clear cache), the 'cheese' won't appear in either the 'Chickado' or 'Vegan' sandwiches.

Mark-L6n commented 9 years ago

Running into issues with #theme when testing basic render array types. Does the name of the theme hook need to match the name of the module? Why asking: I've been adding a 2nd controller (adding info to sandwich.routing.yml), and having success with 3 of the basic ways to create a render array (predefined #type such as 'html_tag'; #type => markup; #type => inline_template). However, I can't get #theme to work. What I've additionally done, for a 'burger' theme:

star-szr commented 9 years ago

Hi @Mark-L6n! For the first question, when doing # keys in render arrays they do need to be defined AFAIK, I think what @joelpittet was referring to was the ability to add new variables at will in preprocess.

Regarding your second question, the following won't work, your hook_theme() (or any hook implementation) needs to match the name of your module:

added a burger_theme() function in sandwich.module

burger_theme() will never be called unless it's in burger.module and burger.module is installed.

Mark-L6n commented 9 years ago

Thanks @Cottser ! I didn't quite follow 'the ability to add new variables at will in preprocess'. Even if a preprocess function added more #properties to a render array, they couldn't get passed to a Twig template unless defined by the hook_theme() in .module, could they?

star-szr commented 9 years ago

Once you're in preprocess you're no longer dealing with the #properties of a render array, it all gets converted to an array of variables, so if you do sandwich_preprocess_sandwich(&$variables) or mytheme_preprocess_sandwich(&$variables) each # key of the render array (other than 'theme') will be a non-# key in the $variables array, along with some other default variables added by template_preprocess() (and any hook_preprocess() implementations).

This bit of code in \Drupal\Core\Theme\ThemeManager::theme() shows the # keys being converted to non-# keys:

      if (isset($info['variables'])) {
        foreach (array_keys($info['variables']) as $name) {
          if (isset($element["#$name"]) || array_key_exists("#$name", $element)) {
            $variables[$name] = $element["#$name"];
          }
        }
      }
Mark-L6n commented 9 years ago

So in this section of code, essentially the initial '#' of all keys is getting stripped, correct? And since pre-process functions are called later, you just used the stripped key-name instead of the #-prefixed-key-name, correct? (Aside: makes it appear that the use of '#' prefixes doesn't matter much, so that coders don't have to worry to much about whether or not to use a '#'; would that be accurate?).

joelpittet commented 9 years ago

When you get to preprocess the the $variables variable that get's passed in has no '#', correct, but if it's an array those values may be renderable arrays still and have the '#'.

The reason they are stripped before preprocess is because those get passed to the template as 'variables'. So $variables['foo'] = 'blah'; in preprocess, will be <?php print $foo ?> inside the template (in twig that is just {{ foo }}

Does that make sense?

Variable names can't start with '#'. {{ variables['#foo'] }} would be a bit inconvenient. Which is the case for $variables['baz'] = ['#markup' => 'test']; Because in the template in twig now you can do {{ baz }} OR dig in {{ baz['#markup'] }} to get the same result. Note that I had to use square brackets to get at that key with the '#' which is annoying but the best we have so far... but better than drupal 7 where it would have been <?php print render($baz); ?>

AlexSkrypnyk commented 9 years ago

Question about attributes: In the template example, the attributes is used for the wrapping <div>. What if i need to pass more attributes to other parts of the template, like say, sandwich name <h2>? Should I create another element like nameAttributes? If so, how would twig know that it is an attributes object and not a property?

star-szr commented 9 years ago

To do that you can sometimes get away with using title_attributes or content_attributes, those are the 2 other special cased attribute names. Although they need to be filled out in preprocess for that to work. Otherwise just set up your new attribute in preprocess:

$variables['my_attribute'] = new Attribute();

Make sure you use the Attribute class if it's not already used in your class/file.

joelpittet commented 9 years ago

@alexdesignworks It may be helpful to have a create_attributes() function in twig. You could even do this in contrib with your own Twig Extension or try to get it into core (but may not make it till 8.1.x).

Though if you are building up all attributes by hand you could just write them in HTML. It's when you pass them in dynamically that the Attribute Object is really handy.