Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(content projection): component inputs accepting arrow functions returning ng-templates #56056

Closed
mauriziocescon opened this issue May 23, 2024 · 4 comments
Labels
area: core Issues related to the framework runtime core: content projection
Milestone

Comments

@mauriziocescon
Copy link

mauriziocescon commented May 23, 2024

Which @angular/* package(s) are relevant/related to the feature request?

compiler, core

Introduction

With the recent @let template variables proposal, there is IMO some sort of clashing at the level of names / concepts / notations between @let template variables and # template variables.

Some similarities are:

  1. they both have scopes,
  2. you can pass them around as inputs.

On the other hand, they (I guess) react differently to CD and have different scope rules:

// https://github.com/angular/angular/issues/15280#issuecomment-2097161182

@if (true) {
    // legal with #, but illegal with @let 
    <div>{{ m.value }}</div>
      
    <input [ngModel]="data" #m="ngModel">
    <div>{{ m.value }}</div>

    @for(i of [1, 2]; track $index) {
        {{ m.value }}
    }
}
<!-- {{ m.value }} illegal -->

# variables are references to html elements. As such, they play well with (conditional) content-projection:

// don't have to move #tempRef before my-component: can stay before / inside / after
<my-component [someTemplate]="tempRef">
    <ng-template #tempRef>...</ng-template>
</my-component>

Speaking about content projection (or composition), I have to admit that I've always found angular options not particularly straightforward. In general, mastering content projection in angular requires decent knowledge of:

  1. ng-content,
  2. ng-template,
  3. # template variables,
  4. structural directives (powerful, but not easy to use),
  5. let-* shorthands and ngTemplateGuard_*,
  6. Ng**Outlet,
  7. many other things.

Proposal

Assuming that

  1. arrow functions in templates are on their way to be delivered (see tweet)
  2. and the mental model for # template variables cannot be fitted in the @let one,

I'd like to propose the following changes:

  1. replace # annotation with @ref:
    1. @ref="varName",
    2. @ref(ngModel) = "varName",
  2. allow the definition of @ref template variables like this
    1. @ref temp = {<ng-template>...</ng-template>},
    2. @ref temp = (...) => {<ng-template>...</ng-template>},
  3. Support the render of @ref template variables defined in 2.

Examples

Some refactored examples from the current Understanding template variables and new possibilities for content projection.

DOM element

<input @ref="phone" placeholder="phone number"/>
<button type="button" (click)="callPhone(phone.value)">Call</button>

Component or template

<my-tree-component @ref="tree"></my-tree-component>

Form

<form @ref(ngForm)="itemForm">
  // ...
</form>

<div [hidden]="!itemForm.form.valid">
  // ...
</div>

Content projection

// consumer-of-my-tree.component.ts
@Component({
  selector: 'consumer-component',
  // ...
  template: `
    <!-- ... -->
    @ref customNode = (node) => {
        <ng-template>
            @if (node.desc) {
                {{ node.desc }}
            }
            // manageNodeClick is defined in ConsumerComponent
            <span [class]="node.style" (click)="manageNodeClick(node)">{{ node.label }}</span>
        </ng-template>
    }
    <my-tree-component [data]="dataStructure" [customNode]="customNode"></my-tree-component>
  `,
})
export class ConsumerComponent {...}

// my-tree.component.ts
export interface Node {
    id: string;
    desc?: string;
    label: string;
    style: string;
}

@Component({
  selector: 'my-tree-component',
  // ...
  template: `
    <!-- ... -->

    <!-- Of course  the usage of ngTemplateOutlet below 
        is just to provide ideas: requires further investigation / adaptations 

        <ng-container [ngTemplateOutlet]="templateRefExp; context: contextExp"></ng-container>
    -->

    for (node of nodes(); track node.id) {
        @let temp = customNode();
        <ng-container [ngTemplateOutlet]="temp(node)"></ng-container>
    }
  `,
})
export class MyTreeComponent {
    data = input.required<...>();

    // I hope something like this is enough for types inference in consumer-of-my-tree.component.ts
    customNode = input<(node: Node) => TemplateRef<any>>();
    
    nodes = signal<Node[]>([]); // populated started from data
    // ...
}

Projection variants

// inline function (this would be great)
<my-tree-component [data]="dataStructure" [customNode]="(node) => {
    <ng-template>
        <my-custom-node [node]="node"></my-custom-node>
    </ng-template>
}">
</my-tree-component>

// inline functions (this would be great)
<my-tree-component [data]="dataStructure" 
    [customNode]="(node) => {
        <ng-template>
            // ...
        </ng-template>
    }"
    [somethingElse]="() => {
        <ng-template>
            // ...
        </ng-template>
}">
</my-tree-component>

// cascading (this would be great)
<my-tree-component [data]="dataStructure" 
    [customNode]="(node) => {
        <ng-template>
            <my-custom-node [node]="node" [somethingElse]="() => {
                <ng-template>
                    // ...
                </ng-template>
            }">
            </my-custom-node>
        </ng-template>
}">
</my-tree-component>

Objections

Some reasonable objections:

  1. Why don't leaving # variables as they are and simply allow @let temp = (...) => {<ng-template>...</ng-template>}?
    Answer: yeah, that would be ideal! The point is: I'm not sure it's possible considering the slightly different mental models and how the compile works. Unfortunately, I have a limited understanding of both concepts... therefore, I went for the @ref way (just a # remapping at compilation time). But of course, defining a @ref variable after its usage, it's really strange. Moreover it will increase confusion between @let and @ref variables.

  2. How about viewChild, contentChild, ... ?
    Answer: well, for templates like @ref temp = {<ng-template>...</ng-template>} I guess it can work as right now (just a different way of definiting an ng-template). Not sure about arrow functions... probably discarded. Problem solved if 1) is applicable. In general, I think templates passed as inputs are more flexible than the current content_projection / content_queries mechanisms .

  3. Your new @ref definition is not real ts
    Answer: that's true... as it's not real ts the following @let user = user$ | async ?? 'unknonw'. The idea behind @ref temp = (...) => {<ng-template>...</ng-template>} is to be familiar, not valid ts. Anyway: the proposal can be adapted if 43485 is strictly required.

  4. Why doing things in a different way? You already have all the tools to achieve content projection needs
    Answer: true! But on the other hand, I think simple / familiar things are always better. Mastering the current angular content projection (derived from html standards) is not easy... especially if you're coming from other frameworks.

    In practice, it's just a parent-child interaction where the parent wants a hook to return a customised template to be used in the child, based on a child-defined object interface (even extendable by the parent). A closure feels very natural and boost components reusability.

  5. Maybe more complicated, but the current angular content projection (see material) is more readable because it's more semanatic and close to html standards
    Answer: that's true! Say: I hope there's a way to inline @ref temp = (...) => {<ng-template>...</ng-template>} in component inputs. Assuming it's the case, it's mostly a matter of decluttering the examples above (remove <ng-template>) and the semantic beauty comes back. With just a different taste! I also hope the upcoming DX for components can help fixing this problem. But for now, I guess no solution.

Conclusion

The main goal of this issue is to propose an expansion of the current content projection capabilities. But due to the ongoing development of @let and my limited understanding of the angular compiler, I might have proposed something very difficult to achieve (or a bit stupid 😅). In case, sorry for this!

And yes, the intention is to push angular content projection into react / solid land... which IMO is cool cause it's one of the things these 2 frameworks are shining on (of course easy to achieve when components are just functions 😅).

PS: thanks a lot for the amazing work you're doing!

@mauriziocescon
Copy link
Author

mauriziocescon commented May 24, 2024

Just a stackblitz to summarise the current possibilities https://stackblitz.com/edit/stackblitz-starters-uoy6sf

Note: you might say that the example could be changed by introducing structural directives. This way consumer.tree would look better.

But as I see it, it's just a way to rebrande complexity and write more code: tree.component will be packed with directives and consumer.tree will use more advance concepts.

IMHO making a closer in tree.component over consumer.tree arrow func inputs is the most natural / easy / elegant way to solve the problem.

My 2 cents!

@pkozlowski-opensource pkozlowski-opensource added the area: core Issues related to the framework runtime label May 28, 2024
@ngbot ngbot bot added this to the needsTriage milestone May 28, 2024
@mauriziocescon
Copy link
Author

mauriziocescon commented Jun 7, 2024

Umh... I wonder if the "content projection and content queries unification" (see #37319) could be partially rephrased using this proposal: use inputs to pass content to project (in the form of input:template or input:arrowFunc_returning_template).

It seems to work for some common problems:

  • Was the content provided at all? => Was the input provided?
  • How many items were provided? => Define inputs in a way that fulfils the need.

Maybe I'm missing something very important / obvious (ng-content is more performant, CD would be a mess, ...). But the new (typed) signal inputs are pretty powerful / flexible. And reducing complex projection scenarios to "variables to pass as inputs", seems to simplify problems. Anyway, the syntax should be beautified (it's a bit ugly with <ng-template>).

Ok... just a comment (hopefully useful) for the future! 😅

@pkozlowski-opensource
Copy link
Member

Hey! Thnx for the suggestion - we absolutely feel the pain of working with the current content projection and dynamic template instantiation - especially for more complex cases. We do think of a project proposal / roadmap entry that would significantly improve the situation.

I'm going to close this issue as it prescribes a particular solution to one of the use-cases. But we are aware of other pain points so we should look at those more holistically. Also, should not constraint the solution space before we understand all the use-cases that need attention.

We do appreciate your input, though - this is one more indicator that we need to dig deeper into those areas.

@pkozlowski-opensource pkozlowski-opensource closed this as not planned Won't fix, can't repro, duplicate, stale Jun 19, 2024
@mauriziocescon
Copy link
Author

Sure np! Perfectly aware that it was a partial solution.

Just last personal suggestion considering I often work with "react" guys: do not underestimate the power of changing ng content projection in a way that results familiar for the react community (on top of making it better for the ng one 😃). IMO it can increase the rate of react devs giving angular another successful try... and of course make the creation of "component libs" easier.

I'm perfectly aware you're busy with several big tracks... and the ng community is often focus on SFC / classes / decorators while comparing frameworks (myself included 😅). But I think this might be a less mainstream point with a potentially high ROI for angular adoption!

Just my 2 cents! And as usual, thanks a lot for your work!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: core Issues related to the framework runtime core: content projection
Projects
None yet
Development

No branches or pull requests

2 participants