manekinekko / angular2-drupal

4 stars 1 forks source link

Explore the Twig integration with Angular 2 #1

Open manekinekko opened 8 years ago

manekinekko commented 8 years ago

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 commented 8 years ago

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 commented 8 years ago

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.

2) 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.

3) 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 https://github.com/manekinekko/angular2-drupal/commit/b22aacbe79b52cc5bea6225dca5b7679abaf3d57

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 commented 8 years ago

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 commented 8 years ago

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?

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


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

manekinekko commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

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 commented 8 years ago

@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 commented 8 years ago

@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 commented 8 years ago

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

mhevery commented 8 years ago

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

manekinekko commented 8 years ago

I added Tobias to this private repo.

AntonEmery commented 7 years ago

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?