Vue to a Perl - Hacking Thy Fearful Symmetry

Hacking Thy Fearful Symmetry

Hacker, hacker coding bright

Vue to a Perl

created: November 18, 2017

So, this week I was working on adding relationalish features to DBIx-NoSQL-Store-Manager, ostenciably to add tagging functionality to my blog engine. Which is good, but now that I'll have tags, I'll also have to change the templates of the blog to incorporate those tags.

Right now I'm using Template::Caribou, a Moose-base template system written by your truly. And while I still like it, I'm also playing a lot with Vue theses days and really, really like the way it does its single component .vue files. So I began to think about server-side rendering.

But then I thought hmmm.... when I say server-side, I really mean that I don't care about the reactive magic of Vue, just its template style. So I wonder, yes, I wonder... how hard would it be to implement it in Perl?

Yes. That's right. I'm about show you how to implement a very rough, but working implementation of the Vue template style in Perl. Because that's exactly what the world needs. Another template module.

The Single Component File, à la Perl

So in Vue.js, a single-component file will look something like this.

<template>
    <div>
        <h1>{ title }<h1>
        <ul>
            <Item v-for="item in items" 
                v-if="item ne 'skip_me'" :label="item"  />
        </ul>
    </div>
</template>

<script>
    import Item from './Item';
    export default {
        components: { Item },
        props: [ 'title', 'items' ],
    }
</script>

For our Perl version, we'll use modules to be our components, and we will use Perl's natural instinct to ignore POD directives to put the template smack there at the top.

package Example::Main;

=begin template
<div>
    <h1>{{ title }}</h1>
    <ul>
        <Item 
            v-for="item in items" 
            v-if="item ne 'skip_me'" 
            :label="item"  
        />
    </ul>
</div>
=cut

use Moose;
with 'Template::Vue';

has '+components' => default => sub {[ 'Example::Item' ]};

has [qw/ title items /] => ( is => 'ro' );

1;

And to make the example interesting, we're adding a subcomponent. With it, we'll be exercising the import of sub-components, v-for iterations, v-if conditionals, {{interpolations}} and attribute :bindings.

package Example::Item;

=begin template
    <li v-if="label ne 'me_too'">{{ label }}</li>
=cut

use Moose;
with 'Template::Vue';

has label => ( is => 'ro' );

1;

From Single Component File to good ol' regular Perl

As seen in the code above, I'm already leveraging some Perl goodies. The template itself will be using Moose, and the templating voodoo will be encapsulated in the role Template::Vue. For now props are simply attributes of the template's object, and the sub-components are defined via a components attribute. I could have gone and began to DSL in new props and components keyword, but for an initial prototype, we can stay with (relatively) vanilla Moose.

But there is still the template itself that is not yet within our coding clutch. Well, that's hardly a problem.

First things first. We create the role.

package Template::Vue;

use Moose::Role;

use experimental 'signatures';

... then we add the components attribute, which defaults to be boring as all hecks.

has components => (
    is      => 'ro',
    default => sub { [] },
);

Then we add the template attribute.

To extract the template, we find where the module lives, and we munge the file like savages.

Eventually, we'll be more civilized and use a POD parsing module. But for now, it'll do.

use Module::Info;

has template => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        join '',
            grep { !/^=(begin|end|cut)/ }
            grep { 
                /^=begin\s+template\s*$/
                    ../^=(?:end\s+template|cut)\s*$/ 
            } 
        path(
            Module::Info
                ->new_from_module($self->meta->name)
                ->file 
        )->lines;
    },
);

From template to final output

With that, we have all the pieces we need. Next step: let's process that template.

If we look at it, the rendering of a Vue template has several steps. There is the interpolation of the mustache-like expressions in the template. There is the exclusing of tags that don't satisfy their v-if condition. There is the iteration for the tags with a v-for attribute. And there is the interpolation of sub-components.

Let's deal with them one by one.

Step 1: Mustache interpolation

So, yeah, Vue uses a syntax that is close to Mustache. Very close. So very muchly close that one could be tempted to use Template-Mustache....

Remember the template we already have?

use Module::Info;

has template => (
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        join '',
            grep { !/^=(begin|end|cut)/ }
            grep { 
                /^=begin\s+template\s*$/
                    ../^=(?:end\s+template|cut)\s*$/ 
            } 
        path(
            Module::Info
                ->new_from_module($self->meta->name)
                ->file 
        )->lines;
    },
);

Bam.

It's now Mustache-enabled, with its context taken from the object's attributes.

(NB: at the time of this writing, one itsy bitsy patch needs to be pushed to Template::Mustache::Trait so that it works in that example. I plan to release it within the week.)

use Module::Info;

has template => (
    traits  => [ 'Mustache' ],
    handles => { render_mustache => 'render' },
    is      => 'ro',
    lazy    => 1,
    default => sub($self) {
        join '',
            grep { !/^=(begin|end|cut)/ }
            grep { 
                /^=begin\s+template\s*$/
                    ../^=(?:end\s+template|cut)\s*$/ 
            } 
        path(
            Module::Info
                ->new_from_module($self->meta->name)
                ->file 
        )->lines;
    },
);

Step 2: v-if

Now we're beginning to manipulate the DOM of the template. For that, we'll use Web::Query, which is heavily inspired from jQuery.

use Web::Query::LibXML;

sub process_directives( $self, $doc, @context ) {
  @context = ( $self ) unless @context;

  $doc = Web::Query::LibXML->new( $doc, { indent => '  ' })
    unless ref $doc;

  $doc->find('.')->and_back->filter( '[v-if]' )->each( sub{
    my($variable,$rest) = split / /, $_->attr('v-if'), 2; 
    $_->attr('v-if' => undef);

    $_->remove unless eval qq{
      Template::Mustache::resolve_context( '$variable', \\\@context )
      $rest
    };
  });

  return $doc->as_html;
}

Basically, we find all nodes that have a v-if attribute...

use Web::Query::LibXML;

sub process_directives( $self, $doc, @context ) {
  @context = ( $self ) unless @context;

  $doc = Web::Query::LibXML->new( $doc, { indent => '  ' })
    unless ref $doc;

  $doc->find('.')->and_back->filter( '[v-if]' )->each( sub{
    my($variable,$rest) = split / /, $_->attr('v-if'), 2; 
    $_->attr('v-if' => undef);

    $_->remove unless eval qq{
      Template::Mustache::resolve_context( '$variable', \\\@context )
      $rest
    };
  });

  return $doc->as_html;
}

...since this is a protype, we're slightly gross and assume blindly that the first token of the expression is a variable to resolve as per the context, and the rest is a Perl expression. Which is... soooomewhat reasonable.

use Web::Query::LibXML;

sub process_directives( $self, $doc, @context ) {
  @context = ( $self ) unless @context;

  $doc = Web::Query::LibXML->new( $doc, { indent => '  ' })
    unless ref $doc;

  $doc->find('.')->and_back->filter( '[v-if]' )->each( sub{
    my($variable,$rest) = split / /, $_->attr('v-if'), 2; 
    $_->attr('v-if' => undef);

    $_->remove unless eval qq{
      Template::Mustache::resolve_context( '$variable', \\\@context )
      $rest
    };
  });

  return $doc->as_html;
}

In any case, if the condition turns out to be false, we delete the node and never speak of it again.

use Web::Query::LibXML;

sub process_directives( $self, $doc, @context ) {
  @context = ( $self ) unless @context;

  $doc = Web::Query::LibXML->new( $doc, { indent => '  ' })
    unless ref $doc;

  $doc->find('.')->and_back->filter( '[v-if]' )->each( sub{
    my($variable,$rest) = split / /, $_->attr('v-if'), 2; 
    $_->attr('v-if' => undef);

    $_->remove unless eval qq{
      Template::Mustache::resolve_context( '$variable', \\\@context )
      $rest
    };
  });

  return $doc->as_html;
}

Step 3: Bindings

For the bindings, it's almost the same tactic. We go through all the nodes.

sub process_directives( $self, $doc, @context ) {
    ...;

    $doc->find('.')->and_back->each( sub {
        my $elt = $_;
        my @attrs = map { $_->all_attr } $_->{trees}->@*;
        for my $attr ( @attrs ) {
            next unless $attr =~ s/^://;
            my $v = $elt->attr( ':'.$attr);
            $elt->attr( ':'.$attr => undef );
            $elt->attr( $attr => Template::Mustache::resolve_context( 
                $v,  \@context
            ));
        }
    });

    ...;
}

We then filter on those that have attributes prefixed with colons.

The code is a little on the ugly side because Web::Query doesn't allow to get all attributes easily. I'll also try to fix this presently.

sub process_directives( $self, $doc, @context ) {
    ...;

    $doc->find('.')->and_back->each( sub {
        my $elt = $_;
        my @attrs = map { $_->all_attr } $_->{trees}->@*;
        for my $attr ( @attrs ) {
            next unless $attr =~ s/^://;
            my $v = $elt->attr( ':'.$attr);
            $elt->attr( ':'.$attr => undef );
            $elt->attr( $attr => Template::Mustache::resolve_context( 
                $v,  \@context
            ));
        }
    });

    ...;
}

Anyhoo, we populate the attribute foo with whatever the variable or method given in :foo interpolate into given the context.

Again, we're playing fast and loose, but we want we could come back and parse less blindly what is in :foo. But that's just finickling around.

sub process_directives( $self, $doc, @context ) {
    ...;

    $doc->find('.')->and_back->each( sub {
        my $elt = $_;
        my @attrs = map { $_->all_attr } $_->{trees}->@*;
        for my $attr ( @attrs ) {
            next unless $attr =~ s/^://;
            my $v = $elt->attr( ':'.$attr);
            $elt->attr( ':'.$attr => undef );
            $elt->attr( $attr => Template::Mustache::resolve_context( 
                $v,  \@context
            ));
        }
    });

    ...;
}

Step 4: iterations

For the v-for iterators, same procedure as usual. We look at all nodes with v-for attributes...

sub process_directives( $self, $doc, @context ) {
    ...;

    use experimental 'postderef';

    # TODO have to think about v-fors in v-fors
    $doc->find( '[v-for]' )->each( sub{
        my( $item, $key ) = split /\s+in\s+/, $_->attr('v-for'), 2;
        my @items = Template::Mustache::resolve_context( $key, \@context )->@*;

        my $block = $_;
        $block->attr('v-for' => undef);

        my $new = join '', map {  
            $self->process_directives( 
                $block->clone, 
                { $item => $_ }, 
                @context 
            );
        } @items;

        $block->after($new);
        $block->detach;
        
    });

    ...;
}

... and for every item of the list iterate over we make a copy of the tag, augment the template context with the iteree, and invoke the MIGHTY POWER OF RECURSION!

ALL* HAIL THE RECURSION GODS!

(*all includes the Recursing Gods themselves, natch)

sub process_directives( $self, $doc, @context ) {
    ...;

    use experimental 'postderef';

    # TODO have to think about v-fors in v-fors
    $doc->find( '[v-for]' )->each( sub{
        my( $item, $key ) = split /\s+in\s+/, $_->attr('v-for'), 2;
        my @items = Template::Mustache::resolve_context( $key, \@context )->@*;

        my $block = $_;
        $block->attr('v-for' => undef);

        my $new = join '', map {  
            $self->process_directives( 
                $block->clone, 
                { $item => $_ }, 
                @context 
            );
        } @items;

        $block->after($new);
        $block->detach;
        
    });

    ...;
}

Step 5: Sub-components

Last bit, the sub-components.

Here we find nodes which tag name is the name of a component.

(for now it's assumed that the tag name of the component Example::Foo is foo)

use Module::Runtime qw/ use_module /;

sub process_directives( $self, $doc, @context ) {
    ...;

    for my $component ( $self->components->@* ) {
        my $name = lc $component =~ s/.*:://r;
        $doc->find($name)->each(sub{
            my $elt = $_;

            my %attr = map { $_ => $elt->attr($_) } 
                            $_->{trees}[0]->all_attr;

            $elt->after(
                use_module($component)->new( %attr )->render
            );
            $elt->detach;
        });
    }

    ...;
}

For those tags, we collect all the attributes, which will be the props of the sub-components.

use Module::Runtime qw/ use_module /;

sub process_directives( $self, $doc, @context ) {
    ...;

    for my $component ( $self->components->@* ) {
        my $name = lc $component =~ s/.*:://r;
        $doc->find($name)->each(sub{
            my $elt = $_;

            my %attr = map { $_ => $elt->attr($_) } 
                            $_->{trees}[0]->all_attr;

            $elt->after(
                use_module($component)->new( %attr )->render
            );
            $elt->detach;
        });
    }

    ...;
}

And since the sub-component is also a template and only needs to be fed its props to do its thang, we go ahead, create the object and render it.

use Module::Runtime qw/ use_module /;

sub process_directives( $self, $doc, @context ) {
    ...;

    for my $component ( $self->components->@* ) {
        my $name = lc $component =~ s/.*:://r;
        $doc->find($name)->each(sub{
            my $elt = $_;

            my %attr = map { $_ => $elt->attr($_) } 
                            $_->{trees}[0]->all_attr;

            $elt->after(
                use_module($component)->new( %attr )->render
            );
            $elt->detach;
        });
    }

    ...;
}

BEHOLD!

And we are done. With the code we did, plus that last method that interconnect the pipeline....

sub render($self) {
    my $mustached = $self->render_mustache;

    my $directived = $self->process_directives( $mustached );

    return $directived;
}

... we now have a set of components that we can call thus...

use 5.20.0;

use lib 'lib';

use Example::Main;

say Example::Main->new( 
    title => 'Hello world!',
    items => [ 'this', 'skip_me', 'me_too', 'that' ] 
)->render;

...will give us that.

<div>
    <h1>Hello world!</h1>

    <ul><li>this</li><li>that</li></ul></div>

Now, it's not like I just recreated Vue. The reactive part of it really the piece de resistance, and the template and single file component format are merely delicious, peripheral trimmings. And a lot of parts are at best penciled in.

But... it's in (arguably) working condition.

And the Template::Vue role as it stands weights 110 lines.

Of which 25 are empty.

(you can check yourself: the code is on GitHub)

So, yeah, I guess what I'm trying to say is

Tadah.

comments powered by Disqus

About the author

Yanick Champoux
Perl necrohacker , ACP writer, orchid lover. Slightly bonker all around.