Changing a Components Template without Subclassing

14 views
Skip to first unread message

Codewise Software

unread,
Aug 31, 2018, 1:00:51 AM8/31/18
to ang...@googlegroups.com
 
I have created an authentication library @mylib/auth  to use across various applications. It works great in that I just need to import it in my app module, and I get all sorts of great components (LoginComponent, RegisterComponent, ResetPasswordComponent, etc) and my AuthService provided for me. Even more still, by importing AuthModule.routing() in my AppModule definition I get all these routes defined for my app as well. '/sign-in', '/register', '/reset-password'. So by adding just a couple of lines (below) into any of my applications I get users and authentication. It's great and I love it.

@NgModule({
 ...
  imports: [
     ...
    AuthModule,
    AuthModule.routing(),
    ...

 Now the catch is, that if I want to change the template for one of these pages, I have to subclass the component. While that is simple enough, it means I lose the magic of just importing AuthenticationModule.routing() in my main module, so then I have to create a routing module and define all my routes there...

import { LoginComponent } from '@mylib/auth'

@Component({
  selector: 'app-login',
  templateUrl: './login.component.html',
  styleUrls: ['./login.component.css'],
})
export class DerivedLoginComponent extends LoginComponent {

}

So, while it's not the end of the world - I REALLY like just having those 2 lines to add into my app to get my authentication components and urls. If I could make it just 3 lines in order to change my template I would LOVE it. Something like this:

template.auth.login = '/templates/auth/login.component.html'

@NgModule({
 ...
  imports: [
     ...
    AuthModule,
    AuthModule.routing(),
    ...



So my  thoughts here was to create a variables called templates in a single file mylib/auth/templates.ts, and pull in that variable in each of my *.component.ts files and use it determine the which template to use.

I was stopped very quickly as even just moving the @component definition variable outside (like the code below) of the decorator causes errors in the browser after compiling - just a blank page and in the console "cannot find ./login.template.html"

let ngComponent = {
  selector: 'app-login',
  templateUrl:  './login.component.html',
  styleUrls: ['./login.component.css'],
}

@Component(
  ngComponent
)
export class LoginComponent extends FormComponent  {


I have plans to make a few more libraries in addition to the auth library  and the consuming apps will pretty much only want to change the template.

Is there a solution to this problem (that works with AOT)? Maybe this whole ordeal is a bit esoteric but I am loving the clean project folder and just adding a few simple lines to get add powerful features. I'm willing to jump through some hoops in the Auth library to make the consuming apps super simple.

Thanks in advance for any insight.


Zlatko Đurić

unread,
Aug 31, 2018, 3:19:30 AM8/31/18
to Angular and AngularJS discussion
The problem here is that your template is not present at runtime. Angular Compiler takes metadata and your typescript code, and creates their internal representation of all the stuff. Basically, decorations are moved to something like CompiledComponent.annotations = { template: '...', ...  }. But messing with that is a bit tricky + you have a problem of Angular potentially changing this internal stuff.

But if you still wanted it, you could write a custom component decorator that would do this magic.

If you wanted less magic, as an alternative, you can simply make your component designed this way, meaning, in the way that it expects optional different templates. Something that would work like this:

1. Import your login component.
2. Use it:

<login-component custom=true>
    <ng-template my-login-template> <!-- this is basically the template that you're overriding with.
        <select name="username">
           <option>Me</option>
           <option>The other guy</option>
        </select>
    </ng-template>
</login-component>

Then your component would have to know about this:

<div *ngIf="!custom">
    <input name="username">
</div>
<ng-content select="[my-login-template]"></ng-content>

Then depending on the "custom" prop (name it better, please), you would hook your listeners into different input fields. A bit tricky part is knowing if the custom template actually provides the proper things - e.g. if it finds or not a form element with [name="username] attribute in the example above.

What you can do for that part is simply fail - have the component throw if it doesn't find username and password fields. I can't think of a way to fail at dev time (e.g. have Code warn the user this field is missing) , but at least it would break some tests and the user(dev I mean) sees what the problem is in the console. When they come to your github screaming "no work!", you point them to the clear error message.

This looks a cleaner approach then tapping into Angular internals here..

Zlatko

Codewise IO

unread,
Sep 1, 2018, 9:19:54 PM9/1/18
to Angular and AngularJS discussion
Hi Zlatco,

Thank you for your response. I'll note that I don't need my template to be present at run time because I am not looking to have my application dynamically set the components template. I just want to be able to define what the template is in @mylib, and over-ride that setting later, a simple registry containing the template path to use that over-riding of the template could happen before the app is compiled in order to be AOT compliant.

So that said the solution I came up with does actually load the template in at run time. While I will admit it seems like a slightly  fragile solution as, like you stated, Angular could change their implementation and the behavior I am utilizing may or may not be intended - but it works.

What I have done is moved all of my templates out of their original directories and into an assets folder. In @mylib, I define my LoginComponent like this:

let ngComponent = {
  selector: 'ag-login',
  templateUrl:  '../../../../assets/agape/auth/login.component.html'
}

@Component( ngComponent )
export class LoginComponent extends FormComponent  {

As I pointed out in my original email, when I move the component definition outside of the @Component() decorator and put it in a variable I was getting an error in the browser "Cannot find /login.template.html". After doing some digging, I realized what was happening is that Angular was not following it's typical behavior during compilation and moving that contents of that login.component.html file into the template property, but leaving it as is. Then during run time was looking for the file at http://myhost/login.component.html  After some experimenting I found that specifying the file path as in the example above - angular was looking for the file at http://myhost/assets/agape/auth/login.component.html. From this point, by adding in the assets directory to my angular.json file angular is able to find my template at runtime. In my library workspace/sandbox my assets declaration looks like this. The first line there pulls in the assets from the distribution directory, and the second pulls in the local assets folder.

"assets": [
  ...
  { "glob": "**/*", "input": "dist/agape/auth/assets/", "output": "/assets/" },
  { "glob": "**/*", "input": "src/assets/", "output": "/assets/" }
  ...
],

By utilizing the same directory structure in my local assets folder, I can effectively over ride the template! Bam! In my consuming applications, "dist/agape/auth/assets/" gets replaced with "node_modules/agape/auth/assets/"

To make sure the @agape/auth/assets folder gets packaged, I need to copy it from the source directory to the distribution directory before packaging because angular cli does not currently do this, so I simply have a post build script in my package.json file.

"postBuildAuth": "cp -r ./projects/agape/auth/src/assets ./dist/agape/auth/assets "


So, here's to keeping my fingers crossed that this doesn't break at some point, but having looked into creating a custom component decorator as you suggested, I think I could probably replicate this behavior if I needed to.


Thank you for the input and I'll hope that somebody else finds this useful.

Zlatko Đurić

unread,
Sep 2, 2018, 2:15:53 AM9/2/18
to ang...@googlegroups.com
Hello,

Well that's certainly an ingenious solution, I haven't thought of that before! What you're doing is essentially shipping the template file as an asset.

The custom loader I've suggested would have worked similar, except it would load the code directly from node_modules on build time and get rid of the extra assets you'd be shipping.

Anyway, glad that you've solved the problem.
Zlatko

--
You received this message because you are subscribed to a topic in the Google Groups "Angular and AngularJS discussion" group.
To unsubscribe from this topic, visit https://groups.google.com/d/topic/angular/oKWxizo63yA/unsubscribe.
To unsubscribe from this group and all its topics, send an email to angular+u...@googlegroups.com.
To post to this group, send email to ang...@googlegroups.com.
Visit this group at https://groups.google.com/group/angular.
For more options, visit https://groups.google.com/d/optout.
--
Zlatko
Reply all
Reply to author
Forward
0 new messages