ng-content: de verborgen documenten

Disclaimer: dit artikel gaat over Angular, in tegenstelling tot AngularJS. Dit betekent dat het van toepassing is op Angular 2.x, 4.x en hopelijk toekomstige versies.

Als je ooit hebt geprobeerd een herbruikbaar onderdeel in Angular te schrijven, moest je waarschijnlijk de inhoud erin projecteren. Je hebt ontdekt, er een paar blogposts over gevonden en je component werkt. Dit artikel leidt je door de eigenaardigheden en geavanceerde gebruikstoepassingen voor inhoudsprojectie om vragen te beantwoorden die steeds opduiken in het Clarity-team en blijkbaar ook in Angular's GitHub-repository.

Normaal zou ik dit artikel beginnen door u te wijzen op de officiële documentatie van de functie die ik beschrijf, maar voor inhoudsprojectie bestaat deze nog niet ... Dus laten we meteen beginnen!

Een eenvoudig voorbeeld

We zullen in dit artikel een enkel voorbeeld gebruiken, dat verschillende manieren toont om inhoud en verschillende randgevallen te projecteren. Omdat veel van de vragen betrekking hebben op de levenscyclus van componenten in Angular, heeft onze hoofdcomponent een teller die het aantal keren weergeeft dat deze is geïnstantieerd:

We zullen dit Counter-onderdeel gebruiken en het op elke manier die we kunnen bedenken in een variatie van dit Wrapper-onderdeel projecteren, dat het gewoon in een gestileerde doos projecteert:

Laten we deze werkzaamheden gewoon controleren zoals verwacht, door drie tellers in de verpakking te plaatsen:

Het geeft 1, 2 en 3 weer zoals verwacht. Tot nu toe, zo goed.

Vanaf nu zullen we voor de eenvoud met één teller testen. Dus de standaard HTML van onze app is:

Gerichte projectie

Soms wilt u dat verschillende kinderen van uw verpakking in verschillende delen van uw sjabloon worden geprojecteerd. Om dit aan te pakken, ondersteunt een select attribuut waarmee u specifieke inhoud op specifieke plaatsen kunt projecteren. Voor dit kenmerk is een CSS-selector (my-element, .my-class, [my-attribuut], ...) nodig die overeenkomt met de gewenste kinderen. Als u een ng-inhoud zonder select attribuut opneemt, zal het dienen als een verzamelobject en alle kinderen ontvangen die niet overeenkwamen met een van de andere ng-inhoudselementen. Om een ​​lang verhaal kort te maken:

De teller wordt correct geprojecteerd in de tweede, blauwe doos, terwijl het kind dat geen teller is in de catch-all rode doos belandt. Merk op dat de getargete ng-inhoud voorrang heeft op de catch-all, zelfs als deze er in de sjabloon achter staat.

ngProjectAs

Soms is je innerlijke component verborgen in een andere grotere component. Soms moet je het gewoon in een extra container wikkelen om ngIf of ngSwitch toe te passen. Om welke reden dan ook, het gebeurt vaak dat je innerlijke component geen direct kind van de verpakking is. Om dat te simuleren, laten we onze Counter-component gewoon in een wikkelen en kijken wat er met onze doelprojectie gebeurt:

Onze Counter-component wordt nu geprojecteerd in het rode catch-all element, omdat de ng-container eromheen niet meer overeenkomt met de select = "counter". Om dit te verhelpen, moeten we het attribuut ngProjectAs gebruiken, dat op elk willekeurig element kan worden geplaatst en waarmee elk element kan worden "vermomd" voor projectiedoeleinden. Het heeft exact dezelfde soort selectors nodig als het select attribuut op .

Dus om onze wikkel hetzelfde te houden als voorheen (met de blauwe en rode vakken), kunnen we nu dit nieuwe kenmerk in onze app gebruiken:

De balie is terug in de blauwe doos, net zoals we wilden.

Tijd om te porren en te porren

Ok, we hebben de eenvoudigere zaken aan het werk. Maar wat gebeurt er als we buiten de kaders denken (knipoog)? Laten we beginnen met een eenvoudig experiment: plaats twee blokken in onze sjabloon zonder selectors. Wat zou er moeten gebeuren? Zullen we eindigen met twee tellers of slechts één? Als we met twee eindigen, geven ze dan 1 en 1 of 1 en 2 weer?

Het antwoord is dat we een enkele teller krijgen in het laatste , de andere is leeg! Laten we wat meer experimenteren voordat we proberen uit te leggen waarom. We komen bij het voorbeeld dat verreweg de meeste vragen op GitHub genereerde: wat als ik mijn in een * ngIf wikkel?

Op het eerste gezicht lijkt het prima te werken. Maar als je het aan en uit zet met de knop, zul je merken dat de teller niet toeneemt. Dit betekent dat onze Counter-component eenmalig wordt geïnstantieerd - nooit vernietigd en opnieuw gemaakt. Is dat niet het tegenovergestelde van wat * ngA zou moeten doen? Laten we controleren met * ngFor om te zien of we hetzelfde probleem hebben:

Hetzelfde als ons meervoudige geval, alleen de laatste krijgt een teller! Waarom werkt het niet zoals we hadden verwacht?

De toelichting

produceert geen inhoud, het projecteert gewoon bestaande inhoud. Zie het als een variatie op node.appendChild (el) of de bekende JQuery-versie $ (node). Append (el): met deze methoden wordt de knoop niet gekloond, maar eenvoudig naar zijn nieuwe locatie verplaatst. Hierdoor is de levenscyclus van de geprojecteerde inhoud gebonden aan waar het wordt aangegeven, niet waar het wordt weergegeven.

Er zijn twee redenen voor dit gedrag: consistentie van verwachtingen en prestaties. Wat "consistentie van verwachtingen" betekent, is dat ik als ontwikkelaar de code van mijn app kan lezen en het gedrag kan raden op basis van de code die ik heb geschreven. Laten we zeggen dat ik dit stukje code heb geschreven:

Het is duidelijk dat de teller eenmaal wordt geïnstantieerd. Maar laten we zeggen, in plaats van mijn statische wrapper, gebruik ik er een uit een externe bibliotheek:

Als de externe bibliotheek de mogelijkheid had om de levenscyclus van mijn teller te regelen, zou ik niet weten hoe vaak deze is geïnstantieerd. De enige manier voor mij om te weten is om naar de code van de externe bibliotheek te kijken en overgeleverd te zijn aan interne wijzigingen die ze aanbrengen. Het afdwingen van de levenscyclus om te worden gebonden aan mijn app-component in plaats van de wrapper betekent dat ik er veilig van kan uitgaan dat mijn teller eenmalig wordt geïnstantieerd, zonder iets te weten over de werkelijke code van de externe bibliotheek.

Het uitvoeringsgedeelte is veel duidelijker. Omdat ng-content alleen elementen verplaatst, kan dit worden gedaan tijdens het compileren in plaats van tijdens het uitvoeren, wat het werk van de daadwerkelijke toepassing aanzienlijk vermindert (vooral bij het vooraf compileren, wat angular-cli standaard doet).

De oplossing

Om de wrapper de instantiatie van zijn kinderen te laten regelen, moeten we hem een ​​sjabloon voor de inhoud geven, in plaats van de inhoud zelf. Dit kan op twee manieren: met behulp van het element rondom onze inhoud, of met een structurele richtlijn met de syntaxis "star", zoals * myContent. Voor de eenvoud gebruiken we de syntaxis in onze voorbeelden, maar u kunt hier alle informatie vinden over het stervoorvoegsel. Onze nieuwe app ziet er zo uit:

De wrapper kan niet meer gebruiken, omdat deze een sjabloon ontvangt. Het moet de sjabloon openen met @ContentChild en ngTemplateOutlet gebruiken om het weer te geven:

Onze teller wordt nu correct verhoogd telkens we hem verbergen en opnieuw tonen! Laten we het opnieuw proberen met * ngFor:

Eén teller in elk vak, met 1, 2 en 3. Precies wat we zochten!

Hopelijk staan ​​deze verklaringen binnenkort in de Angular-documentatie zelf, maar in de tussentijd hoop ik dat deze diepe duik in de meeste van je vragen zal hebben beantwoord. In het bijzonder wordt uitgelegd waarom Angular Libraries zo vaak sjablonen aanvragen in plaats van alleen sjablonen te projecteren. Het maakt de API wel iets uitgebreider, maar het biedt veel meer mogelijkheden voor hen: lui laden van de inhoud van tabbladen, dupliceren van titels op verschillende plaatsen van een component, etc.

Als je nieuwsgierig bent en je vandaag een gekke wetenschapper voelt, ga je gang en probeer geavanceerdere experimenten, zoals het projecteren van inhoud in een . De resultaten zijn allemaal consistent met de vorige uitleg, maar sommige van deze lastige patronen hebben nuttige toepassingen. Misschien komen we ze in een toekomstige blogpost tegen!