One place for hosting & domains

      Components

      How To Create Reusable Blocks of Code with Vue Single-File Components


      The author selected Open Sourcing Mental Illness to receive a donation as part of the Write for DOnations program.

      Introduction

      When creating a web application using Vue.js, it’s a best practice to construct your application in small, modular blocks of code. Not only does this keep the parts of your application focused, but it also makes the application easier to update as it grows in complexity. Since an app generated from the Vue CLI requires a build step, you have access to a Single-File Components (SFC) to introduce modularity into your app. SFCs have the .vue extension and contain an HTML <template>, <script>, and <style> tags and can be implemented in other components.

      SFCs give the developer a way to create their own HTML tags for each of their components and then use them in their application. In the same way that the <p> HTML tag will render a paragraph in the browser, and hold non-rendered functionality as well, the component tags will render the SFC wherever it is placed in the Vue template.

      In this tutorial, you are going to create a SFC and use props to pass data down and slots to inject content between tags. By the end of this tutorial, you will have a general understanding of what SFCs are and how to approach code re-usability.

      Prerequisites

      Step 1 — Setting Up the Project

      In this tutorial, you are going to be creating an airport card component that displays a number of airports and their codes in a series of cards. After following the Prerequisites section, you will have a new Vue project named sfc-project. In this section, you will import data into this generated application. This data will be an array of objects consisting of a few properties that you will use to display information in the browser.

      Once the project is generated, open your terminal and cd or change directory into the root src folder:

      From there, create a new directory named data with the mkdir command, then create a new file with the name us-airports.js using the touch command:

      • mkdir data
      • touch data/us-airports.js

      In your text editor of choice, open this new JavaScript file and add in the following local data:

      sfc-project/data/us-airports.js

      export default [
        {
          name: 'Cincinnati/Northern Kentucky International Airport',
          abbreviation: 'CVG',
          city: 'Hebron',
          state: 'KY'
        },
        {
          name: 'Seattle-Tacoma International Airport',
          abbreviation: 'SEA',
          city: 'Seattle',
          state: 'WA'
        },
        {
          name: 'Minneapolis-Saint Paul International Airport',
          abbreviation: 'MSP',
          city: 'Bloomington',
          state: 'MN'
        }
      ]
      

      This data is an array of objects consisting of a few airports in the United States. This will be rendered in a Single-File Component later in the tutorial.

      Save and exit the file.

      Next, you will create another set of airport data. This data will consist of European airports. Using the touch command, create a new JavaScript file named eu-airports.js:

      • touch data/eu-airports.js

      Then open the file and add the following data:

      sfc-project/data/eu-airports.js

      export default [
        {
          name: 'Paris-Charles de Gaulle Airport',
          abbreviation: 'CDG',
          city: 'Paris',
          state: 'Ile de France'
        },
        {
          name: 'Flughafen München',
          abbreviation: 'MUC',
          city: 'Munich',
          state: 'Bavaria'
        },
        {
          name: 'Fiumicino "Leonardo da Vinci" International Airport',
          abbreviation: 'FCO',
          city: 'Rome',
          state: 'Lazio'
        }
      ]
      

      This set of data is for European airports in France, Germany, and Italy, respectively.

      Save and exit the file.

      Next, in the root directory, run the following command in your terminal to start your Vue CLI application running on a local development server:

      This will open the application in your browser on localhost:8080. The port number may be different on your machine.

      Visit the address in your browser. You will find the following start up screen:

      Vue default template page

      Next, start a new terminal and open your App.vue file in your src folder. In this file, delete the img and HelloWorld tags in the <template> and the components section and import statement in the <script>. Your App.vue will resemble the following:

      sfc-project/src/App.vue

      <template>
      
      </template>
      
      <script>
      export default {
        name: 'App',
      }
      </script>
      
      <style>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      </style>
      

      After this, import the us-airports.js file that you created earlier. In order to make this data reactive so you can use it in the <template>, you need to import the ref function from vue. You will need to return the airport reference so the data can be used in the HTML template.

      Add the following highlighted lines:

      sfc-project/src/App.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation" class="card">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </div>
        </div>
      </template>
      
      <script>
      import { ref } from 'vue'
      import data from '@/data/us-airports.js'
      
      export default {
        name: 'App',
        setup() {
          const airports = ref(data)
      
          return { airports }
        }
      }
      </script>
      ...
      

      In this snippet, you imported the data and rendered it using <div> elements and the v-for directive in the template.

      At this point, the data is imported and ready to be used in the App.vue component. But first, add some styling to make the data easier for users to read. In this same file, add the following CSS in the <style> tag:

      sfc-project/src/App.vue

      ...
      <style>
      #app { ... }
      
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .card p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .card p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      In this case, you are using CSS Grid to compose these cards of airport codes into a grid of three. Notice how this grid is set up in the .wrapper class. The .card class is the card or section that contains each airport code, name, and location. If you would like to learn more about CSS, check out our How To Style HTML with CSS.

      Open your browser and navigate to localhost:8080. You will find a number of cards with airport codes and information:

      Three airport cards rendering the data for the US airports dataset

      Now that you have set up your initial app, you can refactor the data into a Single-File Component in the next step.

      Step 2 — Creating a Single-File Component

      Since Vue CLI uses Webpack to build your app into something the browser can read, your app can use SFCs or .vue files instead of plain JavaScript. These files are a way for you to create small blocks of scalable and reusable code. If you were to change one component, it would be updated everywhere.

      These .vue components usually consist of these three things: <template>, <script>, and <style> elements. SFC components can either have scoped or unscoped styles. When a component has scoped styles, that means the CSS between the <style> tags will only affect the HTML in the <template> in the same file. If a component has un-scoped styles, the CSS will affect the parent component as well as its children.

      With your project successfully set up, you are now going to break these airport cards into a component called AirportCards.vue. As it stands now, the HTML in the App.vue is not very reusable. You will break this off into its own component so you can import it anywhere else into this app while preserving the functionality and visuals.

      In your terminal, create this .vue file in the components directory:

      • touch src/components/AirportCards.vue

      Open the AiportCards.vue component in your text editor. To illustrate how you can re-use blocks of code using components, move most of the code from the App.vue file to the AirportCards.vue component:

      sfc-project/src/components/AirportCards.vue

      <template>
        <div class="wrapper">
          <div v-for="airport in airports" :key="airport.abbreviation" class="card">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </div>
        </div>
      </template>
      
      <script>
      import { ref } from 'vue'
      import data from '@/data/us-airports.js'
      
      export default {
        name: 'Airports',
        setup() {
          const airports = ref(data)
      
          return { airports }
        }
      }
      </script>
      
      <style scoped>
      .wrapper {
        display: grid;
        grid-template-columns: 1fr 1fr 1fr;
        grid-column-gap: 1rem;
        max-width: 960px;
        margin: 0 auto;
      }
      
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      
      .card p:first-child {
        font-weight: bold;
        font-size: 2.5rem;
        margin: 1rem 0;
      }
      
      .card p:last-child {
        font-style: italic;
        font-size: .8rem;
      }
      </style>
      

      Save and close the file.

      Next, open your App.vue file. Now you can clean up the App.vue component and import AirportCards.vue into it:

      sfc-project/src/App.vue

      <template>
        <AirportCards />
      </template>
      
      <script>
      import AirportCards from '@/components/Airports.vue'
      
      export default {
        name: 'App',
        components: {
          AirportCards
        }
      }
      </script>
      
      <style scoped>
      #app {
        font-family: Avenir, Helvetica, Arial, sans-serif;
        -webkit-font-smoothing: antialiased;
        -moz-osx-font-smoothing: grayscale;
        text-align: center;
        color: #2c3e50;
        margin-top: 60px;
      }
      </style>
      

      Now that AirportCards is a standalone component, you have put it in the <template> HTML as you would a <p> tag.

      When you open up localhost:8080 in your browser, nothing will change. The same three airport cards will still display, because you are rendering the new SFC in the <AirportCards /> element.

      Next, add this same component in the template again to illustrate the re-usability of components:

      /src/App.vue

      <template>
        <AirportCards />
        <airport-cards />
      </template>
      ...
      

      You may notice that this new instance of AirportCards.vue is using kebab-case over PascalCase. When referencing components, Vue does not care which one you use. All capitalized words and letters will be separated by a hyphen and will be lower case. The same applies to props as well, which will be explained in the next section.

      Note: The case that you use is up to personal preference, but consistency is important. Vue.js recommends using kebab-case as it follows the HTML standard.

      Open the browser and visit localhost:8080. You will find the cards duplicated:

      The US airport cards rendered twice in the browser.

      This adds modularity to your app, but the data is still static. The row of cards is useful if you want to show the same three airports, but changing the data source would require changing the hard-coded data. In the next step, you are going to expand this component further by registering props and passing data from the parent down to the child component.

      Step 3 — Leveraging Props to Pass Down Data

      In the previous step, you created an AirportCards.vue component that rendered a number of cards from the data in the us-airports.js file. In addition to that, you also doubled the same component reference to illustrate how you can easily duplicate code by adding another instance of that component in the <template>.

      However, leaving the data static will make it difficult to change the data in the future. When working with SFCs, it can help if you think of components as functions. These functions are components that can take in arguments (props) and return something (HTML). For this case, you will pass data into the airports parameter to return dynamic HTML.

      Open the AirportCards.vue component in your text editor. You are currently importing data from the us-airports.js file. Remove this import statement, as well as the setup function in the <script> tag:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      
      export default {
        name: 'Airports',
      }
      </script>
      ...
      

      Save the file. At this point, nothing will render in the browser.

      Next, move forward by defining a prop. This prop can be named anything; it just describes the data coming in and associates it with a name.

      To create a prop, add the props property in the component. The value of this is a series of key/value pairs. The key is the name of the prop, and the value is the description of the data. It’s best to provide as much description as you can:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      
      export default {
        name: 'Airports',
        props: {
          airports: {
            type: Array,
            required: true
          }
        }
      }
      </script>
      ...
      

      Now, in AirportCard.vue, the property airports refers to the data that is passed in. Save and exit from the file.

      Next, open the App.vue component in your text editor. Like before, you will need to import data from the us-airports.js file and import the ref function from Vue to make it reactive for the HTML template:

      sfc-project/src/App.vue

      <template>
        <AirportCards :airports="usAirports" />
        <airport-cards />
      </template>
      
      <script>
      import { ref } from 'vue'
      import AirportCards from '@/components/Airports.vue'
      import usAirportData from '@/data/us-airports.js'
      
      export default {
        name: 'App',
        components: {
          AirportCards
        },
        setup() {
          const usAirports = ref(usAirportData)
      
          return { usAirports }
        }
      }
      </script>
      

      If you open your browser and visit localhost:8080, you will find the same US airports as before:

      US airports rendered on cards in the browser

      There is another AirportCards.vue instance in your template. Since you defined props within that component, you can pass any data with the same structure to render a number of cards from different airports. This is where the eu-airports.js file from the initial setup comes in.

      In App.vue, import the eu-airports.js file, wrap it in the ref function to make it reactive, and return it:

      /src/App.vue

      <template>
        <AirportCards :airports="usAirports" />
        <airport-cards :airports="euAirports" />
      </template>
      
      <script>
      ...
      import usAirportData from '@/data/us-airports.js'
      import euAirportData from '@/data/eu-airports.js'
      
      export default {
        ...
        setup() {
          const usAirports = ref(usAirportData)
          const euAirports = ref(euAirportData)
      
          return { usAirports, euAirports }
        }
      }
      </script>
      ...
      

      Open your browser and visit localhost:8080. You will find the European airport data rendered below the US airport data:

      US airport data rendered as cards, followed by European airport data rendered in the same way.

      You have now successfully passed in a different dataset into the same component. With props, you are essentially re-assigning data to a new name and using that new name to reference data in the child component.

      At this point, this application is starting to become more dynamic. But there is still something else that you can do to make this even more focused and re-usable. In Vue.js, you can use something called slots. In the next step, you are going to create a Card.vue component with a default slot that injects the HTML into a placeholder.

      Step 4 — Creating a General Card Component Using Slots

      Slots are a great way to create re-usable components, especially if you do not know if the HTML in that component will be similar. In the previous step, you created another AirportCards.vue instance with different data. In that example, the HTML is the same for each. It’s a <div> in a v-for loop with paragraph tags.

      Open your terminal and create a new file using the touch command. This file will be named Card.vue:

      • touch src/components/Card.vue

      In your text editor, open the new Card.vue component. You are going to take some of the CSS from AirportCards.vue and add it into this new component.

      Create a <style> tag and add in the following CSS:

      sfc-project/src/components/Card.vue

      <style>
      .card {
        border: 3px solid;
        border-radius: .5rem;
        padding: 1rem;
        margin-bottom: 1rem;
      }
      </style>
      

      Next, create the HTML template for this component. Before the <style> tag, add a <template> tag with the following:

      sfc-project/src/components/Card.vue

      <template>
        <div class="card">
      
        </div>
      </template>
      ...
      

      In between the <div class="card">, add the <slot /> component. This is a component that is provided to you by Vue. There is no need to import this component; it is globally imported by Vue.js:

      sfc-project/src/components/Card.vue

      <template>
        <div class="card">
          <slot />
        </div>
      </template>
      

      This slot is a placeholder for the HTML that is between the Card.vue component’s tags when it is referenced elsewhere.

      Save and exit the file.

      Now go back to the AirportCards.vue component. First, import the new Card.vue SFC you just created:

      sfc-project/src/components/AirportCards.vue

      ...
      <script>
      import Card from '@/components/Card.vue'
      
      export default {
        name: 'Airports',
        props: { ... },
        components: {
          Card
        }
      }
      </script>
      ...
      

      Now all that is left is to replace the <div> with <card>:

      /src/components/AirportCards.vue

      <template>
        <div class="wrapper">
          <card v-for="airport in airports" :key="airport.abbreviation">
            <p>{{ airport.abbreviation }}</p>
            <p>{{ airport.name }}</p>
            <p>{{ airport.city }}, {{ airport.state }}</p>
          </card>
        </div>
      </template>
      ...
      

      Since you have a <slot /> in your Card.vue component, the HTML between the <card> tags is injected in its place while preserving all styles that have been associated with a card.

      Save the file. When you open your browser at localhost:8080, you will find the same cards that you’ve had previously. The difference now is that your AirportCards.vue now reference the Card.vue component:

      US airport data rendered as cards, followed by European airport data rendered in the same way.

      To show the power of slots, open the App.vue component in your application and import the Card.vue component:

      sfc-project/src/App.vue

      ...
      <script>
      ...
      import Card from '@/components/Card.vue'
      
      export default {
        ...
        components: {
          AirportCards,
          Card
        },
        setup() { ... }
      }
      </script>
      ...
      

      In the <template>, add the following under the <airport-cards /> instances:

      sfc-project/src/App.vue

      <template>
        <AirportCards :airports="usAirports"/>
        <airport-cards :airports="euAirports" />
        <card>
          <p>US Airports</p>
          <p>Total: {{ usAirports.length }}</p>
        </card>
        <card>
          <p>EU Airports</p>
          <p>Total: {{ euAirports.length }}</p>
        </card>
      </template>
      ...
      

      Save the file and visit localhost:8080 in the browser. Your browser will now render additional elements displaying the number of airports in the datasets:

      US and European airports displayed, along with two cards that display that there are three airports in each dataset

      The HTML between the <card /> tags is not exactly the same, but it still renders a generic card. When leveraging slots, you can use this functionality to create small, re-usable components that have a number of different uses.

      Conclusion

      In this tutorial, you created single-file components and used props and slots to create reusable blocks of code. In the project, you created an AirportCards.vue component that renders a number of airport cards. You then broke up the AirportCards.vue component further into a Card.vue component with a default slot.

      You ended up with a number of components that are dynamic and can be used in a number of different uses, all while keeping code maintainable and in keeping with the D.R.Y. software principle.

      To learn more about Vue components, it is recommended to ready through the Vue documentation. For more tutorials on Vue, check out the How To Develop Websites with Vue.js series page.



      Source link

      Angular – Shortcut to Importing Styles Files in Components


      In a typical Angular project, you’ll have many components. Each components has it own stylesheet (css, scss, less, etc). It’s quite often that you might need to include global styling files (especially variables file) in your component.

      We’ve talked on this a bit in our other Angular styles article: Using Sass with the Angular CLI

      Let’s explore another option for importing style files:

      A Sass Variables Sample

      Let’s say you have a _variables.scss in your src/stylings folder:

      // your folder structure
      - src
          - app
              - app.component.ts
                  - hello
                      - hello.component.html
                      - hello.component.scss
                      - hello.component.ts
              - ...
          - stylings
              - _variables.scss
      
      // your _variables.scss file
      $brand-color: #800000;
      

      Reference to the Variables file

      Below is our hello.component.html file, let’s style the header with our brand-color.

      <!-- hello.component.html -->
      <h1>
        Hello World!
      </h1>
      

      The $brand-color variable is in stylings/_variables.scss file. We need to import the file in order to use it:

      // hello.component.scss
      @import "../../../stylings/variables"; // this is not cool!
      
      h1 {
          color: $brand-color;
      }
      

      See the ../../../stylings/ syntax? Do you like it?

      Imagine you need to repeat this ../../../stylings/ in another tens or hundreds of components and you need to remember the relative path. This is not cool. Let’s fix this!

      Shortcut with Angular CLI configuration

      If your project is generated with Angular CLI, you can add a configuration stylePreprocessorOptions > includePaths in .angular.cli.json file. This configuration allows you to add extra base paths that will be checked for imports. It tells Angular CLI to look for styling files in the mentioned paths before processing each component style file.

      For example, in our case, let’s add ./stylings in the paths. Since the configuration accept an array, you can add multiple paths.

      {
          ...
          "apps": [{
              "root": "src",
              ...
              "stylePreprocessorOptions": {
                  "includePaths": [
                    "./stylings"
                  ]
              }
      
          }]
      }
      

      With this, we can update our hello.component.scss to just @import "variables". Sweet!

      // hello.component.scss
      @import "variables"; // change to just variables, say goodbye to ../../../stylings/
      
      h1 {
          color: $brand-color;
      }
      

      What if you have duplicated file name in paths?

      Imagine you included two styling paths in .angular.cli.json, both have _variables.scss file. Guess what will happen? Will the CLI pick up both files or throw errors?

      Let’s test it out together!

      // your folder structure
      - src
          - ...
          - stylings
              - _variables.scss
          - stylings2 // add this
              - _variables.scss
      

      In stylings2/_variables.scss, you have the following styles,

      // stylings2/_variables.scss
      $brand-color: blue;
      $font-size-large: 40px;
      

      Update your .angular.cli.json configurations, to include styling2 folder path.

      {
          ...
          "apps": [{
              "root": "src",
              ...
              "stylePreprocessorOptions": {
                  "includePaths": [
                    "./stylings",
                    "./stylings2"
                  ]
              }
      
          }]
      }
      

      Update your hello.component.scss file,

      // hello.component.scss
      @import "variables";
      
      h1 {
          color: $brand-color;
          font-size: $font-size-large;
      }
      

      Restart your dev server. Wait for a while, and you should expect… Error!
      Error, Undefined variable

      Tell me why!

      Turn out, if there are multiple files with same name, Angular CLI will pick only the first file that match the name. In this case, it will pick the stylings/_variables.scss file. That’s why it could not get the variable $font-size-large, because it’s in styling2/_variables.scss file.

      But… I really need two files with the same name!

      Well, there are cases where you have multiple files with the same name and you really need it, and you would like to have shortcuts as well. The workaround would be including the parent path. For example, in our case, both stylings and stylings2 folders parent are src.

      We can update the .angular.cli.json configuration to the following:

      {
          ...
          "apps": [{
              "root": "src",
              ...
              "stylePreprocessorOptions": {
                  "includePaths": [
                    "."
                  ]
              }
      
          }]
      }
      

      Then, in your hello.component.scss, you can refer to both variables file like the following,

      // hello.component.scss
      @import "stylings/variables";
      @import "stylings2/variables"; 
      
      h1 {
          color: $brand-color;
          font-size: $font-size-large;
      }
      

      Well, it’s not perfect, slightly more words to type, but better than ../../../ right? Also, it might be rare scenario that you have multiple style files with same name in the same project, I guess?

      Another shorter workaround would be including parent path and one styling path:

      {
          ...
          "apps": [{
              "root": "src",
              ...
              "stylePreprocessorOptions": {
                  "includePaths": [
                    ".",
                    "./stylings"
                  ]
              }
      
          }]
      }
      

      You can save a few lines in your hello.component.scss.

      // hello.component.scss
      @import "variables"; // shorter, don't need styling/ as it's one of the configured paths
      @import "stylings2/variables"; 
      
      h1 {
          color: $brand-color;
          font-size: $font-size-large;
      }
      

      What about including paths in node_modules?

      The Angular CLI configuration is applicable to files in node_modules as well. Let’s say you are using your custom styling npm package, for example bootstrap-sass module.

      npm install bootstrap-sass --save
      

      Here is the folder structure of bootstrap-sass:

      - node_modules
          - bootstrap-sass
              - assets
                  - stylesheets
                      - bootstrap
                          - ...
                          - _grid.scss
                          - _variables.scss
      

      Let’s say you would like to use the bootstrap’s _variables.scss, you can update your .angular.cli.json file to include bootstrap path,

      {
          ...
          "apps": [{
              "root": "src",
              ...
              "stylePreprocessorOptions": {
                  "includePaths": [
                    ".",
                    "./stylings",
                    "../node_modules/bootstrap-sass/assets/stylesheets"
                  ]
              }
      
          }]
      }
      

      Then, in your hello.component.scss, you can refer to the bootstrap variables file like the following,

      // hello.component.scss
      @import "variables";
      @import "stylings2/variables"; 
      @impoer "bootstrap/variables";
      
      h1 {
          color: $brand-color;
          font-size: $font-size-large;
          font-family: $font-family-serif;
      }
      

      Summary

      Let’s remove the relative path hell (../../../) with this useful Angular CLI configuration!

      That’s it. Happy coding!



      Source link

      3 Ways to Pass Async Data to Angular 2+ Child Components


      Let’s start with a common use case. You have some data you get from external source (e.g. by calling API). You want to display it on screen.

      However, instead of displaying it on the same component, you would like to pass the data to a child component to display.

      The child component might has some logic to pre-process the data before showing on screen.

      Our Example

      For example, you have a blogger component that will display blogger details and her posts. Blogger component will gets the list of posts from API.

      Instead of writing the logic of displaying the posts in the blogger component, you want to reuse the posts component that is created by your teammate, what you need to do is pass it the posts data.

      The posts component will then group the posts by category and display accordingly, like this:

      blogger and posts

      Isn’t That Easy?

      It might look easy at the first glance. Most of the time we will initiate all the process during our component initialization time – during ngOnInit life cycle hook (refer here for more details on component life cycle hook).

      In our case, you might think that we should run the post grouping logic during ngOnInit of the posts component.

      However, because the posts data is coming from server, when the blogger component passes the posts data to posts component, the posts component ngOnInit is already fired before the data get updated. Your post grouping logic will not be fired.

      How can we solve this? Let’s code!

      Our Post Interfaces and Data

      Let’s start with interfaces.

      // post.interface.ts
      
      // each post will have a title and category
      export interface Post {
          title: string;
          category: string;
      }
      
      // grouped posts by category
      export interface GroupPosts {
          category: string;
          posts: Post[];
      }
      

      Here is our mock posts data assets/mock-posts.json.

      
      [
          { "title": "Learn Angular", "type": "tech" },
          { "title": "Forrest Gump Reviews", "type": "movie" },
          { "title": "Yoga Meditation", "type": "lifestyle" },
          { "title": "What is Promises?", "type": "tech" },
          { "title": "Star Wars Reviews", "type": "movie" },
          { "title": "Diving in Komodo", "type": "lifestyle" }
      ]
      
      

      Blogger Component

      Let’s take a look at our blogger component.

      // blogger.component.ts
      
      import { Component, OnInit, Input } from '@angular/core';
      import { Http } from '@angular/http';
      import { Post } from './post.interface';
      
      @Component({
          selector: 'bloggers',
          template: `
              <h1>Posts by: {{ blogger }}</h1>
              <div>
                  <posts [data]="posts"></posts>
              </div>
          `
      })
      export class BloggerComponent implements OnInit {
      
          blogger="Jecelyn";
          posts: Post[];
      
          constructor(private _http: Http) { }
      
          ngOnInit() { 
              this.getPostsByBlogger()
                  .subscribe(x => this.posts = x);
          }
      
          getPostsByBlogger() {
              const url="assets/mock-posts.json";
              return this._http.get(url)
                  .map(x => x.json());
          }
      }
      

      We will get our mock posts data by issuing a HTTP GET call. Then, we assign the data to posts property. Subsequently, we bind posts to posts component in our view template.

      Please take note that, usually we will perform HTTP call in service. However, since it’s not the focus of this tutorial (to shorten the tutorial), we will do that it in the same component.

      Posts Component

      Next, let’s code out posts component.

      // posts.component.ts
      
      import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
      import { BehaviorSubject } from 'rxjs/BehaviorSubject';
      import { Post, GroupPosts } from './post.interface';
      
      @Component({
          selector: 'posts',
          template: `
          <div class="list-group">
              <div *ngFor="let group of groupPosts" class="list-group-item">
                  <h4>{{ group.category }}</h4>
                  <ul>
                      <li *ngFor="let post of group.posts">
                          {{ post.title }}
                      </li>
                  </ul>
              <div>
          </div>
          `
      })
      export class PostsComponent implements OnInit, OnChanges {
      
          @Input()
          data: Post[];
      
          groupPosts: GroupPosts[];
      
          ngOnInit() {
          }
      
          ngOnChanges(changes: SimpleChanges) {
          }
      
          groupByCategory(data: Post[]): GroupPosts[] {
              // our logic to group the posts by category
              if (!data) return;
      
              // find out all the unique categories
              const categories = new Set(data.map(x => x.category));
      
              // produce a list of category with its posts
              const result = Array.from(categories).map(x => ({
                  category: x,
                  posts: data.filter(post => post.category === x)
              }));
      
              return result;
          }
      }
      

      We have an input called data which will receive the posts data from parent component. In our case, blogger component will provide that.

      You can see that we implement two interfaces OnInit and OnChanges. These are the lifecycle hooks that Angular provide to us. We have not done anything in both ngOnInit and ngOnChanges just yet.

      The groupByCategory function is our core logic to group the posts by category. After the grouping, we will loop the result and display the grouped posts in our template.

      Remember to import these components in you module (e.g. app.module.ts) and add it under declarations.

      Save and run it. You will see a pretty empty page with the blogger name only. That’s because we have not code our solution yet.

      Solution 1: Use *ngIf

      Solution one is the easiest. Use *ngIf in blogger component to delay the initialization of posts components. We will bind the post component only if the posts variable has a value. Then, we are safe to run our grouping logic in posts component ngOnInit.

      Our blogger component:

      // blogger.component.ts
      
      ...
          template: `
              <h1>Posts by: {{ blogger }}</h1>
              <div *ngIf="posts">
                  <posts [data]="posts"></posts>
              </div>
          `
      ...
      

      Our posts component.

      // posts.component.ts
      
      ...
          ngOnInit() {
              // add this line here
              this.groupPosts = this.groupByCategory(this.data);
          }
      ...
      

      A few things to note:

      • Since the grouping logic runs in ngOnInit, that means it will run only once. If there’s any future updates on data (passed in from blogger component), it won’t trigger again.
      • Therefore, if someone change the posts: Post[] property in the blogger component to posts: Post[] = [], that means our grouping logic will be triggered once with empty array. When the real data kicks in, it won’t be triggered again.

      Solution 2: Use ngOnChanges

      ngOnChanges is a lifecycle hook that run whenever it detects changes to input properties. That means it’s guaranteed that everytime data input value changed, our grouping logic will be triggered if we put our code here.

      Please revert all the changes in previous solution

      Our blogger component, we don’t need *ngIf anymore.

      // blogger.component.ts
      
      ...
          template: `
              <h1>Posts by: {{ blogger }}</h1>
              <div>
                  <posts [data]="posts"></posts>
              </div>
          `
      ...
      

      Our posts component

      // posts.component.ts
      
      ...
          ngOnChanges(changes: SimpleChanges) {
              // only run when property "data" changed
              if (changes['data']) {
                  this.groupPosts = this.groupByCategory(this.data);
              }
          }
      ...
      

      Please notes that changes is a key value pair object. The key is the name of the input property, in our case it’s data. Whenever writing code in ngOnChanges, you may want to make sure that the logic run only when the target data changed, because you might have a few inputs.

      That’s why we run our grouping logic only if there are changes in data.

      One thing I don’t like about this solution is that we lose the strong typing and need to use magic string “data”. In case we change the property name data to something else, we need to remember to change this as well.

      Of course we can defined another interface for that, but that’s too much work.

      Solution 3: Use RxJs BehaviorSubject

      We can utilize RxJs BehaviorSubject to detect the changes. I suggest you take a look at the unit test of the official document here before we continue.

      Just assume that BehaviorSubject is like a property with get and set abilities, plus an extra feature; you can subscribe to it. So whenever there are changes on the property, we will be notified, and we can act on that. In our case, it would be triggering the grouping logic.

      Please revert all the changes in previous solution

      There are no changes in our blogger component:

      // blogger.component.ts
      
      ...
          template: `
              <h1>Posts by: {{ blogger }}</h1>
              <div>
                  <posts [data]="posts"></posts>
              </div>
          `
      ...
      

      Let’s update our post component to use BehaviorSubject.

      // posts.component.ts
      
      ...
          // initialize a private variable _data, it's a BehaviorSubject
          private _data = new BehaviorSubject<Post[]>([]);
      
          // change data to use getter and setter
          @Input()
          set data(value) {
              // set the latest value for _data BehaviorSubject
              this._data.next(value);
          };
      
          get data() {
              // get the latest value from _data BehaviorSubject
              return this._data.getValue();
          }
      
          ngOnInit() {
              // now we can subscribe to it, whenever input changes, 
              // we will run our grouping logic
              this._data
                  .subscribe(x => {
                      this.groupPosts = this.groupByCategory(this.data);
                  });
          }
      ...
      
      

      First of all, if you are not aware, Javacript supports getter and setter like C# and Java, check MDN for more info. In our case, we split the data to use getter and setter. Then, we have a private variable _data to hold the latest value.

      To set a value to BehaviorSubject, we use .next(theValue). To get the value, we use .getValue(), as simple as that.

      Then during component initialization, we subscribe to the _data, listen to the changes, and call our grouping logic whenever changes happens.

      Take a note for observable and subject, you need to unsubscribe to avoid performance issues and possible memory leaks. You can do it manually in ngOnDestroyor you can use some operator to instruct the observable and subject to unsubscribe itself once it meet certain criteria.

      In our case, we would like to unsubscribe once the groupPosts has value. We can add this line in our subscription to achieve that.

      // posts.component.ts
      
      ...
          ngOnInit() {
              this._data
                  // add this line
                  // listen to data as long as groupPosts is undefined or null
                  // Unsubscribe once groupPosts has value
                  .takeWhile(() => !this.groupPosts)
                  .subscribe(x => {
                      this.groupPosts = this.groupByCategory(this.data);
                  });
          }
      ...
      
      

      With this one line .takeWhile(() => !this.groupPosts), it will unsubscribe automatically once it’s done. There are other ways to unsubscribe automatically as well, e.g take, take Util, but that’s beyond this topic.

      By using BehaviorSubject, we get strong typing, get to control and listen to changes. The only downside would be you need to write more code.

      Which One Should I Use?

      The famous question comes with the famous answer: It depends.

      Use *ngIf if you are sure that your changes run only once, it’s very straightforward. Use ngOnChanges or BehaviorSubject if you want to listen to changes continuously or you want guarantee.

      That’s it. Happy Coding!



      Source link