33

How can the attributes be transparently translated from wrapper component to nested component?

Considering that there is

const FIRST_PARTY_OWN_INPUTS = [...];
const FIRST_PARTY_PASSTHROUGH_INPUTS = ['all', 'attrs', 'are', 'passed'];
@Component({
  selector: 'first-party',
  inputs: [...FIRST_PARTY_OWN_INPUTS, ...FIRST_PARTY_PASSTHROUGH_INPUTS],
  template: `
<div>
  <third-party [all]="all" [attrs]="attrs" [are]="are" [passed]="passed"></third-party>
  <first-party-extra></first-party-extra>
</div>
  `,
  directives: [ThirdParty]
})
export class FirstParty { ... }

Can the inputs be translated in batch, so they would not be enumerated in template?

The code above is supposed to recreate the recipe for Angular 1.x directives:

app.directive('firstParty', function (thirdPartyDirective) {
  const OWN_ATTRS = [...];
  const PASSTHROUGH_ATTRS = Object.keys(thirdPartyDirective[0].scope);

  return {
    scope: ...,
    template: `
<div>
  <third-party></third-party>
  <first-party-extra></first-party-extra>
</div>
    `,
    compile: function (element, attrs) {
      const nestedElement = element.find('third-party');

      for (let [normalizedAttr, attr] of Object.entries(attrs.$attr)) {
        if (PASSTHROUGH_ATTRS.includes(normalizedAttr)) {
          nestedElement.attr(attr, normalizedAttr);
        }
      }
    },
    ...
  };
});
10
  • What value dis you set wirg setAttribute to get the error? Commented Jul 2, 2016 at 15:17
  • @GünterZöchbauer I guess the error was caused by something like this.elementRef.nativeElement.querySelector('third-party').setAttribute('[attr]', 'attr') in onInit. Using bind-attr instead doesn't trigger the error but doesn't help either. Any way, I'm positive that there are idiomatic ways to establish data binding programmatically, and it looks like this isn't one of them. Commented Jul 2, 2016 at 16:10
  • 1
    @olsn This may be the typical task for significant amount of components/directives. And it can be abstracted to helper function. Sometimes WET code is preferable and sometimes it's not, it's the dev's choice. The lack of knowledge of how to keep it DRY is no excuse for WET, methinks. Yes, they are template literals with grave accents. Commented Jul 2, 2016 at 16:56
  • 1
    @GünterZöchbauer It is classic scenario of customizing third-party component by adding style/extra markup/whatever. Done that a lot before. 'If you want it customized, wrap it' is idiomatic, correct me if I'm wrong. I examined another approach to the problem in this question, and the conclusions weren't really optimistic. Commented Jul 2, 2016 at 17:06
  • 2
    I think currently the best oprion is to just forward each input and outpur explicitely. If you need a lot use code generation. Commented Jul 2, 2016 at 17:36

3 Answers 3

6
+150

I'm not sure if I got it right but here is my implementation ( PLUNKER )


const FIRST_PARTY_OWN_INPUTS = ['not', 'passthrough'];
const FIRST_PARTY_PASSTHROUGH_INPUTS = ['all', 'attrs', 'are', 'passed'];

const generateAttributes(arr) {
   return arr.map(att => '[' + att + '] = "' + att + '"').join(' ');
}


//-------------------------------------------------------//////////////////
import {Component} from '@angular/core'

@Component({
  selector: 'third-party',
  inputs: [...FIRST_PARTY_PASSTHROUGH_INPUTS],
  template: `
<div>
  {{all}} , {{attrs}} ,  {{are}} ,  {{passed}}
</div>
  `
})
export class ThirdParty {
}

@Component({
  selector: 'first-party',
  inputs: [...FIRST_PARTY_OWN_INPUTS, ...FIRST_PARTY_PASSTHROUGH_INPUTS],
  template: `
<div>
  <div>
    {{not}} , {{passthrough}}
  </div>
  <third-party ${generateAttributes(FIRST_PARTY_PASSTHROUGH_INPUTS)}></third-party>
  <first-party-extra></first-party-extra>
</div>
  `,
  directives: [ThirdParty]
})
export class FirstParty {
}

@Component({
  selector: 'my-app',
  providers: [],
  template: `
    <div>
      <h2>Hello {{name}}</h2>
      <first-party [not]="'not'" [passthrough]="'passthrough'"  
                   [all]="'all'" [attrs]="'attrs'" [are]="'are'" [passed]="'passed'">
      </first-party>
    </div>
  `,
  directives: [FirstParty]
})
export class App {
  constructor() {
    this.name = 'Angular2 (Release Candidate!)'
  }
}

Hope it helps :)

Sign up to request clarification or add additional context in comments.

2 Comments

I guess it is function generateAttributes. Thanks, I was hoping for component-level solution, but for now I guess I will stick to such helper. Still not sure if it is good or bad for offline template compilation.
glad that I could help :) I'm not that level yet, so. I guess if you have almost same template code here and there with only a few changes, this should be nice, I use it myself, not sure if good or bad though.
0

I think this can be boiled down to a more basic problem without Angular2 at all. When you have a function that takes a lot of parameters, it's annoying and error-prone to have to specify all those parameters every time you want to use it. The problem gets worse when there's an intermediate function that doesn't care about those parameters at all - you find yourself adding parameters to the intermediate function just so it can pass it to the inner function. Yeargh!

There are a few patterns for dealing with this. My favorite is to fully instantiate the inner function and pass the instance already loaded up with the former pass-through parameters embedded in it. I think http://blog.mgechev.com/2016/01/23/angular2-viewchildren-contentchildren-difference-viewproviders/ is a nice post about how to do that in Angular 2 using @ViewChild and @ContentChild. Another strategy is to wrap all of the pass-through parameters up in a single object, so at least there's only one parameter to pass through. That also helps when you want to add more parameters - since they're already being wrapped up and passed through opaquely, your pass-through code doesn't need to change.

1 Comment

I've awarded bonus points just before your answer has been posted. I agree on the example with function, but I can't imagine how this applies to the question. third-party component has a set of bound attributes, and first-party should reflect them all. There was an answer that involved ViewChild but it was deleted before I had a chance to check it. Please, feel free to illustrate the answer with code if you have an idea on how it should be implemented.
-1

You can accomplish this by using @Input() on your child components.

http://plnkr.co/edit/9iyEsnyEPZ4hBmf2E0ri?p=preview

Parent Component:

import {Component} from '@angular/core';
import {ChildComponent} from './child.component';

@Component({
  selector: 'my-parent',
  directives: [ChildComponent],
  template: `
    <div>
      <h2>I am the parent.</h2>
      My name is {{firstName}} {{lastName}}.

        <my-child firstName="{{firstName}}" 
                  lastName="{{lastName}}">

        </my-child>

    </div>
  `
})
export class ParentComponent {
  public firstName:string;
  public lastName: string;
  constructor() {
    this.firstName = 'Bob';
    this.lastName = 'Smith';
  }
}

Child Component:

import {Component, Input} from '@angular/core';

@Component({
  selector: 'my-child',
  template: `
    <div>
      <h3>I am the child.</h3>
      My name is {{firstName}} {{lastName}} Jr.
      <br/>
     The name I got from my parent was: {{firstName}} {{lastName}}

    </div>
  `
})
export class ChildComponent {
  @Input() firstName: string;
  @Input() lastName: string;
}

App Component:

//our root app component
import {Component} from '@angular/core';
import {ParentComponent} from './parent.component';

@Component({
  selector: 'my-app',
  directives: [ParentComponent],
  template: `
    <div>
      <my-parent></my-parent>
    </div>
  `
})
export class App {

  constructor() {
  }
}

3 Comments

This is what was already done in original code. The question is how to avoid the enumeration of child inputs in parent template.
You mean like using an object instead of a string?
The idea is to avoid WET code in templates like that <my-child firstName="{{firstName}}" lastName="{{lastName}}"> and add the properties on the fly, like it could be done in AngularJS by compile function. I hoped that the question states this clearly.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.