vuejs / vue

This is the repo for Vue 2. For Vue 3, go to https://github.com/vuejs/core
http://v2.vuejs.org
MIT License
207.99k stars 33.69k forks source link

Initial inline input value #3924

Closed loranger closed 8 years ago

loranger commented 8 years ago

Vue.js version

2.0.2

Reproduction Link

https://jsfiddle.net/uojhhjr7/ https://jsfiddle.net/k8bfrxwz/

While switching to Vue2, I realize that it's not possible to define a model initial value from the template anymore.

Let's say I have a server-side validated form (with a preview powered by Vue) which brings back the user if the validation detected an error.

I'd like to populate the form with the previously typed values (and get the corresponding preview), but I cannot simply set an inline value anymore because Vue warns me with inline value attributes will be ignored when using v-model

I took a look at the migration guide and found a confirmation it was not possible anymore, but I can't find any workaround.

Of course, the simple solution would be to modify the initial value of the model within the Vue instantiation, but it's a children component from a vue file, webpacked with other scripts.

I got this solution working, but it seems a little bit overkill to me, regarding the usual elegance of Vue.js.

So here is the aim of this issue : Is there a way to pass an initial value to a v-model from template to Vue as we used to ?

yyx990803 commented 8 years ago

In Vue 2.0, the template is like a function: it gets called every time something changes. With this in mind, having an inline value is basically saying the input's value is static and should never change - which doesn't make sense when you are using v-model with it.

This does makes your use case a bit more complicated, but here's how I'd do it - render your form state in your server HTML output and initialize your component with that data: https://jsfiddle.net/vzns7us7/

loranger commented 8 years ago

I'm surprised it's now so hard to instanciate from a simple html element, but I can understand the reasons. Anyway, thank you for your support, and thank you so much for Vue.js !

dsignr commented 7 years ago

I don't think this is an elegant approach - Consider a situation as below:

We got a regular form A with two fields:

When the user for the first time visits this form, I want to show some defaults. So, I set them up in my data as follows:

{
   postName: "Hello world",
  permalink: "sample-permalink"
}

(You could argue this could be set as a placeholder attribute, but this isn't always the case) Now, after the user updates, these values are persisted in the db and each time the user visits the same form, these values will be in the value attribute. HOWEVER, vue will still render from the JSON above. This is a very common scenario in popular MVC frameworks such as rails.

The only other way (being new to Vue) would be to manually set the attributes by accessing the elements:

vm.postName: $("#blah").val() || "Hello world",

But, that defeats the purpose if I manually need to preset the values, isn't it?. Kindly correct me if I'm wrong.

cruzlutor commented 7 years ago

Thanks @dsignr, is not most elegant solution, but that help me a lot

georgecoca commented 7 years ago

Another simple solution would be to use ref and data attributes to reference the element and then access it once the template has been rendered.

Template <input type="text" ref="name" data-value="John">

Javascript

data() {
    return {
        name: ''
    }
},
mounted() {
    this.name = this.$refs.name.dataset.value; // See note below
}

Note: check for dataset compatibility, otherwise use jQuery or any library to extract data-.

Hope that helps πŸ˜‰

ztolley commented 7 years ago

I got around it by doing

document.querySelectorAll('.textfield').forEach((el) => { new Vue({ el, data: function () { return { value, } } })

That said, there's other things in the markup, like error messages, I also want to bind to the model and I'd have to do similar for all of them. At that point the markup becomes a little strange.

My aim, maybe like the original poster, is to try find a framework that combines the template syntax and 2 way binding of Vue with the progressive enhancement and SSR ideals that allow sites to be built that can fall back to no JS.

Actually makes me a little sad that Vue js claims to allow progressive enhancement and then states it wont pickup initial model values from markup thus contradicting that statement.

NoriSte commented 6 years ago

I solved this way (you need to know the input field ID in advance)

data() {
  return {
    defaultValue: null, // the SSR rendered value
    initialValue: null, // the value just before mounting
    focusedBeforeMount: false,
  };
},
beforeMount() {
  const el = document.getElementById('[component_id]');
  this.defaultValue = el.defaultValue;
  this.initialValue = el.value;
  this.focusedBeforeMount = document.activeElement === el;
},
mounted() {
  if (this.initialValue) {
    // this.initialValue contains the state to be saved
  }
  if (this.focusedBeforeMount) {
    this.$refs.input.focus(); // you must set a reference to the input field
  }
},

This code addresses one more problem: if you SSR the page, the input field could be edited manually from the user before the component is been mounted (think about a slow mobile connection thwat could delay the JS execution). Thus the input state could differ from the value set by the server. Thank you all πŸ˜‰

ghost commented 6 years ago

https://vuejs.org/v2/api/#ref

pxwee5 commented 6 years ago

This is my approach to this issue and I think it's pretty neat depending on your usage.

Parent Component

<app-form :defaults="{ name: 'Debbie' }"></app-form>

AppForm Component

created () {
  this.formData = Object.assign({}, this.formData, this.default)
}

As for

having an inline value is basically saying the input's value is static and should never change - which doesn't make sense when you are using v-model with it.

It makes sense to me, because that's how native html works. The value attribute set in an input element <input value="test"> is meant to be changed.

Question is which should take priority when both the inline value and the v-model value are set.

pire commented 6 years ago

I'm aware the issue is closed, but wanted to share my approach in case it helps anyone, or in case I've done a big no no 😨

It should dynamically add any input fields to the data.

const element = '#test';

new Vue({
  el: element,
  data() {
    // grabs the component
    const $el = document.querySelector(element);

    // grabs all fields with v-model within the component
    // in an iterable array
    const $fields = [...$el.querySelectorAll('input[v-model]')];

    const ssrValues = {};

    // add the v-model attribute as the key for the ssrValue
    // and the value of the field as the key's value
    $fields.forEach(field => {
      const name = field.getAttribute('v-model');
      const value = field.value;

      ssrValues[name] = value;
    });

    // define any extra data you would like
    const data = {
      tacos: ['un taco', 'dos tacos', 'tres!']
    };

    // fuse the ssr data with our extra data and return
    return Object.assign(data, ssrValues);
  }
});
lucastruong commented 5 years ago

I'm aware the issue is closed, but wanted to share my approach in case it helps anyone, or in case I've done a big no no 😨

I've updated a fraction of your code.

data() {
  const $el = document.querySelector(evenElement);
  const $fields = [...$el.querySelectorAll('*[v-model]')];
  const ssrValues = {};

  for (let i = 0; i < $fields.length; i++) {
      let field = $fields[i];
      let name = field.getAttribute('v-model');
      let value = field.value;
      let type = field.getAttribute('type');

      if (!value) {
          continue;
      }

      if (['radio', 'checkbox'].includes(type) && !field.hasAttribute('checked')) {
          continue;
      }

      ssrValues[name] = value;
  }

  const data = {};

  return Object.assign(data, ssrValues);
}
yellow1912 commented 5 years ago

Coming from Symfony, this is giving me a huge headache. I want to keep using Symfony way to render form which is extremely convenient for me while making it work with Vue. Things would be much easier if Vue allows some way to lazily initialize the model (which was possible in Angular 1.x)

The problems are:

  1. Before the whole form template is rendered, we don't know the exact structure of the form data (well it's possible but it adds some processing overhead unnecessarily).
  2. The values of the inputs are rendered on the inputs themselves and we need to initialize.

I understand that I can pre-process the form, dump a big ass json and pass that to the form component first, but that doesn't seem like an elegant way to do things :(.

seballot commented 4 years ago

Hi ! Here is another workaround, with a custom directive

// Usage <input v-model="my_input" v-init="'Hi!'" />
// The property my_input will be initialized with the value of v-init, here Hi!
Vue.directive('init', {
  bind (el, binding, vnode) {
    let vModel = vnode.data.directives.find(d => d.rawName == "v-model")
    if (vModel) {
      vnode.context[vModel.expression] = binding.value
    }
  }
})
Vue.new({
  data: {
     input: undefined,
     other: "See"
  }
}

<input v-model="my_input" v-init="'Hi!'" />

image

<input v-model="my_input" v-init="'other + ' you...'" />

image

I'm a beginner with Vue, I know this is not very elegant, but please tell me if I just wrote some crazy things !

You can also automatically fill the v-init attribute before your vue component get initialized :

$('input[v-model], select[v-model]').each(function() {
    if (!$(this).val()) return
    let value = $(this).attr('type') == "checkbox" ? $(this).prop('checked') : `'${$(this).val()}'`
    $(this).attr(`v-init:${$(this).attr('v-model')}`, value)
  })

<input v-model="my_input" value="Hi!" />

CecileV commented 3 years ago

Hi,

I've try this and it's works :

data: {
  inputs : {
    lastname : '',
    firstname : ''
  }
},
beforeMount: function() {
  for (const [input_name, input_value] of Object.entries(this.inputs)) {
    if (this.$el.querySelector('[name="'+input_name+'"]')) {
      this.inputs[input_name] = this.$el.querySelector('[name="'+input_name+'"]').dataset.default;
    }
  }
}

HTML :

<input type="text" name="lastname" v-model="inputs.lastname" data-default="SMITH" >
<input type="text" name="firstname" v-model="inputs.firstname" data-default="John" >

It's not the most elegant solution but I find this one quite simple.