32

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);
        }
      }
    },
    ...
  };
});
Estus Flask
  • 206,104
  • 70
  • 425
  • 565
  • What value dis you set wirg setAttribute to get the error? – Günter Zöchbauer Jul 02 '16 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. – Estus Flask Jul 02 '16 at 16:10
  • There is no way at all to estsblish data-binding programmatically. Data-binding works only for elements added to a components template statically. Instead if you need something like that use shared services to communicate between components. I have a hard time figuring out what problem you're actually trxing to resolve. – Günter Zöchbauer Jul 02 '16 at 16:24
  • Imho this would just make the code less readable and unnecessary complicated - what'd be the downside of simply using @Input and placing the attributes in the template? – olsn Jul 02 '16 at 16:37
  • Just out of curiosity, is ` really the character used around template value? It may be, just wanted to be sure its use is intentional. – mico Jul 02 '16 at 16:40
  • 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. – Estus Flask Jul 02 '16 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](http://stackoverflow.com/questions/38080933/extending-decorating-angular-2-components-and-directives), and the conclusions weren't really optimistic. – Estus Flask Jul 02 '16 at 17:06
  • So you want to build a generic wrapper that can wrap different components even when they have different inputs and outputs? – Günter Zöchbauer Jul 02 '16 at 17:10
  • @GünterZöchbauer I'm thinking more of n wrappers for n components that have common helper function to translate attributes and keep wrapper template DRY. But a generic wrapper may be another good use case for the subject. – Estus Flask Jul 02 '16 at 17:19
  • 2
    I think currently the best oprion is to just forward each input and outpur explicitely. If you need a lot use code generation. – Günter Zöchbauer Jul 02 '16 at 17:36

3 Answers3

6

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 :)

Ankit Singh
  • 24,525
  • 11
  • 66
  • 89
  • 2
    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. – Estus Flask Jul 08 '16 at 13:47
  • 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. – Ankit Singh Jul 10 '16 at 12:23
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.

Riley Lark
  • 20,660
  • 15
  • 80
  • 128
  • 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. – Estus Flask Jul 09 '16 at 13:23
-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() {
  }
}
Nick Acosta
  • 1,890
  • 17
  • 19
  • This is what was already done in original code. The question is how to avoid the enumeration of child inputs in parent template. – Estus Flask Jul 04 '16 at 14:22
  • You mean like using an object instead of a string? – Nick Acosta Jul 04 '16 at 14:24
  • The idea is to avoid WET code in templates like that `` 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. – Estus Flask Jul 04 '16 at 14:45