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

Explore the Twig integration with Angular 2 #1

Open
manekinekko opened this issue Jan 25, 2016 · 13 comments
Open

Explore the Twig integration with Angular 2 #1

manekinekko opened this issue Jan 25, 2016 · 13 comments
Assignees

Comments

@manekinekko
Copy link
Owner

A rendered comment (like all rendered output in Drupal) can be heavily customized by themes. For example, comment.html.twig can be overridden with entirely different markup. In this project, we would like to explore two different architectural approaches:

  1. What the code looks like when the custom Twig templates are manually re-implemented in the framework's preferred template language (Handlebars, Angular templates, JSX, etc.). Although this would mean duplication of the same markup in Twig and the framework's template language, we are hoping that it would not additionally require duplication of non-trivial JS logic (especially for the more JS-centric template languages like JSX and ElmHtml) across custom themes for the case where each custom theme only needs to customize the markup and not the logic.
  2. Whether it's feasible and what the effort would be to make the framework use the existing Twig templates. For example, can Twig.js be modified to compile the Twig templates to what Ember normally compiles Handlebar templates to, etc. We suspect this is more feasible for Ember and Angular than for React and Elm, but are curious to learn if that's right or wrong.

Dedicated Drupal thread https://github.com/acquia/js-exploration/issues/4

@manekinekko manekinekko self-assigned this Jan 25, 2016
@manekinekko
Copy link
Owner Author

1. Twig template to Angular 2 template.

Twig template

{%
  set classes = [
    'field',
    'field--name-' ~ field_name|clean_class,
    'field--type-' ~ field_type|clean_class,
    'field--label-' ~ label_display,
    'comment-wrapper',
  ]
%}
{%
  set title_classes = [
    'title',
    label_display == 'visually_hidden' ? 'visually-hidden',
  ]
%}

<section{{ attributes.addClass(classes) }}>
  {% if comments and not label_hidden %}
    {{ title_prefix }}
    <h2{{ title_attributes.addClass(title_classes) }}>{{ label }}</h2>
    {{ title_suffix }}
  {% endif %}

  {{ comments }}

  {% if comment_form %}
    <h2 class="title comment-form__title">{{ 'Add new comment'|t }}</h2>
    {{ comment_form }}
  {% endif %}

</section>

Angular 2 template

<section [class]="classes">

  <div *ngIf="comments && !label_hidden">
    {{ title_prefix }}
    <h2 [class]="title_class" >{{ label }}</h2>
    {{ title_suffix }}
  </div>

  <!-- "comments" would be a separate component -->
  <angular2-comment-list [comments]="comments"></angular2-comment-list>

  <div *ngIf="comment_form">
    <h2 class="title comment-form__title">{{ 'Add new comment'| t }}</h2>

    <!-- "form" would be a separate component -->
    <angular2-comment-form></angular2-comment-form>

  </div>

</section>

Twig variables classes and title_classes would typically be set inside a component Class (in the JavaScript world).

@manekinekko
Copy link
Owner Author

2. Twig as a template engine in Angular 2

A quick and dirty solution would be...

  1. include the Twig.js library :
<script src="/core/assets/vendor/angular2/twig-0.8.7.js"></script>
<script src="/core/assets/vendor/angular2/twig-custom.js"></script>

We also include a twig-custom.js which contains the reimplementation of some drupal custom filters such as clean_class.

  1. update this portion of code:
    https://github.com/angular/angular/blob/36a423fac8866e8a849cf3ab1c4a15d08b0ccb8c/modules/angular2/src/compiler/template_normalizer.ts#L42-L43

with something like:

//...
var _isTwig = template.templateUrl.endsWith('.twig');
if(_isTwig) {
  templateContent = twig({
    data: templateContent
  }).render(new directiveType.runtime());
}
//...

This code simply checks if the current loaded template is a Twig file (from the extension), and runs the twig API to compile the template using the directive/component context.

  1. use an Angular 2 component as usual:
@Component({
    selector: 'angular2-comment-field', 
    templateUrl: '/core/themes/classy/templates/field/field--comment.html.twig',
    // ...
})
class DrupalCommentField {

    // Twig template variables 
    private comments: string = 'comments';
    private label_hidden: string = 'label_hidden';
    private field_name: string = 'field_name_1';
    private field_type: string = 'field_type_1';
    private label_display: string = 'label_display_1';

    // Drupal Template Function: attributes.addClass()
    private attributes = {
        addClass: (args) => {
            return ` class="${ args.join(' ') }" `;
        }
    }
    // Drupal Template Function: title_attributes.addClass()
    private title_attributes = {
        addClass: (args) => {
            return ` class="${args.join(' ')}" `;
        }   
    }

    constructor() {}
}

This is the default (unmodified!) Twig template:

<section{{ attributes.addClass(classes) }}>
  {% if comments and not label_hidden %}
    {{ title_prefix }}
    <h2{{ title_attributes.addClass(title_classes) }}>{{ label }}</h2>
    {{ title_suffix }}
  {% endif %}

  {{ comments }} 

  {% if comment_form %}
    <h2 class="title comment-form__title">{{ 'Add new comment'|t }}</h2>
    {{ comment_form }} 
  {% endif %}

</section>

DONE!

With these 3 steps, we were able to load a .twig template, compile it using the Twig.js library and pass basic variable to the template.

See this commit for more code b22aacb

Notes:

  1. Some variables such as comments and comment_form are the rendered HTML code representing the DOM structure. We need to figure out how we can replace those kind of variables with actual Angular 2 components, ideally without touching the template!. Otherwise, we'll have to substitute those variables {{ comments }} with some CSS selectors like <comments></comments>.
  2. a) We need a better solution for Drupal function used in Twig template, attributes.addClass() for instance.
  3. b) We need to re-implement Drupal functions and filters used in Twig.
  4. Angular 2 comes with a built-in HTML parser. We need to be able to plug in custom parsers, like the Twig one.
  5. Twig.js does not seem to understand label_display == 'visually_hidden' ? 'visually-hidden'. It needs to be a ternary expression: label_display == 'visually_hidden' ? 'visually-hidden' : ''

@mhevery
Copy link
Collaborator

mhevery commented Jan 26, 2016

Angular 2 comes with a built-in HTML parser. We need to be able to plug in custom parsers, like the Twig one.

That should not be an issue. What is not clear in my head is if the twig and angular templates could be made to be semantically equivalent.

We need to re-implement Drupal functions and filters used in Twig.

should be easy.

Some variables such as comments and comment_form are the rendered HTML code representing the DOM structure. We need to figure out how we can replace those kind of variables with actual Angular 2 components, ideally without touching the template!. Otherwise, we'll have to substitute those variables {{ comments }} with some CSS selectors like <comments></comments>.

In angular you can insert dom like this: <div [innerHTML]="comments"></div> We need to migrate the sanitization work from NG1 to NG2.

@wimleers
Copy link
Collaborator

Some variables such as comments and comment_form are the rendered HTML code representing the DOM structure. We need to figure out how we can replace those kind of variables with actual Angular 2 components, ideally without touching the template!.

This is the scariest/most complex part. This is where Drupal's "Render API" enters the picture.

The Render API allows you to write "render arrays", which are nested associative arrays (PHP lingo for the "map" or "dictionary" data structure in CS) that allow developers to declaratively indicate which HTML should be rendered. Why?

  • Because render arrays can then be modified/extended ("altered") by other Drupal modules, which is how Drupal achieves a big portion of its flexibility/extensibility.
  • Because many things in Drupal allow "the site builder" (a non-technical persona building a complex application solely by installing modules and configuring things via the UI) to configure things. Imagine a "blog post" content type, site A may configure the hero image to come before the intro text, site B may configure it the opposite way. This cannot be easily expressed in templates. Such configurable portions are therefore expressed in render arrays, not in templates. Within the render arrays, templates may still be used (e.g. for rendering a "hero image" field and a "intro text" field), but they are assembled into one whole ("blog post") using a render array.

A bit more high-level context about our Render API which I think will help you relate to it more easily:

  • There are many "render elements" (roughly the equivalent of Angular components), you can find them by grepping for @RenderElement in Drupal's code base.
  • Many render elements allow for nesting. This means you can compose complex HTML by using predefined render elements.
  • It's possible to customize either all instances or a specific instance of a render element.

I'm very curious to see how you're going to tackle this!

@manekinekko
Copy link
Owner Author

In angular you can insert dom like this: <div [innerHTML]="comments"></div> We need to migrate the sanitization work from NG1 to NG2.

@mhevery aren't we going to loose control over those components if we just insert their DOM?

This is the scariest/most complex part. This is where Drupal's "Render API" enters the picture.

@wimleers Thanks for all the details. This is gonna be useful when digging inside Drupal world which is going to be my next step.

@wimleers
Copy link
Collaborator

@manekinekko Let me know whenever you're stuck on something, whether it's code or understanding Drupal stuff. Happy to jump on a call/hangout too.

@mhevery
Copy link
Collaborator

mhevery commented Feb 1, 2016

In angular you can insert dom like this: <div [innerHTML]="comments"></div> We need to migrate the sanitization work from NG1 to NG2.
@mhevery aren't we going to loose control over those components if we just insert their DOM?

Yes, innerHTML will not be allowed to compile the directives. Use DynamicComponentLoader for that instead.

@manekinekko
Copy link
Owner Author

@mhevery In order to use the DynamicComponentLoader we need to have a defined component. However, Drupal uses render arrays to compose complexe HTML. So some inner Twig variables are compiled and rendered dynamically, I don't think they have their own Twig template. Am I right @wimleers

For instance (see diagram below), I am able to use the comment.html.twig template inside the Angular2 component CommentsBlock which renders fine. But the {{ user_picture }} Twig variable gets substituted with a dynamically rendered HTML. So in order to Have a UserPicture Angular2 component, and compose complex UI with Angular, we need that template! I am now trying to figure out how we could get this done.

image

@mhevery
Copy link
Collaborator

mhevery commented Feb 2, 2016

@mhevery In order to use the DynamicComponentLoader we need to have a defined component.

that is correct. Just make a component on the fly.

var Component = ng.Component({template: someHtml}).Class({constructor: function() {{});

NOTE: Selector can not have spaces or > in it.

@manekinekko
Copy link
Owner Author

@mhevery I am using this selector #block-baked-content > article > div.node__content > section and It seems to works fine, in beta.1

@mhevery
Copy link
Collaborator

mhevery commented Feb 2, 2016

@tbosch We should throw an error. I am not sure what exactly happens, perhaps we match on section?

@manekinekko
Copy link
Owner Author

I added Tobias to this private repo.

@AntonEmery
Copy link

I have been trying to incorporate Twig into my Angular 2 CLI project, using @angular/cli": "1.0.0-rc.2 I came across @manekinekko project https://github.com/manekinekko/angular2-twig but have not had any luck incorporating it into my CLI project. Has anyone experimented with that?

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

No branches or pull requests

4 participants