Skip to content

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

prop sugar #229

Closed
patak-dev opened this issue Nov 12, 2020 · 10 comments
Closed

prop sugar #229

patak-dev opened this issue Nov 12, 2020 · 10 comments

Comments

@patak-dev
Copy link
Member

patak-dev commented Nov 12, 2020

I want to avoid commenting on the Ref Sugar RFC about this because it may interfere in the discussion about ref:. I tried to find if the following proposal was discussed before but I couldn't find public discussions about it. Sorry for the noise if this was already analyzed.

Context

In the RFC it is stated that

... we believe ref:'s ergonomics value outweighs the cost by a fair margin. This is also why we are limiting this proposal to ref: only, since ref access is the only problem that requires alternative semantics to solve.

I think that it is on point, but there is also an asymmetry in the usage of props in the template and in the <script>. For simple components, we would end up with:

<script setup="props">
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: multiplier = 2
</script>
<template>
  <p @click="multiplier++">{{ `${num} by ${multiplier} = ${num*multiplier}` }}</p>
</template>

With ref sugar, we can move the onClick handler to the script without the .value. But if we want to move the multiplication out of the template, we can not directly copy it in a computed, because we need to add the props.

<script setup="props">
  import { computed } from 'vue' 
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: multiplier = 2
  ref: result = computed( () => props.num * multiplier )
</script>

We could convert the prop to a ref, taking care not to lose reactivity while destructuring (this is also something that we need also when passing the props to composables). We also need to explain what toRefs is achieving and the prop name needs to be repeated.

<script setup="props">
  import { computed, toRefs } from 'vue' 
  export default { 
    props: {
      num: { type: Number, default: 4 }
    }
  }
  ref: ({ num } = toRefs(props))
  ref: multiplier = 2
  ref: result = computed( () => num * multiplier )
</script>

The DX also suffers from extra boilerplate needed in this case. If ref: ends up being adopted, I think that having better ergonomics for props could also outweighs the cost of adding new syntax.

Proposal

Following the same model as ref:, we introduce a new label prop: to declare components props:

<script setup>
  import { computed } from 'vue'
  prop: num = 4
  ref: multiplier = 2
  ref: result = computed( () => num * multiplier )
</script>

When a prop is defined in this way, you can directly use its value in the template and in the script, and reactivity works properly.

Edited:
We could add a defineProp type-only helper to support complex validation of props:

<script setup>
import { defineProp } from 'vue'

prop: num = 4

prop: msg = defineProp({
  type: String,
  required: true
  validator: (val) => ![""].includes(val)
})
</script>

Implementation

  • When a variable is declared using prop:, it is aggregated to the props block of the component.
  • Every place where the variable is used it is replaced with props.num.
  • In the same way that with ref:, if you use $num it is replaced with toRef(props,'num'), so you get a ref that you can pass to composables. If a different syntax is pushed to get the reference from ref:, that will be the syntax for prop: too (the important part is that they work in the same way).

Benefits

  • Boilerplate is greatly reduced
  • Code that uses props can be moved from template and script without modifications to a computed
  • Getting a ref out of any reactive value (ref or prop) works in the same way (using $var, no need to explain toRefs and why reactivity is lost when destructuring right away when learning)
  • If you need to lift state (a ref) to become a prop, you can change ref: with prop: and refactoring is easier because you do not need to add props. and toRef in all the places the prop is being used.
  • props related to different features can be moved next to the code that uses them, making refactoring easier also for props (like splitting a component for example). It is the same principle used to justify ref, computed instead of the options API
  • Using prop: is optional, if you want a custom validator for example you can declare that props in the normal props block
  • We can use the same scheme for declaring default values and variable types used by svelte for props (looks like typescript support works fine for them now)

Cons

  • Cost in tooling to support the new syntax (it should be similar to the cost of ref:).
  • Extra syntax to explain, same as with ref:

Alternative

  • In case using prop: wants to be avoided, export let could be used to declare props as svelte does. But I think that prop: plays better with ref: for Vue.
@dajpes
Copy link

dajpes commented Nov 15, 2020

I really like the prop label, what if we wanna declare many props like this:

<script setup>

prop: num= {
      type:Boolean,
      default:4}
prop: msg= {
      type:String,
      required:true
      validator:(val) =>  ![""].includes(val)
}
</script>

@patak-dev
Copy link
Member Author

patak-dev commented Nov 15, 2020

@dajpes now that the <script setup> RFC uses definedOptions and I think that also defineProps are being considered given this commit to Volar by @johnsoncodehk we may have a good way to define multiple complex prop: based props using:

<script setup>
import { defineProp } from 'vue'

prop: num = 4

prop: msg = defineProp({
  type: String,
  required: true
  validator: (val) => ![""].includes(val)
})
</script>

So we can still define each prop: next to the code it is related to if we want, and we do not need to fallback to defineOptions or defineProps for complex prop definition.

People could also use prop: together with defineProps like you spread multiple ref:s

<script setup>
import { defineProps } from 'vue'

prop: ({ num, msg } = defineProps({
  num: { 
    type: Number, 
    default: 4
  }, 
  msg: {
    type: String,
    required: true
    validator: (val) => ![""].includes(val)
  }
}))
</script>

@johnsoncodehk
Copy link
Member

@dajpes now that the <script setup> RFC uses definedOptions and I think that also defineProps are being considered given this commit to Volar by @johnsoncodehk we may have a good way to define multiple complex prop: based props using:

A supplement*
I was follow the discuss to commit, so everything will change, currently volar is only support defineOptions, defineProps will be support if it become a consensus.

@patak-dev
Copy link
Member Author

For reference to this discussion, the <script setup> RFC has been updated to use defineProps

@johnsoncodehk
Copy link
Member

johnsoncodehk commented Dec 3, 2020

Sorry forgot to reply... I already added IDE support for defineProps. Please use volar if want to try.

@ghost
Copy link

ghost commented May 9, 2021

为了便于参考,<script setup>RFC已更新为可以使用defineProps

 import { defineProps } from 'vue'

  prop: ({ num, msg } = defineProps({
    num: {
      type: Number,
      default: 4,
    },
    msg: {
      type: String,
      required: true,
    },
  }))

I wrote like above and reported an error defineProps is not defined

@ausgomez
Copy link

the way I am declaring props with sugar setup is like this:

<script setup>
const props = defineProps({
    num1: {
        type: Number,
        default: 6
    },
    num2 {
        type: Number,
        default: 8
    }
})
</script>

This seems to work fine, and you can call those values on the <template> like {{ num1 }}

Hope it helps

@HoraceKeung
Copy link

HoraceKeung commented Sep 23, 2021

I am having a thought about the fact that one advantage of composition API is that you can group codes together by feature, but currently with defineProps, all props will be grouped together in one place.

<script setup>
const props = defineProps({
	prop1: { type: String, default: 'default' },
	prop2: { type: Number, required: true },
	prop3: String,
	prop4: { type: Boolean, default: false }
})
</script>

Then from #369, we have $ref and $computed, etc. This makes me think, would it be nice to have $prop and to define a prop we can do something like:

<script setup>
const prop1 = $prop({ type: String, default: 'default' })
const prop2 = $prop({ type: Number, required: true })
const prop3 = $prop(String)
const prop4 = $prop({ type: Boolean, default: false })
</script>

this way we can put our props anywhere we want.

@patak-dev
Copy link
Member Author

I didn't update this issue so far because I was waiting for ref sugar to stabilize. I still think that this would be good for the reasons you state here (ability to group together logic for related features). There was a comment from Evan in another issue saying that he is already thinking about allowing sugary destructuring for defineProps. That would take care of the other main point in the original proposal (be able to move logic between template and script without adding props., same as the rationale for .value):

const { prop1, prop2, prop3, prop4 } = defineProps({
	prop1: { type: String, default: 'default' },
	prop2: { type: Number, required: true },
	prop3: String,
	prop4: { type: Boolean, default: false }
})
</script>

I think this would already be a big improvement. A problem I see is that with this, we end up repeating a lot the variable names, and more with TS and withDefaults, where we will need them three times:

interface Props {
  msg?: string
  labels?: string[]
}

const { msg, labels } = withDefaults(defineProps<Props>(), {
  msg: 'hello',
  labels: () => ['one', 'two']
})

I think that a $prop() macro would help here, the following code could be compiled to the one above avoiding the need for repeated variables:

<script setup>
const prop1 = $prop<string?>({ default: 'default' })
const prop2 = $prop<number>({ required: true })

const prop3 = $prop<string>()
const prop4 = $prop<boolean?>({ default: false })
</script>

@julie777
Copy link

julie777 commented Sep 24, 2021

vs const prop1 = $prop<string?>({ default: 'default' })

Why not just allow defineProps to be used multiple times with each use adding to the previous use. That wouldn't break the existing code or cause any interface changes. It would allow dividing separate functionality in script setup and make it easy to extract functionality to separate files.

I am in strong support of adding defaults and validators to defineProps. I think withDefaults seems like an afterthought. It would be nice if defineProps worked like composition api props and it actually seems to when I have volar convert <script> to <script setup>

<script setup> is awesome!

@vuejs vuejs locked and limited conversation to collaborators Sep 27, 2021

This issue was moved to a discussion.

You can continue the conversation there. Go to discussion →

Labels
None yet
Projects
None yet
Development

No branches or pull requests

7 participants