What is a good way for the Angular directive to act as a facade for other elements?

This is a more general question about web components, however I will write examples in Angular as it offers some more ways to solve these problems (e.g. replace even if it is deprecated) and it is also more familiar to me and possibly others.

Update

Because of this comment, I think that many of the problems I am facing are Angular-specific, because Angular "compiles" the directives. (I cannot easily add or remove directive at runtime.) Therefore, I am no longer looking for a universal solution, but for a specific Angular solution. Sorry for this confusion!

Problem

Let's say I want to create a menu bar that might look like this:

 <x-menu> <x-menu-item>Open</x-menu-item> <x-menu-item>Edit</x-menu-item> <x-menu-item>Create</x-menu-item> </x-menu> 

This may mean the following:

 <section class="menu"> <ul class="menu-list"> <li class="menu-list-item"> <button type="button" class="menu-button">Open</button> </li> <li class="menu-list-item"> <button type="button" class="menu-button">Edit</button> </li> <li class="menu-list-item"> <button type="button" class="menu-button">Create</button> </li> </ul> </section> 

This is pretty trivial. Problems arise if I want to configure my <x-menu-item> to (existing) directives / attributes. Sometimes an attribute must refer to a button. For instance. a click on <x-menu-item> should probably be proxied for <button> because it is a "real" interactive element inside <x-menu-item> .

 <x-menu-item ng-click="foo()">Open</x-menu-item> <!-- → possible translation --> <li class="menu-list-item"> <button type="button" class="menu-button" ng-click="foo()">Open</button> </li> 

However, other attributes must be related to <li> . Say I want to hide <x-menu-item> I probably want to hide everything, not just <button> .

 <x-menu-item ng-hide="bar">Open</x-menu-item> <!-- → possible translation --> <li class="menu-list-item" ng-hide="bar"> <button type="button" class="menu-button">Open</button> </li> 

And than, of course, the attributes that affect <li> , as well as <button> . Say I want to disable <x-menu-item> , I probably want to create a <li> style, and I want to disable <button> .

 <x-menu-item ng-disabled="baz">Open</x-menu-item> <!-- → possible translation --> <li class="menu-list-item" ng-class="{ 'is-disabled': baz }"> <button type="button" class="menu-button" ng-disabled="baz">Open</button> </li> 

This is basically what I want to achieve. I know some solutions, but they all have their drawbacks.

Solution # 1: dynamically create a template and handle attributes manually

I could replace <x-menu-item> with a full dynamic template and handle the attributes manually. It might look like this (not fully functional):

 // directive definition return { restrict: 'E', transclude: true, template: function(tElement, tAttrs) { var buttonAttrs = []; var liAttrs = []; // loop through tAttrs.$attr // save some attributes like ng-click to buttonAttrs // save some attributes like ng-hiden to liAttrs // save some attributes like ng-disabled to buttonAttrs and liAttrs // optionally alter the attr-name and -value before saving (so ng-disabled is converted to a ng-class for liAttrs) // unknown attribute? save it to either buttonAttrs or liAttrs as a default // generate template var template = '<li class="menu-list-item" ' + liAttrs.join(' ') + '>' + '<button class="menu-button" ' + buttonAttrs.join(' ') + ' ng-transclude>' + '</button>' + '</li>'; return tElement.replaceWith(text); } } 

In some cases, it really works well. I have a custom <x-checkbox> that uses <input type="checkbox"> internally. In 95% of cases, I want all the attributes placed on the <x-checkbox> to be moved to <input type="checkbox"> and only some of the wrappers around <input type="checkbox"> .

I really handle ng-disabled here. If you're curious about how this might look, here is an example:

 angular.forEach(tAttrs.$attr, function(attrHtml, attrJs) { buttonAttrs.push(attrHtml + '="' + tAttrs[attrJs] + '"'); if (attrHtml === 'ng-disabled') { liAttrs.push('ng-class="{ \'is-disabled\': ' + tAttrs[attrJs] + ' }"'); } }); 

Disadvantages: I need to decide where to place attributes that I don’t know about in advance. Should they fit on <button> or <li> ? I think I want more attributes on <button> than on <li> because my <x-menu-item> is basically a wrapped button, and using it is like using a button. The developer would expect <x-menu-item> to work as a <button> . However, it seems strange not to place unknown attributes on the root element (in this case <li> ). You can also expect that the attributes on <li> will affect <button> if necessary (for example, the CSS class). I also create my markup in JavaScript, instead of plain HTML.

Replace or not replace

I know its obsolete, but sometimes I like to use my directive with my template. Say someone puts id in my directive, I like to move id to the canonical element in the template representing the directive (.eg on a <x-checkbox> id will be passed to <input checkbox="type"> ), therefore, if someone tries to getElementById , he will get a canonical element. If I do not replace the entire directive, I will need to decide which attributes (or all) should be removed in the directive because they were moved to another element. This may be a mistake if you missed something (and suddenly get the same id twice).

Solution # 2: use prefix attributes

Similar to # 1, but the user decides whether to use the attribute for the directive or for certain elements. It might look like this:

 <x-menu-item li-ng-hide="bar" button-ng-click="foo()">Open</x-menu-item> <!-- → possible translation --> <li class="menu-list-item" ng-hide="bar"> <button type="button" class="menu-button" ng-click="foo()">Open</button> </li> 

Disadvantages: it becomes more detailed, but provides more flexibility. For instance. the developer can create a user id for the directive, li and button. But what about ng-disabled ? Should the developer post button-ng-disabled as well as li-ng-class ? This is cumbersome and error prone. Therefore, we will probably have to handle these cases manually again ...

Solution # 3: use two directives

If we cannot decide how to handle our attributes, we can introduce two directives. Thus, we do not introduce artificially prefix attributes.

 <x-menu-item ng-hide="bar"> <x-menu-button ng-click="foo()">Open</x-menu-button> </x-menu-item> <!-- → possible translation --> <li class="menu-list-item" ng-hide="bar"> <button type="button" class="menu-button" ng-click="foo()">Open</button> </li> 

Disadvantages: it is not very dry and therefore prone to errors. I always have an <x-menu-button> in my <x-menu-item> . There will never be an empty <x-menu-item> or <x-menu-item> with another child. I also have the same problem with ng-disabled as in solution # 2. The developer should be able to easily deactivate my entire <x-menu-item> . It should not add a specific ng-class style for styling and disable the button itself.

Solution # 4: use a common interface

Limit your interface. Instead of trying to remain universal (which is nice, but cumbersome), you should limit its interface. Instead of special handling for ng-disabled , ng-hide and ng-click try to identify your common use cases and suggest a more convenient interface for using them. Thus, we process explicitly defined attributes in a special way.

 <x-menu-item hidden="bar" action="foo()" disabled="baz">Open</x-menu-item> <!-- → possible translation --> <li class="menu-list-item" ng-show="bar" ng-class="{ 'is-disabled': baz }"> <button type="button" class="menu-button" ng-click="foo()" ng-disabled="baz">Open</button> </li> 

Disadvantages: this approach is not very intuitive. Every Angular developer knows ng-click . No one knows my action attribute.

(partially) Solution # 5: DOM proxy events

Instead of moving ng-click from directives to buttons, this sometimes can not be useful if directives listen for clicks on themselves and automatically start clicking on a button (or vice versa) - it depends on the use of the case).

Solution # 6: Dirty queries.

See @ gulin-serge for details. Brief explanation: "Decorating" existing ng-click directives with user logic if it is used on a specific element and does not allow the use of default behavior.

Disadvantages: each ng-click will be checked if it is used on a specific element, even if it is not. This check is a small invoice. You should also remove the default ng-click behavior, which may lead to unexpected behavior. For instance. The ngTouch corner ngTouch decorates each ng-click , so it also triggers a touch event. This is what should happen for <x-menu-item> , but now you will need to check if ngTouch used manually, and if it is true, listen for touch events. This is a bug and does not scale. This “decoration step” is currently taking place in the link phase, which may have its own flaws: it would be difficult here to create an ng-class for an <li> dependent on ng-disabled . You will need to use $compile , which can have unexpected effects on its own. (For example, I used it on <select> and suddenly all <options> were duplicated. It can be difficult to debug.) Other directives have default behavior that is too useful for easing (for example, ng-class is an "animation" and installs utility classes such as ng-enter - this would not be enough to rebuild some custom element.addClass(cssClass) ).

(partially) Solution # 7: Use multiple templates.

Sometimes it’s enough to use several templates, which are selected depending on some attributes. This can happen inside the templateUrl function.

 <x-menu-item>Open</x-menu-item> <!-- → possible translation using "template-default.html" --> <li class="menu-list-item"> <button type="button" class="menu-button">Open</button> </li> 

Or:

 <x-menu-item disabled="baz">Open</x-menu-item> <!-- → possible translation using "template-disabled.html" --> <li class="menu-list-item" ng-class="{ 'is-disabled': baz }"> <button type="button" class="menu-button" ng-disabled="baz">Open</button> </li> 

Disadvantages: this is not very DRY. If you want to change the menu-list-item class, you need to do this in two templates. But it's nice to write templates in HTML again, and not like JavaScript strings. But it does not scale well if you have more variations. However, this may be your only solution, if not only some attributes change, but all the markup.

Solution # 8. Try to initialize each hidden directive with some default behavior (even if it's some noop ).

Maybe each specially crafted attribute can be initialized with some default value, even if this value does nothing. The default behavior can be overridden.

 <x-menu-item>Open</x-menu-item> <!-- → possible translation --> <!-- $scope.isDisbabled = has('ng-disabled') ? use('ng-disabled') : false --> <!-- $scope.action = has('ng-click') ? use('ng-click') : angular.noop --> <!-- $scope.isHidden = has('ng-hide') ? use('ng-hide') : false --> <li class="menu-list-item" ng-hide="isHidden" ng-class="{ 'is-disabled': isDisabled }"> <button type="button" class="menu-button" ng-disabled="isDisabled" ng-click="action()">Open</button> </li> 

Disadvantages: you initialize directives that are sometimes never used. This can be a performance issue. But this whole approach is relatively clean. This is currently my favorite decision.

Decision #?:???

...

+5
source share
2 answers

Possibly # 6. Dirty requests.

What if we use the definition of the directive as a query expression for the case when we need to disable the default implementation?

You write something like:

  .directive('ngClick', function() { return { restrict: 'A', priority: 100, // higher than default link: function(scope, element, attr) { // don't do that magic in other cases if (element[0].nodeName !== 'X-MENU-ITEM') return; element.bind('click', function() { // passthrough attr value to a controller/scope/etc }) // switch off default implementation based on attr value delete attr.ngClick; } }}) 

This will disable the standard ng-click implementation in your tags. Same job for ng-hide / ng-show / etc.

Yes, it looks awful in meaning, but the result is closer to your idea. Of course, this will slow down the compilation linking process.


PS According to your list, I prefer # 2, but with a custom directive namespace. Sort of:

 <app-menu-item app-click="..." app-hide="..."/> 

And add convention to the docs to use the app prefix for all user things and behavior. Where app usually an abbreviation of the project name.

+1
source

Update: rewrite based on comments

This is largely rewritten based on the explanation in the comments. The Polymer Elements idea gives developers an easy way to accomplish two tasks. The first is the ability to create simple elements with previously unprogrammed functions. The second is the ability to create complex controls with simple user markup.

This differs significantly from Angular, which combines functionality into directives that translate native markup into native markup and combines functionality with translated markup.

Polymer notes:

Polymer is primarily a polyfill infrastructure until web components, shadow DOMs, cloud CSS, and HTML import are standardized and implemented. HTML templates are already well implemented in modern browsers and deserve attention. However, Polymer will not just go away when they are standard, as it provides convenient functions and attributes, as well as a good set of components to get you started. Poly regiments are considered slower than native support, but this is to be expected.

Differences between polymers and Angular:

Unlike Angular, when you create an element, you do not translate it from one markup to another. In fact, you define its functionality and presentation. Despite the fact that it can provide additional markup in its Templates and the Shadow DOM, this additional markup is all functional elements (both user and native).

It also means that your elements can have their own CSS classes and identifiers that are handled by the browser, making it easy to distinguish between presentations. Selector engines will receive the actual user element and can get related properties and methods.

Simple demo:

 <polymer-element name="x-menu"> <template> <style> /* Scoped Style rules */ </style> <content></content> </template> <script> /* Registers the Element with the browser. */ Polymer('x-menu', { // Additional Element properties and methods }); </script> </polymer-element> <polymer-element name="x-menuitem"> <template> <style> /* Scoped style rules */ </style> <button type="button" class="{{parentElement.classList}}"> <content></content> </button> </template> <script> /* Registers the element with the browser */ Polymer('x-menuitem', { // Additional Element properties and methods }); </script> </polymer-element> 

Actual use

 <x-menu class="cool"> <x-menuitem>Open</x-menuitem> <x-menuitem>Edit</x-menuitem> <x-menuitem>Create</x-menuitem> </x-menu> 

When you really run this, you will see that the button actually copies the classList from the parentElement element. In other words: {{parentElement.classList}} is actually abbreviated for this.parentElement.classList . With this in mind, you can build several amd functions based on parent markup. On the contrary, the opposite can be done. You can also use document.querySelector('x-menu') and you will get <x-menu> .

In addition, since these are attributes that apply only to your own custom element, you do not need to worry about importing attributes. No other element will understand your attributes, and the browser will not try to do anything funny with them.

Update: applying polymer to your needs

First and foremost, using your example above, if you have two elements, then you will have two custom elements. How connected they are depends a lot on how they are programmed. You need to understand that for your own elements you need only your own elements if you need several types of functionality or presentation. At first, I would recommend not using your own elements, except when you need to style. With Polymer, it's usually best to start small ...

For your functional example, there is no real need to have anything other than content if you do not need: a) several stylization blocks or b) to integrate the functions of another element or component. Since you need a click function and the ability to focus / style, you need to add a button most of all. The rest can be easily processed in CSS.

For reasons that a component should handle multiple elements, it is important because this is only true when you need multiple elements. A simple element is to expand the set of HTML elements for your needs. Just worry about managing other elements (both regular and native) when you need an advanced component.

Update: facade elements

A polymer provides several mechanisms for an element to have multiple representations or a function based on properties. The simplest mechanism is a template binding, which modifies the corresponding fragment of a document based on conditions.

 <polymer-element name="x-menuitem"> <template> <template if="{{condition}}"> <style>Uses this set of styles</style> </template> <template if="{{condition2 OR !condition}}"> <style>Use this stylesheet instead</style> </template> <content></content> </template> <script>Polymer Registration</script> </polymer-element> 

Any number of conditions can exist, and since <template> is just an element, you can put any elements there, including more <template> bindings.

Update: two examples with disabled

In the first example, we just make <x-menu-item> with the ability to disable it. We will use the main element without <li> or <button> . To disable an element, you can either set the attribute directly (when you marked it, or get the element through a selector request and set the disabled property.

 <polymer-element name="x-menuitem"> <template> <style> :host { color:blue; } :host([disabled]) { color:red; } </style> <content><content> </template> <script> Polymer('x-menu-item', { publish: { disabled: { value: false, reflect: true } }, method: function() { if (this.disabled) return; } }); </script> </polymer-element> 

In our second example, we will use <x-menu-item> with child <li> and <button> . This will have a conditional template binding so that it will be displayed using different classes. It can still be disabled in the same way as above.

 <polymer-element name="x-menuitem"> <template> <style> :host #item { /* styles */ } :host([disabled]) #item { /* styles */ } </style> <template if="{{disabled}}"> <li id="item" class="disabled_class1"> <button class="disabled_class2"> <content><content> </button> </li> </template> <template if="{{!disabled}}"> <li id="item" class="enabled_class1"> <button class="enabled_class2"> <content><content> </button> </li> </template> </template> <script> Polymer('x-menu-item', { publish: { disabled: { value: false, reflect: true } }, method: function() { if (this.disabled) return; } }); </script> </polymer-element> 

Last update:

Keep in mind that the above examples are just two declarative ways to accomplish what you need. There are other declarative methods for achieving the same goal. In addition, you can always use ES / JS to fully define everything, providing even more features.

Polymer has a lot, which corresponds to the default browser behavior for HTML elements. This does not mean that there is nothing to know. This answer was just to demonstrate one specific desired functionality of your question. Since the development process and terminology are so different, I was not sure what else to solve. Anything that may be missing may be added to the response in updates; just let me know what exactly you would like to add.

0
source

Source: https://habr.com/ru/post/1212081/


All Articles