Binding Vue.js to all instances of an element, without(?) using Components - laravel

Today I'm learning Vue.js, and I have a few ideas of where it might be really useful in a new project that's an off-shoot of an existing, live project.
I like the idea of trying to replace some of my existing functionality with Vue, and I see that Components may be quite handy as quite a lot of functionality is re-used (e.g. Postcode lookups).
Once of the pieces of functionality I've used for an age is for invalid form elements - currently in jQuery when a form input or textarea is blurred I add a class of form__blurred, and that is coupled with some Sass such as:
.form__blurred {
&:not(:focus):invalid {
border-color:$danger;
}
}
This is to avoid styling all required inputs as errors immediately on page load.
I'm totally fine with doing this in jQuery, but I figured maybe it could be done in Vue.
I have an idea of how I might do it with components thanks to the laracasts series, but my form inputs are all rendered by Blade based on data received from Laravel and it doesn't seem like a neat solution to have some of the inputs rendered in Javascript, for a number of reasons (no JS, confusion about where to find input templates, etc).
I figured something like the following simplified example would be handy
<input type="text" class="form__text" v-on:blur="blurred" v-bind:class="{ form__blurred : isBlurred }" />
<script>
var form = new Vue({
el : '.form__input',
data : {
isBlurred : false
},
methods : {
blurred : function() {
this.isBlurred = true;
}
}
});
</script>
That actually works great but, as expected, it seems like using a class selector only selects the first instance, and even if it didn't, I'm guessing changing the properties would apply to all elements, not each one individually.
So the question is - is this possible with pre-rendered HTML, in a way that's smarter than just looping through a selector?
If it is, is there a way to create the Vue on a .form element and apply this function to both .form__input and .form__textarea?
Or, as is probably the case, is this just not a good use-case for Vue (since this is actually a lot more code than the jQuery solution).

Sounds like a great use case for a Custom Directive.
Vue allows you to register your own custom directives. Note that in Vue 2.0, the primary form of code reuse and abstraction is components - however there may be cases where you just need some low-level DOM access on plain elements, and this is where custom directives would still be useful.
<div id="app">
<input type="text" name="myforminput" v-my-directive>
</div>
<script>
Vue.directive('my-directive', {
bind: function (el) {
el.onblur = function () {
el.classList.add('form__blurred');
}
}
});
var app = new Vue({
el: '#app'
});
</script>
You can also add the directive locally to a parent component, if it makes sense for your application.

Related

Vue2 + laravel6 - Component implementation

I just started using Vue2 with Laravel6 and got stuck when trying to understand how to use component method.
As I am totally new for Vue, I am using the official tutorial of Vue as a reference. What I learned from here(https://v2.vuejs.org/v2/guide/components.html) about Vue component instantiation is we give options to a component.(e.g. we give 'template:' options for HTML part.)
When I look at laravel6 codes of resouces/js/app.js, it looks something like this:
Vue.component('example-component', require('./components/ExampleComponent.vue').default);
I looked at js/components/ExampleComponent.vue expecting to see some options declared there. However, there's no option in the ExampleComponent.vue file. Instead, I see <template></template> tag. Apparently, the <template></template> tag seems to work as 'template:' option.
I have two questions regarding above:
Does <template></template> tag have the same meaning as 'template:' option?
If question 1 is yes, are other options also replacable with corresponding tags? (e.g. Can I use <props></props> tag for 'props:' option? or <data></data> tag for 'data:' option?
Thanks in advance!
In Vue world, there are two popular types of defining a component
First Type
in this type, you add all of your HTML inside the template property
and the props add as attribute inside the component object to
Vue.component('button-counter', {
data: function () {
return {
count: 0
}
},
template: '<button v-on:click="count++">You clicked me {{ count }} times.</button>'
})
Second Type
in this type you add your component logic in a separate file ends with .vue
for example in laravel there is an ExampleComponent.vue file you will find on
it just template tag just as a wrapper for your component content and your logic you can write it as it mentions below.
<template>
// some content here
</template>
<script>
export default {
props: [],
methods: {
},
data(){
return {
}
}
}
</script>
Finally
there is no tag called props or data
for more info read this article

Array no reactive when modifying an element, only when adding or deleting

I'm doing crud of publications with Vue components and Laravel.
I have one parent component called publications.vue which has 2 childs called create.vue and list.vue, and list.vue I have another to childs called remove.vue and update.vue.
The thing is when I add or remove a publication in the array it works perfect, but when I modify an element it doesn't react. The controller works perfect, but unless I refresh I don't get anything on the screen.
This is my code:
<template>
<div class="main">
<create
:user="user"
v-if="showCreate"
#create="addPublicationOnClient"
/>
<list
v-for="(publication, index) in publications"
:key="publication.id" :publication="publication" :user="user"
#deleted="deletePublicationOnClient(index)"
#updated="updatePublicationOnClient(index, ...arguments)"
/>
</div>
</template>
addPublication(publication) {
this.publications.unshift(publication); // works perfect
},
deletePublication(index) {
this.publications.splice(index, 1); // works perfect
},
updatePublication(index, editedPublication) {
console.log(editedPublication); // shows the correct value of the edited publication
Vue.set(this.publications, index, editedPublication); // do not react. do not show anything
this.publications.splice(index, 1, editedPublication) // do not react neither. do not show anything
console.log(this.publications); // shows the correct values in the array
}
I will really appreciate any help because I really stuck and I have read a lot of posts, but can't find a solution.
Vue has some really tricky behavior when it comes to arrays of objects.
Vue is watching your array, and when the array's .length is modified or if one of its values is modified vue can "see" that change.
When you update fields of an object in the array you will not get the reactivity, because to Vue the array has not changed. This is because the array's values are references to the object, and when you update the object, those references don't change.
Your approach above seems fine to me, but again there can be really weird issues.
I will highlight two tools to combat these reactivity issues. The first is better for your situation I believe.
Explicitly modify the length of the array.
updatePublication(index, editedPublication) {
this.deletePublication(index);
this.addPublication(index, editedPublication);
}
Force re-rendering using :key. When a key changes in a template, it will force re-rendering of all child elements.
<template>
<div class="main" :key="'updated-'+updated">
...
</template>
data() {
return {
updated: 0,
};
},
...
updatePublication(index, editedPublication) {
this.publications.splice(index, 1, editedPublication);
this.updated++;
}

call to ReactJS component in Laravel blade templates

I use Laravel 5.4 and React 15.5.4, code is writing in ES6.
I'd like replace Vue and use React and I did it. But I often will use small components for example 2 in different places of blade template. I don't want use one app component.
I'd like use something like:
<span class="react">
<TestComponent property={true} />
</span>
I can't do it automatically. Now I use
<span data-component="TestComponent" data-props="{property:true}" />
and in app.js
_.each(document.querySelectorAll('[data-react]'), element => {
let props ={};
Array.prototype.slice.call(element.attributes)
.forEach(item => {
props[item.name] = item.value;
if(item.name !== 'data-react'){
element.removeAttribute(item.name);
}
});
ReactDOM.render(React.createElement(reactComponents[element.getAttribute('data-react')],props),element);
});
It works but I need to use add all properties to one react component property and then use for example this.props.out.propery
I also would like set normal component tag in my blade component
I've try to use in app.js
_.each(document.querySelectorAll('.react'), item => {
ReactDOM.render(item.children,item);
});
Someone have any idea to solve this problem?
EDIT
I changed my solution to:
<span data-react="LoginForm" input="{{json(request()->old())}}" error="{{session('error')}}" errors="{{json($errors->getMessages())}}" />
or
<LoginForm data-react="LoginForm" input="{{json(request()->old())}}" error="{{session('error')}}" errors="{{json($errors->getMessages())}}" />
in blade and in resources/assets/js/app.js
var reactComponents = {
LoginForm: require('./components/login').default,
};
_.each(document.querySelectorAll('[data-react]'), element => {
let props ={};
Array.prototype.slice.call(element.attributes)
.forEach(item => {
props[item.name] = item.value;
});
ReactDOM.render(React.createElement(reactComponents[element.getAttribute('data-react')],props),element);
});
It works fine. This is not super clear solution but I have impression that the reasonable.
I can set components name in html code and add props almost same like in JSX.
As far as I know, you can not mix JSX components directly with Blade templates. The only server side rendering available today for React is NodeJS.
What you could do to improve your architecture is add specific HTML tags with certain ids and render the react components in them. So inside Blade you could do something like:
<div id="componentA"></div>
This will act as a place holder in your Blade template for that react component. Then you render your componentA from your app.js like this:
React.render(<ComponentA prop1='valueX'/>, document.getElementById("componentA"))
Remember that in this case the world of react and world of Blade run at different times.
You could use document.getElementsByTagName('LoginForm') getting all the instances and later iterate its attributes. It's clear code but not generic, because it will work just for LoginForm components.
If you want to render any tag name, then maybe it's better to use some attribute as you used with data-react.
getElementsByTagName isn't super supported by old browsers so maybe could a good idea to use jQuery as fallback $('LoginForm')

Form select box in Backbone Marionette

I'm trying using Backbone.Marionette to build an application. The application gets its data through REST calls.
In this application I created a model which contains the following fields:
id
name
language
type
I also created an ItemView that contains a complete form for the model. The template I'm using is this:
<form>
<input id="model-id" class="uneditable-input" name="id" type="text" value="{{id}}"/>
<input id="model-name" class="uneditable-input" name="name" type="text" value="{{name}}" />
<select id="model-language" name="language"></select>
<select id="model-type" name="type"></select>
<button class="btn btn-submit">Save</button>
</form>
(I'm using Twig.js for rendering the templates)
I am able to succesfully fetch a model's data and display the view.
What I want to do now is populate the select boxes for model-language and model-type with options. Language and type fields are to be restricted to values as a result from REST calls as well, i.e. I have a list of languages and a list of types provided to me through REST.
I'm contemplating on having two collections, one for language and one for type, create a view for each (i.e. viewLanguageSelectOptions and viewTypeSelectOptions), which renders the options in the form of the template I specified above. What I am not sure of is if this is possible, or where to do the populating of options and how to set the selected option based on data from the model. It's not clear to me, even by looking at examples and docs available, which Marionette view type this may best be realized with. Maybe I'm looking in the wrong direction.
In other words, I'm stuck right now and I'm wondering of any of you fellow Backbone Marionette users have suggestions or solutions. Hope you can help!
Create a view for a Select in my opinion is not needed in the scenario that you are describing, as Im assuming that your languages list will not be changing often, and the only porpouse is to provide a list from where to pick a value so you can populate your selects in the onRender or initializace function of your view using jquery.
you can make the calls to your REST service and get the lists before rendering your view and pass this list to the view as options and populate your selects on the onRender function
var MyItemView = Backbone.Marionette.ItemView.extend({
initialize : function (options) {
this.languages = options.languages;
this.typeList = options.typeList;
},
template : "#atemplate",
onRender : function () {
this.renderSelect(this.languages, "#languagesSelect", "valueofThelist");
this.renderSelect(this.typeList, "#typesSelect", "valueofThelist")
},
renderSelect :function (list, element, value) {
$.each(list, function(){
_this.$el.find(element).append("<option value='"+this[value]+"'>"+this[value]+"</option>");
});
}
})
var languagesList = getLanguages();
var typeList = getTypesList();
var myItemView = new MyItemView({languages:languagesList,typeList :typeList });
Hope this helps.

Separating template logic from Backbone.View

I just started learning Backbone.js, and have been working on (what else) a simple to-do application. In this app, I want to display my to-do items inside of <ul id="unfinished-taks"></ul> with each task as a <li> element. So far, so simple.
According to the tutorials I have read, I should create a View with the following:
// todo.js
window.TodoView = Backbone.View.extend({
tagName: 'li',
className: 'task',
// etc...
});
This works fine, but it seems like bad practice to define the HTML markup structure of my to-do item inside of my Javascript code. I'd much rather define the markup entirely in a template:
// todo.js
window.TodoView = Backbone.View.extend({
template: _.template($("#template-task").html()),
// etc...
});
<!-- todo.html -->
<script type="text/template" id="template-task">
<li class="task <%= done ? 'done' : 'notdone' %>"><%= text %></li>
</script>
However, if I do it that way Backbone.js defaults to using tagName: 'div' and wraps all my to-do items in useless <div> tags. Is there a way to have the HTMl markup entirely contained within my template without adding unsemantic <div> tags around every view element?
If you are only planning to render the view once, you can set the el property of the view manually in .initialize():
// todo.js
window.TodoView = Backbone.View.extend({
template: _.template($("#template-task").html()),
initialize: function() {
this.el = $(this.template(this.model.toJSON())).get(0);
},
// etc
});
There are some caveats here, though:
Backbone expects the el property to be a single element. I'm not sure what will happen if your template has multiple elements at the root, but it probably won't be what you expect.
Re-rendering is difficult here, because re-rendering the template gives you a whole new DOM element, and you can't use $(this.el).html() to update the existing element. So you have to somehow stick the new element into the spot of the old element, which isn't easy, and probably involves logic you don't want in .render().
These aren't necessarily show-stoppers if your .render() function doesn't need to use the template again (e.g. maybe you change the class and the text manually, with jQuery), or if you don't need to re-render. But it's going to be a pain if you're expecting to use Backbone's standard "re-render the template" approach for updating the view when the model changes.

Resources