diff --git a/package.json b/package.json index 884c6c2db9..5572e638e2 100644 --- a/package.json +++ b/package.json @@ -5,5 +5,8 @@ "scripts": { "serve": "vuepress dev src", "build": "vuepress build src" + }, + "dependencies": { + "axios": "^0.19.1" } } diff --git a/src/.vuepress/components/computed-1.vue b/src/.vuepress/components/computed-1.vue new file mode 100644 index 0000000000..a9634ee11a --- /dev/null +++ b/src/.vuepress/components/computed-1.vue @@ -0,0 +1,24 @@ + + + diff --git a/src/.vuepress/components/computed-2.vue b/src/.vuepress/components/computed-2.vue new file mode 100644 index 0000000000..2a194f208b --- /dev/null +++ b/src/.vuepress/components/computed-2.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/.vuepress/config.js b/src/.vuepress/config.js index e6feb5592e..78f610d547 100644 --- a/src/.vuepress/config.js +++ b/src/.vuepress/config.js @@ -25,7 +25,14 @@ module.exports = { ], sidebarDepth: 2, sidebar: { - '/guide/': ['installation', 'introduction', 'instance', 'template-syntax'] + '/guide/': [ + 'installation', + 'introduction', + 'instance', + 'template-syntax', + 'computed', + 'class-and-style' + ] } }, plugins: { @@ -39,4 +46,4 @@ module.exports = { } } } -}; +} diff --git a/src/guide/class-and-style.md b/src/guide/class-and-style.md new file mode 100644 index 0000000000..e5ea4952d5 --- /dev/null +++ b/src/guide/class-and-style.md @@ -0,0 +1,223 @@ +# Class and Style Bindings + +A common need for data binding is manipulating an element's class list and its inline styles. Since they are both attributes, we can use `v-bind` to handle them: we only need to calculate a final string with our expressions. However, meddling with string concatenation is annoying and error-prone. For this reason, Vue provides special enhancements when `v-bind` is used with `class` and `style`. In addition to strings, the expressions can also evaluate to objects or arrays. + +## Binding HTML Classes + +[Watch a free video lesson on Vue School](https://vueschool.io/lessons/vuejs-dynamic-classes?friend=vuejs) + +### Object Syntax + +We can pass an object to `v-bind:class` to dynamically toggle classes: + +```html +
+``` + +The above syntax means the presence of the `active` class will be determined by the [truthiness](https://developer.mozilla.org/en-US/docs/Glossary/Truthy) of the data property `isActive`. + +You can have multiple classes toggled by having more fields in the object. In addition, the `v-bind:class` directive can also co-exist with the plain `class` attribute. So given the following template: + +```html +
+``` + +And the following data: + +```js +data: { + isActive: true, + hasError: false +} +``` + +It will render: + +```html +
+``` + +When `isActive` or `hasError` changes, the class list will be updated accordingly. For example, if `hasError` becomes `true`, the class list will become `"static active text-danger"`. + +The bound object doesn't have to be inline: + +```html +
+``` + +```js +data: { + classObject: { + active: true, + 'text-danger': false + } +} +``` + +This will render the same result. We can also bind to a [computed property](computed.md) that returns an object. This is a common and powerful pattern: + +```html +
+``` + +```js +data: { + isActive: true, + error: null +}, +computed: { + classObject() { + return { + active: this.isActive && !this.error, + 'text-danger': this.error && this.error.type === 'fatal' + } + } +} +``` + +### Array Syntax + +We can pass an array to `v-bind:class` to apply a list of classes: + +```html +
+``` + +```js +data: { + activeClass: 'active', + errorClass: 'text-danger' +} +``` + +Which will render: + +```html +
+``` + +If you would like to also toggle a class in the list conditionally, you can do it with a ternary expression: + +```html +
+``` + +This will always apply `errorClass`, but will only apply `activeClass` when `isActive` is truthy. + +However, this can be a bit verbose if you have multiple conditional classes. That's why it's also possible to use the object syntax inside array syntax: + +```html +
+``` + +### With Components + +> This section assumes knowledge of [Vue Components](TODO:components.html). Feel free to skip it and come back later. + +When you use the `class` attribute on a custom component, those classes will be added to the component's root element. Existing classes on this element will not be overwritten. + +For example, if you declare this component: + +```js +const MyComponent = { + template: '

Hi!

' +} +``` + +Then add some classes when using it: + +```html +
+ +
+``` + +```js +Vue.createApp().mount( + { + components: { + 'my-component': MyComponent + } + }, + '#app' +) +``` + +The rendered HTML will be: + +```html +

Hi

+``` + +> TODO: needs a check after https://github.com/vuejs/rfcs/blob/attr-fallthrough/active-rfcs/0000-attr-fallthrough.md is merged + +The same is true for class bindings: + +```html + +``` + +When `isActive` is truthy, the rendered HTML will be: + +```html +

Hi

+``` + +## Binding Inline Styles + +### Object Syntax + +The object syntax for `v-bind:style` is pretty straightforward - it looks almost like CSS, except it's a JavaScript object. You can use either camelCase or kebab-case (use quotes with kebab-case) for the CSS property names: + +```html +
+``` + +```js +data: { + activeColor: 'red', + fontSize: 30 +} +``` + +It is often a good idea to bind to a style object directly so that the template is cleaner: + +```html +
+``` + +```js +data: { + styleObject: { + color: 'red', + fontSize: '13px' + } +} +``` + +Again, the object syntax is often used in conjunction with computed properties that return objects. + +### Array Syntax + +The array syntax for `v-bind:style` allows you to apply multiple style objects to the same element: + +```html +
+``` + +### Auto-prefixing + +When you use a CSS property that requires [vendor prefixes](https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix) in `v-bind:style`, for example `transform`, Vue will automatically detect and add appropriate prefixes to the applied styles. + +### Multiple Values + +You can provide an array of multiple (prefixed) values to a style property, for example: + +```html +
+``` + +This will only render the last value in the array which the browser supports. In this example, it will render `display: flex` for browsers that support the unprefixed version of flexbox. diff --git a/src/guide/computed.md b/src/guide/computed.md new file mode 100644 index 0000000000..0c6beba719 --- /dev/null +++ b/src/guide/computed.md @@ -0,0 +1,243 @@ +# Computed Properties and Watchers + +## Computed Properties + +[Learn how computed properties work with a free lesson on Vue School](https://vueschool.io/lessons/vuejs-computed-properties?friend=vuejs) + +In-template expressions are very convenient, but they are meant for simple operations. Putting too much logic in your templates can make them bloated and hard to maintain. For example: + +```html +
+ {{ message.split('').reverse().join('') }} +
+``` + +At this point, the template is no longer simple and declarative. You have to look at it for a second before realizing that it displays `message` in reverse. The problem is made worse when you want to include the reversed message in your template more than once. + +That's why for any complex logic, you should use a **computed property**. + +### Basic Example + +```html +
+

Original message: "{{ message }}"

+

Computed reversed message: "{{ reversedMessage }}"

+
+``` + +```js +const vm = Vue.createApp().mount( + { + data() { + return { + message: 'Hello' + } + }, + computed: { + // a computed getter + reversedMessage() { + // `this` points to the vm instance + return this.message + .split('') + .reverse() + .join('') + } + } + }, + '#example' +) +``` + +Result: + + + +Here we have declared a computed property `reversedMessage`. The function we provided will be used as the getter function for the property `vm.reversedMessage`: + +```js +console.log(vm.reversedMessage) // => 'olleH' +vm.message = 'Goodbye' +console.log(vm.reversedMessage) // => 'eybdooG' +``` + +You can open the sandbox(TODO) and play with the example vm yourself. The value of `vm.reversedMessage` is always dependent on the value of `vm.message`. + +You can data-bind to computed properties in templates just like a normal property. Vue is aware that `vm.reversedMessage` depends on `vm.message`, so it will update any bindings that depend on `vm.reversedMessage` when `vm.message` changes. And the best part is that we've created this dependency relationship declaratively: the computed getter function has no side effects, which makes it easier to test and understand. + +### Computed Caching vs Methods + +You may have noticed we can achieve the same result by invoking a method in the expression: + +```html +

Reversed message: "{{ reverseMessage() }}"

+``` + +```js +// in component +methods: { + reverseMessage() { + return this.message.split('').reverse().join('') + } +} +``` + +Instead of a computed property, we can define the same function as a method. For the end result, the two approaches are indeed exactly the same. However, the difference is that **computed properties are cached based on their reactive dependencies.** A computed property will only re-evaluate when some of its reactive dependencies have changed. This means as long as `message` has not changed, multiple access to the `reversedMessage` computed property will immediately return the previously computed result without having to run the function again. + +This also means the following computed property will never update, because `Date.now()` is not a reactive dependency: + +```js +computed: { + now() { + return Date.now() + } +} +``` + +In comparison, a method invocation will **always** run the function whenever a re-render happens. + +Why do we need caching? Imagine we have an expensive computed property **A**, which requires looping through a huge array and doing a lot of computations. Then we may have other computed properties that in turn depend on **A**. Without caching, we would be executing **A**’s getter many more times than necessary! In cases where you do not want caching, use a method instead. + +### Computed vs Watched Property + +Vue does provide a more generic way to observe and react to data changes on a Vue instance: **watch properties**. When you have some data that needs to change based on some other data, it is tempting to overuse `watch` - especially if you are coming from an AngularJS background. However, it is often a better idea to use a computed property rather than an imperative `watch` callback. Consider this example: + +```html +
{{ fullName }}
+``` + +```js +const vm = Vue.createApp().mount( + { + data() { + return { + firstName: 'Foo', + lastName: 'Bar', + fullName: 'Foo Bar' + } + }, + watch: { + firstName(val) { + this.fullName = val + ' ' + this.lastName + }, + lastName(val) { + this.fullName = this.firstName + ' ' + val + } + } + }, + '#demo' +) +``` + +The above code is imperative and repetitive. Compare it with a computed property version: + +```js +const vm = Vue.createApp().mount( + { + data() { + return { + firstName: 'Foo', + lastName: 'Bar' + } + }, + computed: { + fullName() { + return this.firstName + ' ' + this.lastName + } + } + }, + '#demo' +) +``` + +Much better, isn't it? + +### Computed Setter + +Computed properties are by default getter-only, but you can also provide a setter when you need it: + +```js +// ... +computed: { + fullName: { + // getter + get() { + return this.firstName + ' ' + this.lastName + }, + // setter + set(newValue) { + const names = newValue.split(' ') + this.firstName = names[0] + this.lastName = names[names.length - 1] + } + } +} +// ... +``` + +Now when you run `vm.fullName = 'John Doe'`, the setter will be invoked and `vm.firstName` and `vm.lastName` will be updated accordingly. + +## Watchers + +While computed properties are more appropriate in most cases, there are times when a custom watcher is necessary. That's why Vue provides a more generic way to react to data changes through the `watch` option. This is most useful when you want to perform asynchronous or expensive operations in response to changing data. + +For example: + +```html +
+

+ Ask a yes/no question: + +

+

{{ answer }}

+
+``` + +```html + + + + + + +``` + +Result: + + + +In this case, using the `watch` option allows us to perform an asynchronous operation (accessing an API) and sets a condition for performing this operation. None of that would be possible with a computed property. + +In addition to the `watch` option, you can also use the imperative [vm.\$watch API](TODO:../api/#vm-watch). diff --git a/yarn.lock b/yarn.lock index 24237d2aa4..b955fe45ab 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1364,6 +1364,13 @@ aws4@^1.8.0: resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.9.0.tgz#24390e6ad61386b0a747265754d2a17219de862c" integrity sha512-Uvq6hVe90D0B2WEnUqtdgY1bATGz3mw33nH9Y+dmA+w5DHvUmBgkr5rM/KCHpCsiFNRUfokW/szpPPgMK2hm4A== +axios@^0.19.1: + version "0.19.1" + resolved "https://registry.yarnpkg.com/axios/-/axios-0.19.1.tgz#8a6a04eed23dfe72747e1dd43c604b8f1677b5aa" + integrity sha512-Yl+7nfreYKaLRvAvjNPkvfjnQHJM1yLBY3zhqAwcJSwR/6ETkanUgylgtIvkvz0xJ+p/vZuNw8X7Hnb7Whsbpw== + dependencies: + follow-redirects "1.5.10" + babel-loader@^8.0.4: version "8.0.6" resolved "https://registry.yarnpkg.com/babel-loader/-/babel-loader-8.0.6.tgz#e33bdb6f362b03f4bb141a0c21ab87c501b70dfb" @@ -2338,6 +2345,13 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3, debug@^2.6.9: dependencies: ms "2.0.0" +debug@=3.1.0, debug@~3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" + integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== + dependencies: + ms "2.0.0" + debug@^3.0.0, debug@^3.1.1, debug@^3.2.5: version "3.2.6" resolved "https://registry.yarnpkg.com/debug/-/debug-3.2.6.tgz#e83d17de16d8a7efb7717edbe5fb10135eee629b" @@ -2352,13 +2366,6 @@ debug@^4.1.0, debug@^4.1.1: dependencies: ms "^2.1.1" -debug@~3.1.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/debug/-/debug-3.1.0.tgz#5bb5a0672628b64149566ba16819e61518c67261" - integrity sha512-OX8XqP7/1a9cqkxYw2yXss15f26NKWBpDXQd0/uK/KPqdQhxbPa994hnzjcE2VqQpDslf55723cKPUOGSmMY3g== - dependencies: - ms "2.0.0" - decamelize@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" @@ -3032,6 +3039,13 @@ flush-write-stream@^1.0.0: inherits "^2.0.3" readable-stream "^2.3.6" +follow-redirects@1.5.10: + version "1.5.10" + resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.5.10.tgz#7b7a9f9aea2fdff36786a94ff643ed07f4ff5e2a" + integrity sha512-0V5l4Cizzvqt5D44aTXbFZz+FtyXV1vrDN6qrelxtfYQKW0KO0W2T/hkE8xvGa/540LkZlkaUjO4ailYTFtHVQ== + dependencies: + debug "=3.1.0" + follow-redirects@^1.0.0: version "1.9.0" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.9.0.tgz#8d5bcdc65b7108fe1508649c79c12d732dcedb4f"