Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Initial inline input value #3924

Closed
loranger opened this issue Oct 12, 2016 · 14 comments
Closed

Initial inline input value #3924

loranger opened this issue Oct 12, 2016 · 14 comments

Comments

@loranger
Copy link

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
Copy link
Member

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
Copy link
Author

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
Copy link

dsignr commented Feb 19, 2017

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

We got a regular form A with two fields:

  • Post Name
  • Permalink

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
Copy link

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

@georgecoca
Copy link

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
Copy link

ztolley commented Jul 12, 2017

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
Copy link

NoriSte commented Mar 2, 2018

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
Copy link

ghost commented Mar 19, 2018

@pxwee5
Copy link

pxwee5 commented Aug 6, 2018

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
Copy link

pire commented Aug 9, 2018

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
Copy link

lucastruong commented Aug 22, 2019

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
Copy link

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
Copy link

seballot commented Sep 2, 2020

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
Copy link

CecileV commented Dec 4, 2020

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests