diff --git a/.gitignore b/.gitignore deleted file mode 100644 index b58000d..0000000 --- a/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -.DS_Store -node_modules -*.log -.temp -docs/.vuepress/dist diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 75130d7..0000000 --- a/.travis.yml +++ /dev/null @@ -1,28 +0,0 @@ -language: node_js - -node_js: - - 10 - -cache: - directories: - - node_modules - -script: - - npm run docs:build - -after_success: - - npm run docs:deploy - -branches: - except: - - gh-pages - -deploy: - provider: pages - skip_cleanup: true - github_token: $GITHUB_TOKEN - keep_history: true - local_dir: docs/.vuepress/dist - repo: learn-vuejs/vue-patterns - on: - branch: master diff --git a/404.html b/404.html new file mode 100644 index 0000000..1673490 --- /dev/null +++ b/404.html @@ -0,0 +1,33 @@ + + +
+ + +
-
- -
- - - - - diff --git a/docs/.vuepress/components/ThemeButton.vue b/docs/.vuepress/components/ThemeButton.vue deleted file mode 100644 index 64d1e62..0000000 --- a/docs/.vuepress/components/ThemeButton.vue +++ /dev/null @@ -1,25 +0,0 @@ - - - - - diff --git a/docs/.vuepress/components/ThemeProvider.vue b/docs/.vuepress/components/ThemeProvider.vue deleted file mode 100644 index d69c1f0..0000000 --- a/docs/.vuepress/components/ThemeProvider.vue +++ /dev/null @@ -1,13 +0,0 @@ - diff --git a/docs/.vuepress/components/ThrowError.vue b/docs/.vuepress/components/ThrowError.vue deleted file mode 100644 index 085e429..0000000 --- a/docs/.vuepress/components/ThrowError.vue +++ /dev/null @@ -1,25 +0,0 @@ - -- -
- - - diff --git a/docs/.vuepress/config.js b/docs/.vuepress/config.js deleted file mode 100644 index 7dd32fe..0000000 --- a/docs/.vuepress/config.js +++ /dev/null @@ -1,244 +0,0 @@ -module.exports = { - base: '/vue-patterns/', - locales: { - '/': { - lang: 'en-US', - title: 'Vue Patterns', - description: - 'Useful Vue patterns, techniques, tips and tricks and curated helpful links.', - }, - '/ru/': { - lang: 'ru', - title: 'Паттерны Vue', - description: 'Полезные паттерны, методы, советы и рекомендации, а также тщательно подобранный список ссылок по Vue' - }, - '/es/': { - lang: 'es', - title: 'Patrones de Vue', - description: 'Patrones útiles de Vue, técnicas, consejos, trucos y enlaces seleccionados.' - } - }, - head: [ - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '57x57', - href: '/icons/apple-icon-57x57.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '60x60', - href: '/icons/apple-icon-60x60.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '72x72', - href: '/icons/apple-icon-72x72.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '76x76', - href: '/icons/apple-icon-76x76.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '114x114', - href: '/icons/apple-icon-114x114.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '120x120', - href: '/icons/apple-icon-120x120.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '144x144', - href: '/icons/apple-icon-144x144.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '152x152', - href: '/icons/apple-icon-152x152.png', - }, - ], - [ - 'link', - { - rel: 'apple-touch-icon', - sizes: '180x180', - href: '/icons/apple-icon-180x180.png', - }, - ], - [ - 'link', - { - rel: 'icon', - type: 'image/png', - sizes: '192x192', - href: '/icons/android-icon-192x192.png', - }, - ], - [ - 'link', - { - rel: 'icon', - type: 'image/png', - sizes: '32x32', - href: '/icons/favicon-32x32.png', - }, - ], - [ - 'link', - { - rel: 'icon', - type: 'image/png', - sizes: '96x96', - href: '/icons/favicon-96x96.png', - }, - ], - [ - 'link', - { - rel: 'icon', - type: 'image/png', - sizes: '16x16', - href: '/icons/favicon-16x16.png', - }, - ], - ['link', { rel: 'manifest', href: '/manifest.json' }], - [ - 'script', - { defer: true, async: true, src: 'https://buttons.github.io/buttons.js' }, - ], - ], - plugins: [ - [ - '@vuepress/pwa', - { - serviceWorker: true, - updatePopup: true - } - ], - [ - '@vuepress/google-analytics', - { - 'ga': 'UA-175336794-1' - } - ] - ], - themeConfig: { - displayAllHeaders: true, - lastUpdated: 'Last Updated', - repo: 'learn-vuejs/vue-patterns', - docsDir: 'docs', - editLinks: true, - locales: { - '/': { - lastUpdated: 'Last Updated', - nav: [ - { text: 'Home', link: '/' }, - { text: 'Docs', link: '/patterns/' }, - { - text: 'Translations', - items: [ - { - text: '简体中文', - link: 'https://github.com/ZYSzys/vue-patterns-cn', - }, - { - text: '繁體中文', - link: 'https://github.com/yoyoys/vue-patterns-cht', - }, - ], - }, - ], - sidebar: [ - ['/patterns/', 'Patterns'], - ['/useful-links/', 'Useful Links'], - ['/sponsors/', 'Fullstack Vue Book'], - ['/translations/', 'Translations'], - ], - }, - '/ru/': { - label: 'Русский', - selectText: 'Переводы', - lastUpdated: 'Последнее обновление', - editLinkText: 'Изменить эту страницу на GitHub', - nav: [ - { text: 'Главная', link: '/ru/' }, - { text: 'Документация', link: '/ru/patterns/' }, - { - text: 'Внешние переводы', - items: [ - { - text: '简体中文', - link: 'https://github.com/ZYSzys/vue-patterns-cn', - }, - { - text: '繁體中文', - link: 'https://github.com/yoyoys/vue-patterns-cht', - }, - ], - }, - ], - sidebar: [ - ['/ru/patterns/', 'Паттерны'], - ['/ru/useful-links/', 'Полезные ссылки'], - ['/ru/sponsors/', 'Книга Fullstack Vue'], - ['/ru/translations/', 'Переводы'], - ], - }, - '/es/': { - label: 'Español', - selectText: 'Idiomas', - lastUpdated: 'Última actualización', - editLinkText: 'Modificar esta página en GitHub', - nav: [ - { text: 'Inicio', link: '/es/' }, - { text: 'Docs', link: '/es/patterns/' }, - { - text: 'Traducciones', - items: [ - { - text: '简体中文', - link: 'https://github.com/ZYSzys/vue-patterns-cn', - }, - { - text: '繁體中文', - link: 'https://github.com/yoyoys/vue-patterns-cht', - }, - ], - }, - ], - sidebar: [ - ['/es/patterns/', 'Patrones'], - ['/es/useful-links/', 'Enlaces útiles'], - ['/es/sponsors/', 'Libro Fullstack Vue'], - ['/es/translations/', 'Traducciones'], - ], - }, - }, - }, -}; diff --git a/docs/.vuepress/override.styl b/docs/.vuepress/override.styl deleted file mode 100644 index c802d6d..0000000 --- a/docs/.vuepress/override.styl +++ /dev/null @@ -1,24 +0,0 @@ -.btn { - display: inline-block; - font-size: 1.2rem; - color: #212121; - padding: 0.8rem 1.6rem; - border-radius: 4px; - transition: background-color 0.1s ease; - box-sizing: border-box; -} - -.btn-primary { - color: #fff; - background-color: #3eaf7c; -} - -.btn-danger { - color: #fff; - background-color: #da5961; -} - -.bg-danger { - color: #fff; - background-color: #da5961; -} diff --git a/docs/README.md b/docs/README.md deleted file mode 100644 index 286c2ac..0000000 --- a/docs/README.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -home: true -heroImage: /learn-vue-logo.png -actionText: Get Started → -actionLink: /patterns/ ---- - -
-
-
will be rendered into DOM
-except `template` element
- -``` - -### Función de Renderizado o JSX - -Si usas funciones de renderizado o JSX en tu aplicación vue, puedes aplicar todas las técnicas, tales como declaraciones `if else` y `switch case` y operadores `ternarios` y `logicos`. - -Declaración `if else` - -```jsx -export default { - data() { - return { - isTruthy: true, - }; - }, - render(h) { - if (this.isTruthy) { - return{{ props.description }}
- -``` -**index.vue** -```vue - -{{ name }}
-A paragraph for the main content.
-And another one.
- - -Here's some contact info
- -{{ props.state.label }}
- -
-
-
-
-
will be rendered into DOM
-except `template` element
- -``` - -### Render Function or JSX - -If you use render functions or JSX in your vue application, you can apply all the techniques, such as the `if else` and `switch case` statements and `ternary` and `logical` operators. - -`if else` statement - -```jsx -export default { - data() { - return { - isTruthy: true, - }; - }, - render(h) { - if (this.isTruthy) { - return{{ props.description }}
- -``` -**index.vue** -```vue - -{{ name }}
-A paragraph for the main content.
-And another one.
- - -Here's some contact info
- -{{ props.state.label }}
- -
-
-
-
-
-
будут отрендерены в DOM,
-за исключением элемента `template`
- -``` - -### Render-функция или JSX - -Если вы используете JSX в своем Vue-приложении, то можете применять все техники, например использования выражения `if else` и `switch case`, а также тернарные и логические операторы. - -Использование выражения `if else` - -```jsx -export default { - data() { - return { - isTruthy: true, - }; - }, - render(h) { - if (this.isTruthy) { - returnАбзац для основного контента.
-И еще один.
- - -Здесь некоторые контактные данные
- -<template>
+ <p class="demo">
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+};
+</script>
+
+<style scoped>
+.btn-primary {
+ display: inline-block;
+ font-size: 1.2rem;
+ color: #fff;
+ background-color: #3eaf7c;
+ padding: 0.8rem 1.6rem;
+ border-radius: 4px;
+ transition: background-color 0.1s ease;
+ box-sizing: border-box;
+ border-bottom: 1px solid #389d70;
+}
+</style>
+
Vue.component('my-btn', {
+ template: `
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ `,
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+ render(h) {
+ return h(
+ 'button',
+ {
+ attrs: {
+ class: 'btn-primary',
+ },
+ on: {
+ click: this.handleClick,
+ },
+ },
+ this.$slots.default
+ );
+ },
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ text: 'Click me',
+ };
+ },
+ methods: {
+ handleClick() {
+ console.log('clicked');
+ },
+ },
+ render() {
+ return (
+ <button class="btn-primary" @click.prevent="handleClick">
+ {this.$slots.default}(clicked - {{count}})
+ </button>
+ );
+ },
+});
+
<template>
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{ count }})
+ </button>
+</template>
+
+<script>
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+@Component
+export default MyBtn extends Vue {
+ count = 0;
+
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ }
+}
+</script>
+
+<style scoped>
+.btn-primary {
+ background-color: blue;
+}
+</style>
+
Básicamente, los componentes de vue siguen un flujo en un sólo sentido, esto es propiedades hacia los hijos (Ver la guía oficial) y eventos hacia el padre.
+Las props son información de solo lectura, así que es imposible cambiar las props desde un componente hijo.
+Cuando las props cambian, los componentes hijo serán renderizados de nuevo automáticamente (las props
son una fuente de información reactiva).
+Los componentes hijo solo pueden emitir eventos a sus padres directos, de tal manera que el componente padre puede cambiar su data
, que está asignada a las props
del componente hijo.
<template>
+ <button @click="$emit('click');">{{ text }}</button>
+</template>
+
+<script>
+export default {
+ name: 'v-btn',
+ props: {
+ text: String,
+ },
+};
+</script>
+
<template>
+ <v-btn :text="buttonText" @click="handleClick"></v-btn>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ clickCount: 0,
+ buttonText: 'initial button text',
+ };
+ },
+ methods: {
+ handleClick() {
+ this.buttonText = `Button clicked ${++this.clickCount}`;
+ console.log('clicked', this.buttonText);
+ },
+ },
+};
+</script>
+
v-if
/ v-else
/ v-else-if
/ v-show
)v-if
<h1 v-if="true">Render only if v-if condition is true</h1>
+
v-if
y v-else
<h1 v-if="true">Render only if v-if condition is true</h1>
+<h1 v-else>Render only if v-if condition is false</h1>
+
v-else-if
<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
+<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
+<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
+<div v-else>Render if `type` is not `A` or `B` or `C`</div>
+
v-show
<h1 v-show="true">
+ Always rendered, but it should be visible only if `v-show` conditions is true
+</h1>
+
Si necesitas renderizar condicionalmente más de un elemento, puedes usar las directivas (v-if
/ v-else
/ v-else-if
/v-show
) en un elemento <template>
.
+Ten en cuenta que el elemento <template>
no se renderiza en el DOM. Es un contenedor invisible.
<template v-if="true">
+ <h1>All the elements</h1>
+ <p>will be rendered into DOM</p>
+ <p>except `template` element</p>
+</template>
+
Si usas funciones de renderizado o JSX en tu aplicación vue, puedes aplicar todas las técnicas, tales como declaraciones if else
y switch case
y operadores ternarios
y logicos
.
Declaración if else
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ if (this.isTruthy) {
+ return <h1>Render value is true</h1>;
+ } else {
+ return <h1>Render value is false</h1>;
+ }
+ },
+};
+
Declaración switch case
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ switch (this.type) {
+ case 'info':
+ return <Info text={text} />;
+ case 'warning':
+ return <Warning text={text} />;
+ case 'error':
+ return <Error text={text} />;
+ default:
+ return <Success text={text} />;
+ }
+ },
+};
+
o puedes utilizar un objeto
diccionario para simplificar el switch case
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+const COMPONENT_MAP = {
+ info: Info,
+ warning: Warning,
+ error: Error,
+ success: Success,
+};
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ const Comp = COMPONENT_MAP[this.type || 'success'];
+
+ return <Comp />;
+ },
+};
+
Operador ternario
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ return (
+ <div>
+ {this.isTruthy ? (
+ <h1>Render value is true</h1>
+ ) : (
+ <h1>Render value is false</h1>
+ )}
+ </div>
+ );
+ },
+};
+
Operador lógico
export default {
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ render(h) {
+ return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
+ },
+};
+
<component>
con atributo is
<component :is="currentTabComponent"></component>
+
Con el ejemplo de código de arriba, el componente renderizado será destruido si un componente diferente es asignado en la propiedad is
. Si deseas que los componentes mantengan sus instancias y su estado sin ser destruido, puedes envolver la etiqueta <component>
en otra etiqueta <keep-alive>
de la siguiente manera:
<keep-alive> <component :is="currentTabComponent"></component> </keep-alive>
+
Un componente funcional es un SFC especial, es básicamente un componente sin estado (es decir, sin etiqueta de script). Solamente acepta props
para mostrar datos.
Para poder hacer un SFC un componente funcional, necesitas agregar el atributo functional
a la etiqueta de <template>
de la siguiente manera <template functional>
fp-component.vue
<template functional>
+ <h1>{{ props.title }}</h1>
+ <p>{{ props.description }}</p>
+</template>
+
index.vue
<template>
+ <fp-component v-bind="{ title: 'FP Component', description: 'Only takes props' }" />
+</template>
+
+<script>
+import FPComponent from './fp-component';
+
+export default {
+ components: {
+ FPComponent
+ }
+}
+</script>
+
Beneficios de usar un Componente Funcional sobre un Componente con Estado:
Un componente sin renderizado es básicamente un componente que no renderiza ningún HTML en el DOM pero proporciona lógica JavaScript reutilizable abstraída en un SFC.
Un componente sin renderizado utiliza la API de Slots para lograr lo que queremos.
Renderless.vue
<script>
+export default {
+ render() {
+ return this.$scopedSlots.default({ name: 'John' });
+ }
+};
+</script>
+
El único trabajo de Renderless.vue es proporcionar la prop name
App.vue
<template>
+ <renderless v-slot="{ name }">
+ <p>{{ name }}</p>
+ </renderless>
+</template>
+
+<script>
+import Renderless from './Renderless.vue';
+
+export default {
+ components: {
+ Renderless,
+ }
+};
+</script>
+
Lo bueno de utilizar Componentes sin renderizado es que nos permite separar nuestra lógica del HTML.
<template>
+ <div class="component-b"><component-a></component-a></div>
+</template>
+
+<script>
+import ComponentA from './ComponentA';
+
+export default {
+ components: {
+ ComponentA,
+ },
+};
+</script>
+
Cuando quieres extender un solo componente de vue
<template>
+ <button class="button-primary" @click.prevent="handleClick">
+ {{ buttonText }}
+ </button>
+</template>
+
+<script>
+import BaseButton from './BaseButton';
+
+export default {
+ extends: BaseButton,
+ props: ['buttonText'],
+};
+</script>
+
// closableMixin.js
+export default {
+ props: {
+ isOpen: {
+ default: true,
+ },
+ },
+ data: function() {
+ return {
+ shown: this.isOpen,
+ };
+ },
+ methods: {
+ hide: function() {
+ this.shown = false;
+ },
+ show: function() {
+ this.shown = true;
+ },
+ toggle: function() {
+ this.shown = !this.shown;
+ },
+ },
+};
+
<template>
+ <div
+ v-if="shown"
+ class="alert alert-success"
+ :class="'alert-' + type"
+ role="alert"
+ >
+ {{ text }}
+ <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
+ </div>
+</template>
+
+<script>
+import closableMixin from './mixins/closableMixin';
+
+export default {
+ mixins: [closableMixin],
+ props: ['text'],
+};
+</script>
+
2.6.0+
Si usas Vue en la versión 2.6.0 o mayor, Vue introdujo una nueva y unificada api para slots, que es
v-slot
. +Reemplaza los atributos slot y slot-scope, que son obsoletos, pero no han sido removidos y se encuentran todavía documentados aquí. +Puedes hacer referencia la API obsoleta aquí.
<template>
+ <button class="btn btn-primary">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ name: 'VBtn',
+};
+</script>
+
<template>
+ <v-btn>
+ <span class="fa fa-user"></span> Login
+ </v-btn>
+</template>
+
+<script>
+import VBtn from './VBtn';
+
+export default {
+ components: {
+ VBtn,
+ },
+};
+</script>
+
BaseLayout.vue
<div class="container">
+ <header>
+ <slot name="header"></slot>
+ </header>
+ <main>
+ <slot></slot>
+ </main>
+ <footer>
+ <slot name="footer"></slot>
+ </footer>
+</div>
+
App.vue
<base-layout>
+ <template v-slot:header>
+ <h1>Here might be a page title</h1>
+ </template>
+
+ <p>A paragraph for the main content.</p>
+ <p>And another one.</p>
+
+ <template v-slot:footer>
+ <p>Here's some contact info</p>
+ </template>
+</base-layout>
+
Vue proporciona una sintaxis abreviada para slots nombrados.
+Puedes remplazar v-slot:
con #
<template>
+ <ul>
+ <li v-for="todo in todos" v-bind:key="todo.id">
+ <!-- We have a slot for each todo, passing it the -->
+ <!-- `todo` object as a slot prop. -->
+ <slot v-bind:todo="todo"> {{ todo.text }} </slot>
+ </li>
+ </ul>
+</template>
+
+<script>
+export default {
+ name: 'TodoList',
+ props: {
+ todos: {
+ type: Array,
+ default: () => [],
+ },
+ },
+};
+</script>
+
<template>
+ <todo-list v-bind:todos="todos">
+ <template v-slot:default="{ todo }">
+ <span v-if="todo.isComplete">✓</span>
+ {{ todo.text }}
+ </template>
+ </todo-list>
+</template>
+
+<script>
+import TodoList from "./TodoList";
+
+export default {
+ components: {
+ TodoList
+ },
+ data() {
+ return {
+ todos: [
+ { text: "todo 1", isComplete: true },
+ { text: "todo 2", isComplete: false },
+ { text: "todo 3", isComplete: false },
+ { text: "todo 4", isComplete: true }
+ ]
+ };
+ }
+};
+</script>
+
En la mayoría de los casos, puedes usar slots con scope en vez de props de renderizado. Pero, puede llegar a ser util en algunas ocasiones.
con SFC
<template>
+ <div id="app"><Mouse :render="__render" /></div>
+</template>
+
+<script>
+import Mouse from './Mouse.js';
+export default {
+ name: 'app',
+ components: {
+ Mouse,
+ },
+ methods: {
+ __render({ x, y }) {
+ return (
+ <h1>
+ The mouse position is ({x}, {y})
+ </h1>
+ );
+ },
+ },
+};
+</script>
+<style>
+* {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+</style>
+
con JSX
const Mouse = {
+ name: 'Mouse',
+ props: {
+ render: {
+ type: Function,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ x: 0,
+ y: 0,
+ };
+ },
+ methods: {
+ handleMouseMove(event) {
+ this.x = event.clientX;
+ this.y = event.clientY;
+ },
+ },
+ render(h) {
+ return (
+ <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
+ {this.$props.render(this)}
+ </div>
+ );
+ },
+};
+
+export default Mouse;
+
En ciertas ocasiones, es posible que quieras pasar props y listeners a un componente hijo sin tener que declarar todas las props del componente hijo.
+Puedes ligar $attrs
y $listeners
en el componente hijo y establecer inheritAttrs
como false
(de lo contrario, div
y child-component` recibiran los atributos)
<template>
+ <div>
+ <h1>{{title}}</h1>
+ <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
+ </div>
+</template>
+
+<script>
+import PassingPropsChild from './PassingPropsChild';
+
+export default {
+ components: {
+ PassingPropsChild,
+ },
+ inheritAttrs: false,
+ props: {
+ title: {
+ type: String,
+ default: 'Hello, Vue!',
+ },
+ },
+};
+</script>
+
Desde el componente padre, puedes hacer esto:
<template>
+ <p class="demo">
+ <passing-props
+ title="This is from <passing-props />"
+ childPropA="This is from <passing-props-child />"
+ @click="handleClickPassingPropsChildComponent"
+ >
+ </passing-props>
+ </p>
+</template>
+
+<script>
+import PassingProps from './PassingProps';
+
+export default {
+ components: {
+ PassingProps,
+ },
+ methods: {
+ handleClickPassingPropsChildComponent() {
+ console.log('This event comes from `<passing-props-child />`');
+ alert('This event comes from `<passing-props-child />`');
+ },
+ },
+};
+</script>
+
El patrón Proveedor / Consumidor es muy sencillo, su objetivo es separar la lógica de la presentación. Necesitamos dos componentes para crear este patrón.
Provider.vue
<template>
+ <div>
+ <slot v-bind="{ state, actions }" />
+ </div>
+</template>
+
+<script>
+export default {
+ computed: {
+ state() {
+ return {
+ label: 'button',
+ };
+ },
+ actions() {
+ return {
+ click: this.click,
+ };
+ },
+ },
+ methods: {
+ click() {
+ console.log('Clicked');
+ },
+ },
+}
+</script>
+
Provider.vue
es responsable de contener la lógica de estado, estamos separándola de la presentación. Podemos hacer uso de la API de Slots
como proveedor de la información.
Consumer.vue
<template functional>
+ <div>
+ <p>{{ props.state.label }}</p>
+ <button @click="props.actions.click">CLICK</button>
+ </div>
+</template>
+
Consumer.vue
es responsable de contener la presentación, toma en cuenta que estamos utilizando un Componente Funcional.
App.vue
<template>
+ <provider v-slot="{ state, actions }">
+ <consumer v-bind="{ state, actions }" />
+ </provider>
+</template>
+
+<script>
+import Provider from './Provider.vue';
+import Consumer from './Consumer.vue';
+
+export default {
+ components: {
+ Provider,
+ Consumer,
+ },
+};
+</script>
+
Este patrón proporciona una buena manera que nos permite componer componentes limpios y desacoplados. Puedes ver los ejemplos en CodeSandbox
Vue tiene un mecanismo para proveer / inyectar un objeto
a todos sus descendientes, sin importar el nivel de profundidad de la jerarquía del componente, siempre y cuando se encuentre dentro de la misma cadena del padre. Vale mencionar que las ligaduras provide
e inject
no son reactivas, a menos que pasen un objeto observado.
<parent-component>
+ <child-component>
+ <grand-child-component></grand-child-component>
+ </child-component>
+</parent-component>
+
En el ejemplo de la jerarquía de componentes de arriba, para poder derivar información desde parent-component
, necesitas pasar el data(object) como props
a child-component
y a grand-child-component
. Sin embargo, si parent-component
utiliza provide
con el data(object), el componente grand-child-component
puede definir inject
para inyectar el objeto que proviene de parent-component
.
TIP
Puedes usar también @Provide
, @Inject
de vue-property-decorator
<script>
+export default {
+ provide: {
+ theme: {
+ primaryColor: '#3eaf7c',
+ secondaryColor: '#1FA2FF'
+ },
+ },
+ render(h) {
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor) }">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ inject: {
+ theme: {
+ default: {},
+ },
+ },
+ props: {
+ primary: {
+ type: Boolean,
+ default: false,
+ },
+ secondary: {
+ type: Boolean,
+ default: false,
+ },
+ },
+};
+</script>
+
<theme-provider>
+ <p class="demo">
+ <button class="btn">Normal Button</button>
+ <theme-button secondary>Themed Button</theme-button>
+ </p>
+</theme-provider>
+
errorCaptured
<script>
+export default {
+ name: 'ErrorBoundary',
+ data() {
+ return {
+ error: false,
+ errorMessage: '',
+ };
+ },
+ errorCaptured(err, vm, info) {
+ this.error = true;
+ this.errorMessage = `Sorry, error occured in ${info}`;
+
+ return false;
+ },
+ render(h) {
+ if (this.error) {
+ return h('p', { class: 'demo bg-danger' }, this.errorMessage);
+ }
+
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <p class="demo">
+ <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ watch: {
+ count() {
+ throw new Error('error');
+ },
+ },
+ methods: {
+ throwError() {
+ this.count++;
+ },
+ },
+};
+</script>
+
<error-boundary> <throw-error></throw-error> </error-boundary>
+
watch al crear un componente
// no hagas esto
+created() {
+ this.fetchUserList();
+},
+watch: {
+ searchText: 'fetchUserList',
+}
+
// mejor haz esto
+watch: {
+ searchText: {
+ handler: 'fetchUserList',
+ immediate: true,
+ }
+}
+
+ Enlaces útiles + + → +
+ ← + + Enlaces útiles + + Traducciones + + → +
+ ← + + Patrones + + Libro Fullstack Vue + + → +
<template>
+ <p class="demo">
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+};
+</script>
+
+<style scoped>
+.btn-primary {
+ display: inline-block;
+ font-size: 1.2rem;
+ color: #fff;
+ background-color: #3eaf7c;
+ padding: 0.8rem 1.6rem;
+ border-radius: 4px;
+ transition: background-color 0.1s ease;
+ box-sizing: border-box;
+ border-bottom: 1px solid #389d70;
+}
+</style>
+
Vue.component('my-btn', {
+ template: `
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ `,
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+ render(h) {
+ return h(
+ 'button',
+ {
+ attrs: {
+ class: 'btn-primary',
+ },
+ on: {
+ click: this.handleClick,
+ },
+ },
+ this.$slots.default
+ );
+ },
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ text: 'Click me',
+ };
+ },
+ methods: {
+ handleClick() {
+ console.log('clicked');
+ },
+ },
+ render() {
+ return (
+ <button class="btn-primary" @click.prevent="handleClick">
+ {this.$slots.default}(clicked - {{count}})
+ </button>
+ );
+ },
+});
+
<template>
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{ count }})
+ </button>
+</template>
+
+<script>
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+@Component
+export default MyBtn extends Vue {
+ count = 0;
+
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ }
+}
+</script>
+
+<style scoped>
+.btn-primary {
+ background-color: blue;
+}
+</style>
+
Basically, vue components follow one-way data flow, that is props down (See official guide) and events up.
+Props are read-only data, so it's impossible to change props from child components.
+When props change, child components will be rerendered automatically (props
are a reactive data source).
+Child components can only emit events to their direct parent, so that the parent component may change data
, mapped to the child component's props
.
<template>
+ <button @click="$emit('click');">{{ text }}</button>
+</template>
+
+<script>
+export default {
+ name: 'v-btn',
+ props: {
+ text: String,
+ },
+};
+</script>
+
<template>
+ <v-btn :text="buttonText" @click="handleClick"></v-btn>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ clickCount: 0,
+ buttonText: 'initial button text',
+ };
+ },
+ methods: {
+ handleClick() {
+ this.buttonText = `Button clicked ${++this.clickCount}`;
+ console.log('clicked', this.buttonText);
+ },
+ },
+};
+</script>
+
v-if
/ v-else
/ v-else-if
/ v-show
)v-if
<h1 v-if="true">Render only if v-if condition is true</h1>
+
v-if
and v-else
<h1 v-if="true">Render only if v-if condition is true</h1>
+<h1 v-else>Render only if v-if condition is false</h1>
+
v-else-if
<div v-if="type === 'A'">Render only if `type` is equal to `A`</div>
+<div v-else-if="type === 'B'">Render only if `type` is equal to `B`</div>
+<div v-else-if="type === 'C'">Render only if `type` is equal to `C`</div>
+<div v-else>Render if `type` is not `A` or `B` or `C`</div>
+
v-show
<h1 v-show="true">
+ Always rendered, but it should be visible only if `v-show` conditions is true
+</h1>
+
If you want to conditionally render more than one element,
+you can use directives(v-if
/ v-else
/ v-else-if
/v-show
) on a <template>
element.
+Notice that the <template>
element is not actually rendered into the DOM. It is an invisible wrapper.
<template v-if="true">
+ <h1>All the elements</h1>
+ <p>will be rendered into DOM</p>
+ <p>except `template` element</p>
+</template>
+
If you use render functions or JSX in your vue application, you can apply all the techniques, such as the if else
and switch case
statements and ternary
and logical
operators.
if else
statement
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ if (this.isTruthy) {
+ return <h1>Render value is true</h1>;
+ } else {
+ return <h1>Render value is false</h1>;
+ }
+ },
+};
+
switch case
statement
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ switch (this.type) {
+ case 'info':
+ return <Info text={text} />;
+ case 'warning':
+ return <Warning text={text} />;
+ case 'error':
+ return <Error text={text} />;
+ default:
+ return <Success text={text} />;
+ }
+ },
+};
+
or you can use object
map to simplify switch case
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+const COMPONENT_MAP = {
+ info: Info,
+ warning: Warning,
+ error: Error,
+ success: Success,
+};
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ const Comp = COMPONENT_MAP[this.type || 'success'];
+
+ return <Comp />;
+ },
+};
+
ternary
operator
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ return (
+ <div>
+ {this.isTruthy ? (
+ <h1>Render value is true</h1>
+ ) : (
+ <h1>Render value is false</h1>
+ )}
+ </div>
+ );
+ },
+};
+
logical
operator
export default {
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ render(h) {
+ return <div>{this.isLoading && <h1>Loading ...</h1>}</div>;
+ },
+};
+
<component>
with is
attribute<component :is="currentTabComponent"></component>
+
With the above code example, the rendered component will be destroyed if a different component is rendered in <component>
. If you want components to keep their instances without being destroyed within the <component>
tag, you can wrap the <component>
tag in a <keep-alive>
tag like so:
<keep-alive> <component :is="currentTabComponent"></component> </keep-alive>
+
A functional component is a special SFC, it is basically a component that is stateless (meaning no script tag). It only accepts props
in order to display data.
In order to make a SFC a functional one you add the the functional
attribute to the <template>
tag like this: <template functional>
fp-component.vue
<template functional>
+ <h1>{{ props.title }}</h1>
+ <p>{{ props.description }}</p>
+</template>
+
index.vue
<template>
+ <fp-component v-bind="{ title: 'FP Component', description: 'Only takes props' }" />
+</template>
+
+<script>
+import FPComponent from './fp-component';
+
+export default {
+ components: {
+ FPComponent
+ }
+}
+</script>
+
The benefits of using a Functional Component over a Stateful Component:
A renderless component is basically a component that does not render any HTML to the DOM but inside provides reusable JavaScript logic abstracted into a SFC.
A renderless component makes use of the Slots API in order to achieve what we want.
Renderless.vue
<script>
+export default {
+ render() {
+ return this.$scopedSlots.default({ name: 'John' });
+ }
+};
+</script>
+
The only job of Renderless.vue is to provide the prop name
App.vue
<template>
+ <renderless v-slot="{ name }">
+ <p>{{ name }}</p>
+ </renderless>
+</template>
+
+<script>
+import Renderless from './Renderless.vue';
+
+export default {
+ components: {
+ Renderless,
+ }
+};
+</script>
+
The neat thing about using a Renderless Component is that we can seperate our logic from our markup.
<template>
+ <div class="component-b"><component-a></component-a></div>
+</template>
+
+<script>
+import ComponentA from './ComponentA';
+
+export default {
+ components: {
+ ComponentA,
+ },
+};
+</script>
+
When you want to extend a single vue component
<template>
+ <button class="button-primary" @click.prevent="handleClick">
+ {{ buttonText }}
+ </button>
+</template>
+
+<script>
+import BaseButton from './BaseButton';
+
+export default {
+ extends: BaseButton,
+ props: ['buttonText'],
+};
+</script>
+
// closableMixin.js
+export default {
+ props: {
+ isOpen: {
+ default: true,
+ },
+ },
+ data: function() {
+ return {
+ shown: this.isOpen,
+ };
+ },
+ methods: {
+ hide: function() {
+ this.shown = false;
+ },
+ show: function() {
+ this.shown = true;
+ },
+ toggle: function() {
+ this.shown = !this.shown;
+ },
+ },
+};
+
<template>
+ <div
+ v-if="shown"
+ class="alert alert-success"
+ :class="'alert-' + type"
+ role="alert"
+ >
+ {{ text }}
+ <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
+ </div>
+</template>
+
+<script>
+import closableMixin from './mixins/closableMixin';
+
+export default {
+ mixins: [closableMixin],
+ props: ['text'],
+};
+</script>
+
2.6.0+
If you use Vue version above 2.6.0, Vue introduces new unified slot api, which is
v-slot
. +It replaces the slot and slot-scope attributes, which are deprecated, but have not been removed and are still documented here. +You can refer to deprecated API here.
<template>
+ <button class="btn btn-primary">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ name: 'VBtn',
+};
+</script>
+
<template>
+ <v-btn>
+ <span class="fa fa-user"></span> Login
+ </v-btn>
+</template>
+
+<script>
+import VBtn from './VBtn';
+
+export default {
+ components: {
+ VBtn,
+ },
+};
+</script>
+
BaseLayout.vue
<div class="container">
+ <header>
+ <slot name="header"></slot>
+ </header>
+ <main>
+ <slot></slot>
+ </main>
+ <footer>
+ <slot name="footer"></slot>
+ </footer>
+</div>
+
App.vue
<base-layout>
+ <template v-slot:header>
+ <h1>Here might be a page title</h1>
+ </template>
+
+ <p>A paragraph for the main content.</p>
+ <p>And another one.</p>
+
+ <template v-slot:footer>
+ <p>Here's some contact info</p>
+ </template>
+</base-layout>
+
Vue provides shorthand syntax for named slots.
+You can replace v-slot:
with #
.
<template>
+ <ul>
+ <li v-for="todo in todos" v-bind:key="todo.id">
+ <!-- We have a slot for each todo, passing it the -->
+ <!-- `todo` object as a slot prop. -->
+ <slot v-bind:todo="todo"> {{ todo.text }} </slot>
+ </li>
+ </ul>
+</template>
+
+<script>
+export default {
+ name: 'TodoList',
+ props: {
+ todos: {
+ type: Array,
+ default: () => [],
+ },
+ },
+};
+</script>
+
<template>
+ <todo-list v-bind:todos="todos">
+ <template v-slot:default="{ todo }">
+ <span v-if="todo.isComplete">✓</span>
+ {{ todo.text }}
+ </template>
+ </todo-list>
+</template>
+
+<script>
+import TodoList from "./TodoList";
+
+export default {
+ components: {
+ TodoList
+ },
+ data() {
+ return {
+ todos: [
+ { text: "todo 1", isComplete: true },
+ { text: "todo 2", isComplete: false },
+ { text: "todo 3", isComplete: false },
+ { text: "todo 4", isComplete: true }
+ ]
+ };
+ }
+};
+</script>
+
In most cases, you can use scoped slots instead of render props. But, it might be useful in some cases.
with SFC
<template>
+ <div id="app"><Mouse :render="__render" /></div>
+</template>
+
+<script>
+import Mouse from './Mouse.js';
+export default {
+ name: 'app',
+ components: {
+ Mouse,
+ },
+ methods: {
+ __render({ x, y }) {
+ return (
+ <h1>
+ The mouse position is ({x}, {y})
+ </h1>
+ );
+ },
+ },
+};
+</script>
+<style>
+* {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+</style>
+
with JSX
const Mouse = {
+ name: 'Mouse',
+ props: {
+ render: {
+ type: Function,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ x: 0,
+ y: 0,
+ };
+ },
+ methods: {
+ handleMouseMove(event) {
+ this.x = event.clientX;
+ this.y = event.clientY;
+ },
+ },
+ render(h) {
+ return (
+ <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
+ {this.$props.render(this)}
+ </div>
+ );
+ },
+};
+
+export default Mouse;
+
Sometimes, you may want to pass props and listeners to a child component without having to declare all props for the child component.
+You can bind $attrs
and $listeners
in the child component and set inheritAttrs
to false
(otherwise both, div
and child-component
will receive the attributes)
<template>
+ <div>
+ <h1>{{title}}</h1>
+ <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
+ </div>
+</template>
+
+<script>
+import PassingPropsChild from './PassingPropsChild';
+
+export default {
+ components: {
+ PassingPropsChild,
+ },
+ inheritAttrs: false,
+ props: {
+ title: {
+ type: String,
+ default: 'Hello, Vue!',
+ },
+ },
+};
+</script>
+
From the parent component, you can do this:
<template>
+ <p class="demo">
+ <passing-props
+ title="This is from <passing-props />"
+ childPropA="This is from <passing-props-child />"
+ @click="handleClickPassingPropsChildComponent"
+ >
+ </passing-props>
+ </p>
+</template>
+
+<script>
+import PassingProps from './PassingProps';
+
+export default {
+ components: {
+ PassingProps,
+ },
+ methods: {
+ handleClickPassingPropsChildComponent() {
+ console.log('This event comes from `<passing-props-child />`');
+ alert('This event comes from `<passing-props-child />`');
+ },
+ },
+};
+</script>
+
The Provider / Consumer pattern is very simple, it aims at separating stateful logic from the presentation. We need two components to create this pattern.
Provider.vue
<template>
+ <div>
+ <slot v-bind="{ state, actions }" />
+ </div>
+</template>
+
+<script>
+export default {
+ computed: {
+ state() {
+ return {
+ label: 'button',
+ };
+ },
+ actions() {
+ return {
+ click: this.click,
+ };
+ },
+ },
+ methods: {
+ click() {
+ console.log('Clicked');
+ },
+ },
+}
+</script>
+
Provider.vue
is responsible for containing all the stateful logic, we are successfully separating it from the presentation. We are making use of the Slots API
as a data provider.
Consumer.vue
<template functional>
+ <div>
+ <p>{{ props.state.label }}</p>
+ <button @click="props.actions.click">CLICK</button>
+ </div>
+</template>
+
Consumer.vue
is responsible for containing the presentation, note that we are using a Functional Component.
App.vue
<template>
+ <provider v-slot="{ state, actions }">
+ <consumer v-bind="{ state, actions }" />
+ </provider>
+</template>
+
+<script>
+import Provider from './Provider.vue';
+import Consumer from './Consumer.vue';
+
+export default {
+ components: {
+ Provider,
+ Consumer,
+ },
+};
+</script>
+
This pattern provides a neat way of allowing us to compose clean and decoupled components. Check out the example on CodeSandbox
Vue supports provide / inject mechanism to provide object
into all its descendants, regardless of how deep the component hierarchy is, as long as they are in the same parent chain. Notice that provide
and inject
bindings are not reactive, unless you pass down an observed object.
<parent-component>
+ <child-component>
+ <grand-child-component></grand-child-component>
+ </child-component>
+</parent-component>
+
With the above example component hierarchy, in order to derive data from parent-component
, you should pass down data(object) as props
to child-component
and grand-child-component
. However, if parent-component
provide
data(object), grand-child-component
can just define inject
provided object from parent-component
.
TIP
You can also use vue-property-decorator's @Provide
, @Inject
<script>
+export default {
+ provide: {
+ theme: {
+ primaryColor: '#3eaf7c',
+ secondaryColor: '#1FA2FF'
+ },
+ },
+ render(h) {
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor) }">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ inject: {
+ theme: {
+ default: {},
+ },
+ },
+ props: {
+ primary: {
+ type: Boolean,
+ default: false,
+ },
+ secondary: {
+ type: Boolean,
+ default: false,
+ },
+ },
+};
+</script>
+
<theme-provider>
+ <p class="demo">
+ <button class="btn">Normal Button</button>
+ <theme-button secondary>Themed Button</theme-button>
+ </p>
+</theme-provider>
+
errorCaptured
Hook<script>
+export default {
+ name: 'ErrorBoundary',
+ data() {
+ return {
+ error: false,
+ errorMessage: '',
+ };
+ },
+ errorCaptured(err, vm, info) {
+ this.error = true;
+ this.errorMessage = `Sorry, error occured in ${info}`;
+
+ return false;
+ },
+ render(h) {
+ if (this.error) {
+ return h('p', { class: 'demo bg-danger' }, this.errorMessage);
+ }
+
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <p class="demo">
+ <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ watch: {
+ count() {
+ throw new Error('error');
+ },
+ },
+ methods: {
+ throwError() {
+ this.count++;
+ },
+ },
+};
+</script>
+
<error-boundary> <throw-error></throw-error> </error-boundary>
+
watch on create
// don't
+created() {
+ this.fetchUserList();
+},
+watch: {
+ searchText: 'fetchUserList',
+}
+
// do
+watch: {
+ searchText: {
+ handler: 'fetchUserList',
+ immediate: true,
+ }
+}
+
+ Useful Links + + → +
<template>
+ <p class="demo">
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+};
+</script>
+
+<style scoped>
+.btn-primary {
+ display: inline-block;
+ font-size: 1.2rem;
+ color: #fff;
+ background-color: #3eaf7c;
+ padding: 0.8rem 1.6rem;
+ border-radius: 4px;
+ transition: background-color 0.1s ease;
+ box-sizing: border-box;
+ border-bottom: 1px solid #389d70;
+}
+</style>
+
Vue.component('my-btn', {
+ template: `
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+ `,
+ data() {
+ return {
+ text: 'Click me',
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ }
+ }
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ methods: {
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ },
+ },
+ render(h) {
+ return h(
+ 'button',
+ {
+ attrs: {
+ class: 'btn-primary',
+ },
+ on: {
+ click: this.handleClick,
+ },
+ },
+ this.$slots.default
+ );
+ },
+});
+
Vue.component('my-btn', {
+ data() {
+ return {
+ text: 'Click me',
+ };
+ },
+ methods: {
+ handleClick() {
+ console.log('clicked');
+ },
+ },
+ render() {
+ return (
+ <button class="btn-primary" @click.prevent="handleClick">
+ {this.$slots.default}(clicked - {{count}})
+ </button>
+ );
+ },
+});
+
<template>
+ <button class="btn-primary" @click.prevent="handleClick">
+ <slot></slot>(clicked - {{count}})
+ </button>
+</template>
+
+<script>
+import Vue from 'vue';
+import Component from 'vue-class-component';
+
+@Component
+export default MyBtn extends Vue {
+ count = 0;
+
+ handleClick() {
+ this.count++;
+ console.log('clicked', this.count);
+ }
+}
+</script>
+
+<style scoped>
+.btn-primary {
+ background-color: blue;
+}
+</style>
+
В целом, компонент Vue следует однонаправленному потоку данных, то есть входные параметры передаются вниз (см. официальное руководство), а события — наверх. Входные параметры — это данные только для чтения, поэтому невозможно изменить входные параметры дочерних компонентов. При изменении входных параметров, дочерние компоненты будут автоматически повторно отрендерены (входные параметры являются реактивными источниками данных). Дочерние компоненты могут генерировать событие только к непосредственному родительскому компоненту, так что он может изменять data
, сопоставляемые с props
дочернего компонента.
<template>
+ <button @click="$emit('click')">{{text}}</button>
+</template>
+
+<script>
+export default {
+ name: 'v-btn',
+ props: {
+ text: String,
+ },
+};
+</script>
+
<template>
+ <v-btn :text="buttonText" @click="handleClick"></v-btn>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ clickCount: 0,
+ buttonText: 'initial button text',
+ };
+ },
+ methods: {
+ handleClick() {
+ this.buttonText = `Button clicked ${++this.clickCount}`;
+ console.log('clicked', this.buttonText);
+ },
+ },
+};
+</script>
+
v-if
/ v-else
/ v-else-if
/ v-show
)v-if
<h1 v-if="true">Рендеринг только, если условие v-if равняется true</h1>
+
Использование v-if
и v-else
<h1 v-if="true">Рендеринг только, если условие v-if равняется true</h1>
+<h1 v-else>Рендеринг только, если условие v-if равняется false</h1>
+
Использование v-else-if
<div v-if="type === 'A'">Рендеринг только, если `type` равняется `A`</div>
+<div v-else-if="type === 'B'">Рендеринг только, если `type` равняется `B`</div>
+<div v-else-if="type === 'C'">Рендеринг только, если `type` равняется `C`</div>
+<div v-else>Рендеринг если `type` не равен ни `A`, ни `B`, ни `C`</div>
+
Использование v-show
<h1 v-show="true">Всегда рендерится, но виден только в том случае, если условия `v-show` равняются true</h1>
+
Если вы хотите по условию отобразить более одного элемента, вы можете использовать директивы (v-if
/ v-else
/ v-else-if
/v-show
) на элементе <template>
. Обратите внимание, что элемент <template>
фактические не будет отображаться в DOM. Это как невидимая обёртка.
<template v-if="true">
+ <h1>Все элементы</h1>
+ <p>будут отрендерены в DOM,</p>
+ <p>за исключением элемента `template`</p>
+</template>
+
Если вы используете JSX в своем Vue-приложении, то можете применять все техники, например использования выражения if else
и switch case
, а также тернарные и логические операторы.
Использование выражения if else
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ if (this.isTruthy) {
+ return <h1>Рендеринг, если значение равно true</h1>;
+ } else {
+ return <h1>Рендеринг, если значение равно false</h1>;
+ }
+ },
+};
+
Использование выражения switch case
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ switch (this.type) {
+ case 'info':
+ return <Info text={text} />;
+ case 'warning':
+ return <Warning text={text} />;
+ case 'error':
+ return <Error text={text} />;
+ default:
+ return <Success text={text} />;
+ }
+ },
+};
+
Или можно использовать сопоставление с помощью объекта для упрощения выражений switch case
import Info from './Info';
+import Warning from './Warning';
+import Error from './Error';
+import Success from './Success';
+
+const COMPONENT_MAP = {
+ info: Info,
+ warning: Warning,
+ error: Error,
+ success: Success,
+};
+
+export default {
+ data() {
+ return {
+ type: 'error',
+ };
+ },
+ render(h) {
+ const Comp = COMPONENT_MAP[this.type || 'success'];
+
+ return <Comp />;
+ },
+};
+
Использование тернарного оператора
export default {
+ data() {
+ return {
+ isTruthy: true,
+ };
+ },
+ render(h) {
+ return (
+ <div>
+ {this.isTruthy ? (
+ <h1>Рендеринг, если значение равно true</h1>
+ ) : (
+ <h1>Рендеринг, если значение равно false</h1>
+ )}
+ </div>
+ );
+ },
+};
+
Использование логического оператора
export default {
+ data() {
+ return {
+ isLoading: true,
+ };
+ },
+ render(h) {
+ return <div>{this.isLoading && <h1>Загрузка ...</h1>}</div>;
+ },
+};
+
<component>
с атрибутом is
<component :is="currentTabComponent"></component>
+
В приведённом выше примере отрендеренный компонент будет уничтожаться, если другой компонент должен будет рендериться в <component>
. Если необходимо, чтобы компоненты сохраняли свои экземпляры без их уничтожения в теге <component>
, можно обернуть <component>
в тег <keep-alive>
:
<keep-alive>
+ <component :is="currentTabComponent"></component>
+</keep-alive>
+
<template>
+ <div class="component-b">
+ <component-a></component-a>
+ </div>
+</template>
+
+<script>
+import ComponentA from './ComponentA';
+
+export default {
+ components: {
+ ComponentA,
+ },
+};
+</script>
+
Если вы хотите расширить один Vue-компонент, можно поступить следующим образом:
<template>
+ <button class="button-primary" @click.prevent="handleClick">
+ {{buttonText}}
+ </button>
+</template>
+
+<script>
+import BaseButton from './BaseButton';
+
+export default {
+ extends: BaseButton,
+ props: ['buttonText'],
+};
+</script>
+
// closableMixin.js
+export default {
+ props: {
+ isOpen: {
+ default: true
+ }
+ },
+ data: function() {
+ return {
+ shown: this.isOpen
+ }
+ },
+ methods: {
+ hide: function() {
+ this.shown = false;
+ },
+ show: function() {
+ this.shown = true;
+ },
+ toggle: function() {
+ this.shown = !this.shown;
+ }
+ }
+}
+
<template>
+ <div v-if="shown" class="alert alert-success" :class="'alert-' + type" role="alert">
+ {{ text }}
+ <i class="pull-right glyphicon glyphicon-remove" @click="hide"></i>
+ </div>
+</template>
+
+<script>
+import closableMixin from './mixins/closableMixin';
+
+export default {
+ mixins: [closableMixin],
+ props: ['text']
+};
+</script>
+
<template>
+ <button class="btn btn-primary">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ name: 'VBtn',
+};
+</script>
+
<template>
+ <v-btn>
+ <span class="fa fa-user"></span>
+ Логин
+ </v-btn>
+</template>
+
+<script>
+import VBtn from './VBtn';
+
+export default {
+ components: {
+ VBtn,
+ }
+};
+</script>
+
BaseLayout.vue
<div class="container">
+ <header>
+ <slot name="header"></slot>
+ </header>
+ <main>
+ <slot></slot>
+ </main>
+ <footer>
+ <slot name="footer"></slot>
+ </footer>
+</div>
+
App.vue
<base-layout>
+ <template slot="header">
+ <h1>Здесь может быть заголовок страницы</h1>
+ </template>
+
+ <p>Абзац для основного контента.</p>
+ <p>И еще один.</p>
+
+ <template slot="footer">
+ <p>Здесь некоторые контактные данные</p>
+ </template>
+</base-layout>
+
<template>
+ <ul>
+ <li
+ v-for="todo in todos"
+ v-bind:key="todo.id"
+ >
+ <!-- У нас есть слот для каждого todo, передавая его -->
+ <!-- в объект `todo` в виде входного параметра для слота. -->
+ <slot v-bind:todo="todo">
+ {{ todo.text }}
+ </slot>
+ </li>
+ </ul>
+</template>
+
+<script>
+export default {
+ name: 'TodoList',
+ props: {
+ todos: {
+ type: Array,
+ default: () => ([]),
+ }
+ },
+};
+</script>
+
<template>
+ <todo-list v-bind:todos="todos">
+ <template slot-scope="{ todo }">
+ <span v-if="todo.isComplete">✓</span>
+ {{ todo.text }}
+ </template>
+ </todo-list>
+</template>
+
+<script>
+import TodoList from './TodoList';
+
+export default {
+ components: {
+ TodoList,
+ },
+ data() {
+ return {
+ todos: [
+ { todo: 'todo 1', isComplete: true },
+ { todo: 'todo 2', isComplete: false },
+ { todo: 'todo 3', isComplete: false },
+ { todo: 'todo 4', isComplete: true },
+ ];
+ };
+ },
+};
+</script>
+
В большинстве случаев вы можете использовать слоты с ограниченной областью видимости вместо рендеринга входных параметров. Но в некоторых случаях это может быть полезно.
С однофайловым компонентом SFC
<template>
+ <div id="app">
+ <Mouse :render="__render"/>
+ </div>
+</template>
+
+<script>
+import Mouse from './Mouse.js';
+export default {
+ name: 'app',
+ components: {
+ Mouse,
+ },
+ methods: {
+ __render({ x, y }) {
+ return (
+ <h1>
+ The mouse position is ({x}, {y})
+ </h1>
+ );
+ },
+ },
+};
+</script>
+<style>
+- {
+ margin: 0;
+ height: 100%;
+ width: 100%;
+}
+</style>
+
С использованием JSX
const Mouse = {
+ name: 'Mouse',
+ props: {
+ render: {
+ type: Function,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ x: 0,
+ y: 0,
+ };
+ },
+ methods: {
+ handleMouseMove(event) {
+ this.x = event.clientX;
+ this.y = event.clientY;
+ },
+ },
+ render(h) {
+ return (
+ <div style={{ height: '100%' }} onMousemove={this.handleMouseMove}>
+ {this.$props.render(this)}
+ </div>
+ );
+ },
+};
+
+export default Mouse;
+
Иногда вам может понадобиться передать входные параметры и обработчики дочернему компоненту, не объявляя всех входных параметров дочернего компонента. Вы можете привязать $attrs
и $listeners
в дочернем компоненте и установить inheritAttrs
на false
(в противном случае div
и child-component
получат атрибуты).
<template>
+ <div>
+ <h1>{{title}}</h1>
+ <passing-props-child v-bind="$attrs" v-on="$listeners"></passing-props-child>
+ </div>
+</template>
+
+<script>
+import PassingPropsChild from './PassingPropsChild';
+
+export default {
+ components: {
+ PassingPropsChild,
+ },
+ inheritAttrs: false,
+ props: {
+ title: {
+ type: String,
+ default: 'Hello, Vue!',
+ },
+ },
+};
+</script>
+
Из родительского компонента вы можете сделать следующее:
<template>
+ <p class="demo">
+ <passing-props
+ title="This is from <passing-props />"
+ childPropA="This is from <passing-props-child />"
+ @click="handleClickPassingPropsChildComponent"
+ >
+ </passing-props>
+ </p>
+</template>
+
+<script>
+import PassingProps from './PassingProps';
+
+export default {
+ components: {
+ PassingProps,
+ },
+ methods: {
+ handleClickPassingPropsChildComponent() {
+ console.log('This event comes from `<passing-props-child />`');
+ alert('This event comes from `<passing-props-child />`');
+ },
+ },
+};
+</script>
+
Vue поддерживает механизм предоставления и внедрения объекта во всех потомки, независимо от глубины иерархии компонентов, при условии, что компоненты находятся в одной и той же цепочке родителей. Обратите внимание, что привязки provide
и inject
*не являются- реактивными, пока вы не передадите наблюдаемый объект.
<parent-component>
+ <child-component>
+ <grand-child-component></grand-child-component>
+ </child-component>
+</parent-component>
+
С приведенной выше иерархией компонентов в качестве примера для получения данных из parent-component
вам нужно передавать данные (объект) в качестве props
компоненту child-component
и компоненту grand-child-component
. Однако, если parent-component
предоставляет (provide
) данные (объект), grand-child-component
может просто определить свойство inject
для получения объекта, предоставляемого parent-component
.
TIP
Вы также можете использовать @Provide
, @Inject
из vue-property-decorator
<script>
+export default {
+ provide: {
+ theme: {
+ primaryColor: '#3eaf7c',
+ secondaryColor: '#1FA2FF'
+ },
+ },
+ render(h) {
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <button class="btn" :style="{ color: '#fff', backgroundColor: (primary && theme.primaryColor) || (secondary && theme.secondaryColor) }">
+ <slot></slot>
+ </button>
+</template>
+
+<script>
+export default {
+ inject: {
+ theme: {
+ default: {},
+ },
+ },
+ props: {
+ primary: {
+ type: Boolean,
+ default: false,
+ },
+ secondary: {
+ type: Boolean,
+ default: false,
+ },
+ },
+};
+</script>
+
<theme-provider>
+ <theme-button secondary>Themed Button</theme-button>
+</theme-provider>
+
errorCaptured
<script>
+export default {
+ name: 'ErrorBoundary',
+ data() {
+ return {
+ error: false,
+ errorMessage: '',
+ };
+ },
+ errorCaptured(err, vm, info) {
+ this.error = true;
+ this.errorMessage = `Sorry, error occured in ${info}`;
+
+ return false;
+ },
+ render(h) {
+ if (this.error) {
+ return h('p', { class: 'demo bg-danger' }, this.errorMessage);
+ }
+
+ return this.$slots.default[0];
+ },
+};
+</script>
+
<template>
+ <p class="demo">
+ <button class="btn btn-danger" @click.prevent="throwError()">Error Thrown Button ({{count}})</button>
+ </p>
+</template>
+
+<script>
+export default {
+ data() {
+ return {
+ count: 0,
+ };
+ },
+ watch: {
+ count() {
+ throw new Error('error');
+ },
+ },
+ methods: {
+ throwError() {
+ this.count++;
+ },
+ },
+};
+</script>
+
<error-boundary>
+ <throw-error></throw-error>
+</error-boundary>
+
Наблюдение при создании
// don't
+created() {
+ this.fetchUserList();
+},
+watch: {
+ searchText: 'fetchUserList',
+}
+
// do
+watch: {
+ searchText: {
+ handler: 'fetchUserList',
+ immediate: true,
+ }
+}
+
+ Полезные ссылки + + → +
+ ← + + Полезные ссылки + + Переводы + + → +
+ ← + + Паттерны + + Книга Fullstack Vue + + → +
+ ← + + Useful Links + + Translations + + → +
+ ← + + Patterns + + Fullstack Vue Book + + → +
Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.
Alternative Proxies: