Tuesday, February 23, 2021

Vue.js - More on Components

Component Registration


Component Registration could be global or local.


Global



Components you can use anywhere in your Vue app, which means you can use those custom html you defined in any template under its app. 

Take below codes as an example, all components are registered in global level (bind to app).

Ex:

// main.js

const app = createApp(App);

app.component('my-header'MyHeader);
app.component('my-body'MyBody);
app.component('my-content'MyContent);
app.component('my-message'MyMessage);

app.mount("#app");


// MyBody.vue

    <template>
        <header>
<!-- Using global component -->
            <my-message message="This is Body"></my-message>
            <my-content></my-content>
        </header>
    </template>


Since MyMessage Component was registered in Global Level, MyBody.vue can use it without any issues.


Local


Another approach is called Local Registration. Components can be used only by the component whom register them directly. Even its child component can not access those registered components.

Ex:
 
// main.js

    const app = createApp(App);

    app.component('my-header'MyHeader);
    app.component('my-body'MyBody);
    app.component('my-message'MyMessage);

    app.mount("#app");



// MyBody.vue

<template>
     <header>
         <my-message message="This is Body"></my-message>
<!-- Using local component -->
         <my-content></my-content>
    </header>
</template>

<script>
import MyContent from "./MyContent.vue";
export default {
// Local Registration
     components: {
         "my-content": MyContent,
    },
};
</script>


From the example above, since 'my-content' is only used by MyBody.vue, therefore, we can move it from global to local level.

And 'my-message' is used by plenty of components, so we keep it as global level.


Component Styling - scoped


It is called 'style encapsulation' to limit the CSS styling to the template under the same file (Single File Component).

It will not be applied to any child, sibling, or other components.

Ex:

// MyMessage.vue

<template>
     <header>
         <h1>{{ message }}</h1>
    </header>
</template>

    <script>
    export default {
        props: ["message"],
    };
    </script>

    <style scoped>
    h1 {
        colorblue;
    }
    </style>


If you want to know how Vue implemented it, you can inspect HTML elements, and you will notice that there is a special attribute added to html element.

<header data-v-345b333b="">
        <h1 data-v-345b333b="">This is Header</h1>
    </header>


Also, the styling will be changed by Vue as below.

 h1[data-v-345b333b] {
         color: blue;
    }



Slots


When your app get bigger, and most components have the same layout and styling, you might want to find a way to reduce the redundant code (both html and CSS) in your app.

Then you might want to abstract this common layout and styling as a wrapper (or container), which can wrap the content from input.

Then Vue provide 'slot' for you.

Ex: (Before using slot)

// MyLeftContent.vue
<template>
     <section>
         <h1>This is content area</h1>
        <p>Hello World</p>

         <h2>This is left section</h2>
    </section>
</template>

<style scoped>
section {
     background-colorblue;
}
</style>


// MyRightContent.vue

<template>
     <section>
         <h1>This is content area</h1>
        <p>Hello World</p>

         <h2>This is right section</h2>
    </section>
</template>

<style scoped>
section {
     background-colorblue;
}
</style>


You can see it is so duplicated both in template and style.

Using 'slot', then we can simplify our code.

Ex: (Using slot)

// ContentWrapper.vue

<template>
     <section>
         <h1>This is content area</h1>
        <p>Hello World</p>
<!-- Placeholder for the passing content -->
         <slot></slot>
    </section>
</template>

<style scoped>
section {
     background-colorblue;
}
</style>


// MyLeftContent.vue

<template>
    <content-wrapper>
<!-- Passing content -->
         <h2>This is left section</h2>
     </content-wrapper>
</template>


// MyRightContent.vue

<template>
     <content-wrapper>
<!-- Passing content -->
         <h2>This is right section</h2>
    </content-wrapper>
</template>



Named Slots



It will be useful if we can pass multiple content into a component.

Giving slots names can help to determine which slot to distribute.
 
Ex:

// ContenWrapper.vue

<template>
     <section>
         <h1>This is wrapper</h1>

        <div>
             <!-- slot: message -->
             <slot name="message"> </slot>
        </div>

         <!-- slot: default -->
         <slot></slot>
    </section>
</template>


// MyLeftContent.vue

<template>
     <content-wrapper>
         <!-- slot: message -->
        <template v-slot:message>
             <h1>{{ message }}</h1>
         </template>

        <!-- slot: default -->
         <h1>This is left section</h1>
     </content-wrapper>
</template>

<script>
export default {
     data() {
         return {
             message: "Hello left section",
        };
     },
};
</script>


NOTE: If you did not provide slot names, then it will be treated as 'default' name.


Slot with default content



To cover a case if some slots did not pass content, Vue provide 'default' content.

Ex:

// ContentWrapper.vue

<template>
     <section>
         <h1>This is wrapper</h1>

        <div>
             <!-- slot: message -->
             <slot name="message">
                 <!-- Default content -->
                 <h1>This is default message</h1>
            </slot>
         </div>

         <!-- slot: default -->
        <slot></slot>
     </section>
</template>


// MyLeftContent.vue

<template>
     <content-wrapper>
<!-- Did not pass content to message slot -->

         <!-- slot: default -->
        <h1>This is left section</h1>
     </content-wrapper>
</template>


Since MyLeftContnet.vue did not provide template with v-slot:message, the default content will be shown.


Using $slots



Instead providing default content, we can use $slots to determine slots are included or not, and then control the UI.

Ex:

// ContentWrapper.vue

<template>
     <section>
         <h1>This is wrapper</h1>
    
            <!-- Using $slot -->
        <div v-if="$slots.message">
             <!-- slot: message -->
             <slot name="message"></slot>
        </div>

        <!-- slot: default -->
         <slot></slot>
     </section>
</template>



Scoped Slots


It is advanced topic, and it is seldom to be used in simple projects.


Dynamic Components



Instead using v-if to show/hide the components, Vue provide a way to dynamically switch it.


Ex:

// App.vue

<template>
    <div>
         <button v-on:click="onBtnClicked('a')">Show Section A</button>
        <button v-on:click="onBtnClicked('b')">Show Section B</button>

<!-- Using dynamic component to switch -->
         <component :is="currentComponent"></component>
    </div>
</template>

<script>
export default {
     data() {
         return {
             currentComponent: "section-a",
        };
     },
    methods: {
         onBtnClicked(tab) {
             if (tab == "a") {
                 this.currentComponent = "section-a";
            } else {
                 this.currentComponent = "section-b";
             }
         },
    },
};
</script>



Keep Alive?


You may encountered an issue that using dynamic component will lost some user input.

The reason is that when switching to another component, Vue will create a new instance for you.

Vue provide an element called 'keep-alive' to solve this issue.



Teleport



Vue allows us to wrap our components (relating template) into Tree format.

However, some nested template will against Semantic HTML.

The common case is that templates need to use full window to display.

Vue provide a special tag called 'teleport' to help you move some template to other position with CSS selector.


Ex:

// App.vue

<template>
     <div>
         <button v-on:click="showModal = true">Open My Modal</button>
        <div v-if="showModal">
<!-- Using teleport to move this template to other position-->
             <teleport to="body">
                 <my-modal>
                     Modal Content
                     <button v-on:click="onCloaseBtnClicked">Close</button>
                </my-modal>
             </teleport>
         </div>
    </div>
</template>

<script>
export default {
     data() {
         return {
             showModal: false,
        };
     },
    methods: {
         onCloaseBtnClicked() {
             this.showModal = false;
        },
     },
};
</script>


// MyModal.vue

<template>
     <div class="container">
         <h1>Modal Header</h1>
        <slot></slot>
         <h1>Modal Footer</h1>
    </div>
</template>

<style scoped>
.container {
     positionfixed;
    background-colorgray;
    colorwhite;
     height100vh;
    width100vw;
}
</style>


No comments:

Post a Comment