ng-content: De skjulte dokumenter

Ansvarsfraskrivelse: Denne artikel handler om Angular i modsætning til AngularJS. Dette betyder, at det gælder Angular 2.x, 4.x og forhåbentlig fremtidige versioner.

Hvis du nogensinde har prøvet at skrive en genanvendelig komponent i Angular, skulle du sandsynligvis projicere indhold inde i det. Du opdagede , fandt et par blogindlæg om det og fik din komponent til at fungere. Denne artikel leder dig gennem forældrene og sager om avanceret brug til indholdsprojektion til at besvare spørgsmål, der fortsat dukker op i Clarity-teamet og tilsyneladende også på Angular's GitHub-arkiv.

Normalt ville jeg starte denne artikel med at henvise dig til den officielle dokumentation af den funktion, jeg beskriver, men til indholdsprojektion findes den ikke endnu ... Så lad os springe lige ind!

Et simpelt eksempel

Vi vil bruge et enkelt eksempel i hele denne artikel, der viser forskellige måder at projicere indhold og forskellige kanter på. Da mange af spørgsmålene er relateret til komponentens livscyklus i Angular, vil vores hovedkomponent have en tæller, der viser antallet af gange, den er blevet instantieret:

Vi vil bruge denne Counter-komponent og projicere den på enhver måde, vi kan tænke på i en eller anden variation af denne Wrapper-komponent, som bare projicerer den til en stylet kasse:

Lad os bare kontrollere dette arbejde som forventet ved at sætte tre tællere inde i indpakningen:

Det viser 1, 2 og 3 som forventet. Så langt så godt.

Fra nu af tester vi med en enkelt tæller for enkelhed. Så vores standardapps HTML fremover vil være:

Målrettet projektion

Undertiden ønsker du, at forskellige børn af dit indpakning skal projiceres i forskellige dele af din skabelon. For at håndtere dette understøtter en valgt attribut, der giver dig mulighed for at projicere specifikt indhold på bestemte steder. Denne attribut tager en CSS-vælger (min-element, .my-klasse, [min-attribut], ...) for at matche de børn, du ønsker. Hvis du inkluderer et ng-indhold uden en valgt attribut, fungerer det som en opsamling og vil modtage alle børn, der ikke matchede nogen af ​​de andre ng-indholdselementer. Kort fortalt:

Tælleren projiceres korrekt i den anden, blå boks, mens barnet, der ikke er en tæller, havner i den samlede røde boks. Bemærk, at det målrettede ng-indhold har forrang for fangsten, selvom det er efter det i skabelonen.

ngProjectAs

Nogle gange er din indre komponent skjult i en anden større komponent. Nogle gange skal du bare pakke den ind i en ekstra beholder for at anvende ngIf eller ngSwitch. Uanset hvad der sker, sker det ofte, at din indre komponent ikke er et direkte barn af indpakningen. For at simulere det, lad os bare indpakke vores tællerkomponent i en og se, hvad der sker med vores målprojektion:

Vores tællerkomponent projiceres nu i det røde opsamlingselement, fordi ng-containeren omkring det ikke længere matcher select = "tælleren". For at afhjælpe dette er vi nødt til at bruge attributten ngProjectAs, som kan sættes på absolut ethvert element og lader det "skjule" ethvert element til indholdsprojektionsformål. Det tager nøjagtigt samme type vælgere som select attributten på .

Så hvis vi holder vores indpakning det samme som før (med de blå og røde felter), kan vi nu bruge denne nye attribut i vores app:

Tælleren er tilbage i den blå boks, ligesom vi ønskede.

Tid til at pirke og prod

Ok, vi har de enklere sager til at fungere. Men hvad sker der, hvis vi tænker uden for boksen (blink blink)? Lad os starte med et simpelt eksperiment: sæt to blokke i vores skabelon uden valg. Hvad skal der ske? Vil vi ende med to tællere eller kun en? Hvis vi ender med to, viser de 1 og 1 eller 1 og 2?

Svaret er, at vi får en enkelt tæller i den sidste , den anden er tom! Lad os eksperimentere lidt mere, før vi prøver at forklare hvorfor. Vi kommer langt fra det eksempel, der genererede flest spørgsmål til GitHub: hvad nu hvis jeg pakker min i en * ngIf?

Ved første øjekast ser det ud til at fungere fint. Men hvis du tænder og slukker for det med knappen, vil du bemærke, at tælleren ikke øges. Dette betyder, at vores tællerkomponent er øjeblikkelig instantieret - aldrig ødelagt og genskabt. Er det ikke det modsatte af, hvad * ng Hvis vi skal gøre? Lad os tjekke med * ngFor at se, om vi har det samme problem:

Samme ting som vores multiple sag, kun den sidste får en tæller! Hvorfor fungerer det ikke som vi forventede?

Forklaringen

producerer ikke indhold, det projicerer blot eksisterende indhold. Tænk på det som en variation af node.appendChild (el) eller den velkendte JQuery-version $ (node) .append (el): med disse metoder er noden ikke klonet, den flyttes blot til sin nye placering. På grund af dette er livscyklussen for det projicerede indhold bundet til det sted, hvor det er deklareret, ikke hvor det vises.

Der er to grunde til denne opførsel: konsekvens af forventninger og ydeevne. Hvad "konsekvens af forventninger" betyder, er, at jeg som udvikler kan læse min apps kode og gætte dens opførsel baseret på den kode, jeg har skrevet. Lad os sige, at jeg skrev dette stykke kode:

Det er klart, tælleren kommer til at blive øjeblikkelig. Men nu, lad os sige i stedet for min statiske indpakning, bruger jeg en fra et tredjepartsbibliotek:

Hvis tredjepartsbiblioteket havde evnen til at kontrollere min tællers livscyklus, ville jeg ikke have nogen måde at vide, hvor mange gange det er blevet instantieret. Den eneste måde for mig at vide det var at se på koden til tredjepartsbiblioteket og være prisgunstig med alle interne ændringer, de foretager. At håndhæve livscyklussen, der skal være bundet til min app-komponent i stedet for indpakningens midler, kan jeg med sikkerhed antage, at min tæller bliver instantieret en gang uden at vide noget om den faktiske kode på tredjepartsbiblioteket.

Ydelsesdelen er meget mere indlysende. Da ng-indhold kun flytter elementer, kan det gøres på kompileringstid i stedet for kørselstid, hvilket markant reducerer arbejdet med den faktiske applikation (især når man kompilerer forud for tid, hvilket vinkel-cli gør som standard).

Løsningen

For at lade indpakningen kontrollere øjeblikkeligheden af ​​dets børn, er vi nødt til at give det en skabelon til indholdet i stedet for selve indholdet. Dette kan gøres på to måder: ved hjælp af -elementet omkring vores indhold eller ved hjælp af et strukturelt direktiv med "stjerne" -syntaks, som * myContent. For at gøre det lettere skal vi bruge -syntaksen i vores eksempler, men du kan finde alle de oplysninger, du har brug for om stjernepræfikset her. Vores nye app ser sådan ud:

Indpakningen kan ikke bruge mere, da den modtager en skabelon. Den skal have adgang til skabelonen med @ContentChild og bruge ngTemplateOutlet til at vise den:

Vores tæller øges nu korrekt, hver gang vi skjuler den og viser den igen! Lad os prøve igen med * ngFor:

En tæller i hver boks, der viser 1, 2 og 3. Præcis hvad vi ledte efter!

Forhåbentlig vil disse forklaringer snart komme i selve vinkeldokumentationen, men i mellemtiden håber jeg, at dette dybe dykk i har besvaret de fleste af dine spørgsmål. Især forklarer det, hvorfor vinkelbiblioteker anmoder om skabeloner så ofte i modsætning til bare projicerende skabeloner. Det gør API'et lidt mere ordlyd, men det åbner mange flere muligheder for dem: doven-indlæsning af indholdet af faner, duplikering af titler forskellige steder i en komponent osv.

Hvis du er nysgerrig og føler dig som en gal videnskabsmand i dag, så gå videre og prøv mere avancerede eksperimenter som at projicere indhold inde i et . Resultaterne stemmer alle sammen med den foregående forklaring, men nogle af disse vanskelige mønstre har nyttige anvendelser. Måske kommer vi til dem i et fremtidig blogindlæg!