DEV Community

Daniel P πŸ‡¨πŸ‡¦
Daniel P πŸ‡¨πŸ‡¦

Posted on

Trying the composition API in Vue3

Trying the composition API in Vue3

On October 4th, The Vue-3 code was made public at github.com/vuejs/vue-next
It is still in pre-alpha monorepo that you can't quite use the same way as Vue2 just yet. There is also functionality from Vue2 that hasn't reached parity yet. At the time of writing, these are SSR, kep-alive, transitions, DOM-specific transforms (v-on, v-dom, etc.)

It does allow us, however, to start playing with though.

I spent the last couple nights trying to make some sample code to work. Unfortunately, the going was a bit tough. I wanted to start by making a parent component pass a prop to a child component. It took me a lot longer than I expected, but that was mostly my fault. I'll try to describe the path I took to get it eventually to work, and some of the mistakes I've made.

Get the repo

I started by downloading the git repo with the following commands

# clone the vue-next mono repo to vue-next folder
git clone https://github.com/vuejs/vue-next.git

cd vue-next

# install dependencies
npm install

# build vue.global.js
npm run dev
Enter fullscreen mode Exit fullscreen mode

this will generate a vue.global.js file at packages\vue\dist\vue.global.js

Of course later I realized that the best place to start is here: https://github.com/vuejs/vue-next/blob/master/.github/contributing.md

The problem I had was that the file that was generated mounts Vue as a global, so is not suited for use with bundlers like parcel or webpack, which as what I was trying to use it with. In the contributing link from the repo, there are further instructions for generating other builds, but I've decided to use the global package instead with a simple file server (like serve or http-sever), it even works on online code editors like jsfiddle, which I ended up using.

I've found a sample code from the vue-composition-api-rfc at https://vue-composition-api-rfc.netlify.com/#basic-example

<template>
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>

<script>
  import { reactive, computed } from "vue";

  export default {
    setup() {
      const state = reactive({
        count: 0,
        double: computed(() => state.count * 2)
      });

      function increment() {
        state.count++;
      }

      return {
        state,
        increment
      };
    }
  };
</script>
Enter fullscreen mode Exit fullscreen mode

First I made it available by uploading it to gitlab as a gist and generating a rawgist link for includeing in a jsfiddle

https://gistcdn.githack.com/dasDaniel/f3cebc1274339055729c6e887ca8d2ad/raw/8f0432bfd812c453cdecee61862963fe3e24119a/vue.global.js

I had to make some changes to make it work with the global package, since that doesn't support Single File Components.

HTML:

<div id="app"></div>

<template id="appTemplate">
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
</template>
Enter fullscreen mode Exit fullscreen mode

JS:

const { reactive, computed } = Vue

const MyComponent = {
  setup(props) {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    });

    function increment() {
      state.count++
    }

    return {
      state,
      increment
    };
  },
  template: document.getElementById("appTemplate").innerHTML
};

const app = Vue.createApp({})
app.mount(MyComponent, "#app")
Enter fullscreen mode Exit fullscreen mode

As you can see, instead of using template literals or SFC, I used the <template> html tag and referenced it with getElementById. Otherwise it's pretty much the same.

The next goal was to add another component and pass a prop to it.

I added the following code to the appTemplate

<my-component :count="state.count"></my-component>
Enter fullscreen mode Exit fullscreen mode

and this to the script


const MyComponent = Vue.createComponent({
  template: `<div>my component {{count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    return {...props}
  }
})
Enter fullscreen mode Exit fullscreen mode

I also registered the component before mounting the app

app.component('my-component', MyComponent)
Enter fullscreen mode Exit fullscreen mode

The result was that the prop was passed initially with a value of 0, and then it didn't update any after that. So I tried copying what the app does.

const MyComponent = Vue.createComponent({
  template: `<div>my component {{state.count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    const state = reactive({...props})
    return {
        state
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

Shis still doesn't work and it's not clear to me why.

Now is the time when I frantically try a hundred different things and nothing seems to work. I could list all the things, but I'll just mention a few.

// added `onUpdated`
const { reactive, computed, onUpdated } = Vue;

const MyComponent = Vue.createComponent({
  template: `<div>my component {{state.count}}<div>`,
  props: {
    count: Number
  },
  setup(props){
    const state = reactive({...props})
    // added onUpdated function
    onUpdated(() => {
        state.count = props.count
    })
    return {
        state
    }
  }
});
Enter fullscreen mode Exit fullscreen mode

When I console logged the state after the update, the state did change, but the template did not update. This wasn't making any sense.

Eventually after more reading and debugging I've found out two things.

The way to do it correctly from what I gather is to use reactive AND computed

  const state = reactive({
    count: computed(() => props.count)
  })
Enter fullscreen mode Exit fullscreen mode

The other thing that I eventually noticed, was that my div tag wasn't closed. This was causing the layout to only render the first time, and likely why I may have tried something that should have been working (like using onUpdate) and weren't.

the working code (https://jsfiddle.net/dapo/fp34wj6z/)

<div id="app"></div>

<template id="appTemplate">
  <button @click="increment">
    Count is: {{ state.count }}, double is: {{ state.double }}
  </button>
  <pre>{{ state }}</pre>
  <my-component :count="state.count"></my-component>
</template>

<template id="myComponent">
  <div>
    Child
    <pre>{{ state }}</pre>
  </div>
</template>
Enter fullscreen mode Exit fullscreen mode
const {
  reactive,
  computed,
} = Vue;

const MyComponent = Vue.createComponent({
  props: {
    count: Number
  },
  setup(props) {
    const state = reactive({
      count: computed(() => props.count),
      triple: computed(() => props.count * 3)
    });

    return {
      state,
    }
  },
  template: document.getElementById('myComponent').innerHTML
});

const App = {
  setup(props) {
    const state = reactive({
      count: 0,
      double: computed(() => state.count * 2)
    });

    function increment() {
      state.count++;
    }

    return {
      state,
      increment
    };
  },
  template: document.getElementById('appTemplate').innerHTML
};

const app = Vue.createApp({});
app.component('my-component', MyComponent)
app.mount(App, "#app");
Enter fullscreen mode Exit fullscreen mode

TL;DR;

  • I didn't watch my template syntax and missed that a tag wasn't closed, which was causing the rendering not to work properly.
  • should have spent more time reading the docs than trying madly to just get it to work.

Resources :

Anyway, I'll be playing more with this in the future. I certainly subscribe to the benefits of the composition API, I just need to spend some more time understanding the how ref, reactive, computed, and watch all work together.

Discussion (2)

Collapse
dasdaniel profile image
Daniel P πŸ‡¨πŸ‡¦ Author

Currently v-model is not supported. So I've used a react-useState-style helper function to have reusable functionality (somewhat) similar to v-model.

jsfiddle.net/dapo/a1fsw23n/

It's amazing what RTFMing can do for your understanding.

Collapse
bennaceurhichem profile image
Bennaceur Hichem

I have a vue 2 component that i couldn't make using defineComponent correctly, I need some help if you don't mind

<template>
  <client-only>
    <litepie-datepicker
      :as-single="asSingle"
      :overlay="overlayDatepicker"
      :disable-date="disableDate"
      :shortcuts="dateShortcuts"
      :disable-in-range="disableInRange"
      :options="datepickerOptions"
      :i18n="i18n"
      :auto-apply="autoApply"
      :trigger="triggerDatepicker"
      v-model="context.model"
    />
  </client-only>
</template>
<script lang="ts">
import { ssrRef, watch } from '@nuxtjs/composition-api';
import LitepieDatepicker from 'vue2-litepie-datepicker';
import { Vue, Component, Prop } from 'vue-property-decorator';
@Component({
  name: 'datepicker',
  components: {
    LitepieDatepicker,
  },
})
export default class Datepicker extends Vue {
  @Prop({
    type: Object,
    required: true,
  })
  private context!: any;
  private asSingle: Boolean = false;
  private overlayDatepicker: Boolean = false;
  private dateShortcuts: Boolean = false;
  private disableInRange: Boolean = false;
  private triggerDatepicker = '';
  private autoApply: Boolean = false;
  private i18n = '';
  private disableDate = '';
  private slot = null;
  private datepickerOptions = null;
  setup(props) {
    const myRef = ssrRef(null);
    const dateValue = ssrRef([]);
    const formatter = ssrRef({
      date: 'DD MMM YYYY',
      month: 'MMM',
    });
    watch(dateValue, (oldValue, newValue) => {
      props.context.model = newValue;
    });
    return {
      myRef,
      dateValue,
      formatter,
    };
  }

  mounted() {
    this.asSingle = this.context?.attributes['as-single'];
    this.overlayDatepicker = this.context?.attributes['overlay-datepicker'];
    this.disableDate = this.context?.attributes['disable-date'];
    this.dateShortcuts = this.context?.attributes['date-shortcuts'];
    this.disableInRange = this.context?.attributes['disable-in-range'];
    this.triggerDatepicker = this.context?.attributes['trigger-datepicker'];
    this.datepickerOptions = this.context?.attributes['datepicker-options'];
    this.slot = this.context?.attributes['v-slot-datepicker'];
    this.i18n = this.context?.attributes['i18n-datepicker'];
    this.autoApply = this.context?.attributes['auto-apply'];
  }
}
</script>

<style lang="scss">
*[style*='display: none'] {
  display: none !important;
}

.focus\:ring:focus {
  @apply ring-2 focus:ring-primary-600;
}

.border-litepie-secondary-300 {
  @apply border-solid border border-gray-300 focus:ring-gray-300 focus:border-primary-300;
}

.border-litepie-secondary-300:hover {
  @apply ring-1 hover:bg-gray-200 ring-gray-300;
}
#litepie {
  @apply my-2;
}

.bg-litepie-primary-500 {
  @apply bg-primary-500;
}
.text-litepie-primary-600 {
  @apply text-primary-600;
}
.bg-litepie-secondary-100 {
  @apply bg-primary-100;
}
.text-litepie-secondary-700 {
  @apply text-gray-700;
}

.placeholder-litepie-secondary-400 {
  @apply placeholder-gray-300;
}

.text-litepie-secondary-400 {
  @apply text-gray-400;
}
</style>

Enter fullscreen mode Exit fullscreen mode

how can I use existed props here into defineComponent and make things work worrectly, because I need to pass the formatter to change the datepicker format and without defineComponent it won't works