This is a feature request for procedurally/dynamically compiled components.
As of now, when declaring a component in HTML you can only hard-code the tag. This is a limitation, when you want the type of a component to be determined based on the current container's state. A common workaround for this is using ng-switch
or ng-if
alongside multiple hard-coded components:
<div ng-switch="dynamic.component">
<alpha ng-switch-when="alpha"></alpha>
<beta ng-switch-when="beta"></beta>
</div>
or
<alpha ng-if="dynamic.component == 'alpha'"></alpha>
<beta ng-if="dynamic.component == 'beta'"></beta>
In my opinion this is an antipattern, because
What angular is currently missing are dynamic components, which types are determined at runtime. I'm therefore suggesting a new directive to be implemented, which either replaces itself with a given component or creates that component inside its own element. It would need to take two arguments: a component name (expression evaluated to string) and its parameters/attributes packed into an object.
The following syntax suggestions are separated into component definition (A and B) and arguments passing (C and D). All combinations are part of the overall suggestion.
<component name="'alpha'"></component>
which would render the component inside itself:
<component name="'alpha'">
<alpha></alpha>
</component>
Note that the value of the name attribute is an Angular expression, so you could also write
name="$ctrl.componentname"
Syntax suggestion B:
<component name="'alpha'"></component>
or
<ANYELEMENT component="'alpha'"></ANYELEMENT>
which would render:
<alpha component="'alpha'"></alpha>
thus de-facto replacing itself.
<component name="'alpha'" args="{ numb: 123, strng : '\"arg2\"', exp: "scopeexpression" }"></component>
which would render:
<component name="'alpha'" args="{ numb : 123, strng : '\"arg2\"', exp: "scopeexpression" }">
<alpha numb="123" strng="'arg2'" exp="scopeexpression"></alpha>
</component>
The above syntax looks much better when using variables of the current scope:
<component name="parent.cmp_name" args="parent.cmp_args"></component>
which would render:
<component name="parent.cmp_name" args="parent.cmp_args">
<alpha numb="args.numb" strng="args.strng" exp="args.exp"></alpha>
</component>
As you can see, the bindings of the alpha component are served by the component's scope, which has a binding to its own "args" attribute.
<component name="expr">
<placeholder numb="expr1" strng="expr2" exp="expr3"></placeholder>
</component>
which renders to:
<component name="'alpha'">
<alpha numb="expr1" strng="expr2" exp="expr3"></alpha>
</component>
In this case the component doesn't create any new elements, but only changes the tag of its contained "placeholder" elements. The bindings stay as they are and there is no need for packing them into one object. I think, this is the best solution since it doesn't mess with proxying of the arguments and enables a much simpler syntax.
A dashboard widget is being configured. The user is presented a dropdown list (select-element) with possible widgets: gauge, line-graph, pie-chart. Once he choses one, the widget should be rendered beside. Using procedural/dynamic components you could save the component names as values in the select-element and the dynamic component would render the selected one, without knowing anything about the list's contents. This way the view is reactive and needs no adjustments when the list-entries change.
Example code:
<select ng-model="$ctrl.widget" ng-options="$ctrl.options">
<option value="gauge">Gauge</option>
<option value="linegraph">Line Graph</option>
<option value="piechart">Pie Chart</option>
</select>
<component name="$ctrl.widget"></component>
Think of a typical smartphone chat app. The conversation view consists of a scrollable list of various items: incoming messages, outgoing messages, status infos, horizontal lines (separators), date indicators etc. Using Angular you would want to save all these items in one array and simply iterate in the view. If you save the type of object alongside the object itself in the list, you can use the procedural/dynamic component directive to render the component inside a ng-repeat in only one line of code.
Example code:
<component ng-repeat="item in $ctrl.conversationItems" name="item.type"></component>
alternatively:
<div ng-repeat="item in $ctrl.conversationItems">
<component name="item.type"></component>
</div>
Hint: Android implementation of WhatsApp most probably uses RecyclerView, which is basically a list widget that can hold various item-widgets. Based on the list item's data, different item-widget can be used.
Implications
This technique can act as Inversion Of Control, since the data tells how it is to be rendered, not the parent template. However, the concerns of model, view and controller stay separated, since we only carry a reference to the component - its name.
Since components are like elements to the browser, unknown components and recognized HTML elements are no problem and render normally (like hard-coded).
Example of an early implementation can be seen here: http://stackoverflow.com/questions/38599293/including-components-procedurally-in-angularjs/38601503#38601503
Problems with this implementation: Uses arguments syntax C and is therefore very quirky (two-way bindings broken etc.).
Great write-up! Although I think it is perfectly fine for certain usecases, I don't find this feature is common enough (or "specific" enough) to make it a reasonable addition to Angular core.
In practice, there are very few usecases were different components can handle the same set of attributes/inputs and you can swap one for another just by changing the name. (I am not saying there aren't such cases, but they are the exception.)
Besides, this is a typical case of a higher level component that can be as simple or as complex as you want and implementing such components for a specific app makes much more sense than adding them to a generic library or framework. Making the component too simple (and lightweight) it will not cover all usecases. Making it generic and flexible would solve that, but add extra cost (in terms of code size, maintenance, usage complexity etc) even for people that don't need it.
Finally, considering that this can be equally well implemented as a 3rd party directive (i.e. it doesn't need access to any internal stuff), I am against adding this to core.
Let's see what others think...
Short and fully working example of this: http://plnkr.co/edit/uDxIUulQPx4C3s11b5cG?p=preview
I've encountered this sort of problem before too - something like this would be immensely useful when dealing with ng-repeat for example, where one explicitly requires different element types being rendered as siblings.
One can work around it, but various solutions end up creating extra watchers.
I would consider this a nice-to-have, given that most of the significant work is going into getting Angular 2 right currently.
I've put the code together with extensive documentation in an own repo now: https://github.com/hubertgrzeskowiak/angular-component-directive. Self-replacement is managed by an additional attribute, so syntax proposal A and B are both in. Args were implemented following syntax C.
@hubertgrzeskowiak you can achieve this using named view with ui-router https://github.com/angular-ui/ui-router/wiki/Multiple-Named-Views
It's another solution :)
Hi @aganglada and thanks for the suggestion. I've looked through the docs a little, and it would surely help in use case 1, but I'm not sure it could solve situations like the one shown in use case 2.
I agree with @gkalpak that this is not needed in core, as the demand is not very high, suitable solutions exist, and the proposed format can be achieved with a 3rd party directive.
Most helpful comment
I've encountered this sort of problem before too - something like this would be immensely useful when dealing with ng-repeat for example, where one explicitly requires different element types being rendered as siblings.
One can work around it, but various solutions end up creating extra watchers.
I would consider this a nice-to-have, given that most of the significant work is going into getting Angular 2 right currently.