Enhance parts of multi page application with react or vue - javascript

I'm developing a enterprise application with java & hibernate & spring mvc in the server side and using jquery in the client side (not a SPA).
Now in the search page i use ajax and get only json response, but i don't want to write something like this below in every search or pagination request.
function(ajaxData) {
....
$('#search').html('' +
'<div class="search-container">' +
'<div class="search-item">' +
'<div class="title-item">'+ajaxData.title+'</div>' +
...
...
'</div>' +
'</div>'
)
....
}
I think it's easy to use jsx with a react or vue component just in this page to refresh the results.
I want also reuse some html blocks and i think it will be easy with react or vue
I used to build a little SPA project and it's all about npm and webpack and bundling, but i really don't want to use them since i have a multi page application and it's very suitable for my project.
I think the same thing facebook is doing, they use react but facebook is not a SPA.
How can i achieve this hybrid approach ?

I had done a similar kind of stuff in the past. I injected a small react component into the DOM.
Here is how I did it:
Create a React component in JSX, let's call it Demo:
export class Demo extends React.Component {
render() {
return <h1>This is a dummy component.</h1>
}
}
Now use the renderToStaticMarkup function to get the static html.
const staticMarkup = renderToStaticMarkup(<Demo PASS_YOUR_PROPS/>);
You have the HTML, now you can insert this markup at the desired location using the innerHTML.
Apologies if I misunderstood your question.
UPDATE
We could also use the render() for this purpose. Example:
document.getElementById("button").onclick = () => {
render(
<Demo PASS_YOUR_PROPS/>,
document.getElementById("root")
);
};
Working solution with render() and renderToStaticMarkup: https://codesandbox.io/s/w061xx0n38
render()
Render a ReactElement into the DOM in the supplied container and
return a reference to the component.
If the ReactElement was previously rendered into the container, this
will perform an update on it and only mutate the DOM as necessary to
reflect the latest React component.
renderToStaticMarkup()
This doesn't create extra DOM attributes such as data-react-id, that
React uses internally. This is useful if you want to use React as a
simple static page generator, as stripping away the extra attributes
can save lots of bytes.

Since you have an existing multi page application without a build step (that is, without webpack/babel), I believe one very simple way of achieving what you want is using Vue.js.
You can define a template and update only the data.
Here's a demo of how you would do the code you showed in the question:
new Vue({
el: '#app',
data: {
ajaxDataAvailable: false,
ajaxData: {
title: '',
results: []
}
},
methods: {
fetchUsers() {
this.ajaxDataAvailable = false; // hide user list
$.getJSON("https://jsonplaceholder.typicode.com/users", (data) => {
this.ajaxData.title = 'These are our Users at ' + new Date().toISOString();
this.ajaxData.results = data;
this.ajaxDataAvailable = true; // allow users to be displayed
});
}
}
})
/* CSS just for demo, does not affect functionality could be existing CSS */
.search-container { border: 2px solid black; padding: 5px; }
.title-item { background: gray; font-weight: bold; font-size: x-large; }
.result-item { border: 1px solid gray; padding: 3px; }
<script src="https://unpkg.com/vue"></script>
<script src="https://code.jquery.com/jquery-3.3.1.min.js"></script>
<div id="app">
<button #click="fetchUsers">Click to fetch the Users</button><br><br>
<div class="search-container" v-if="ajaxDataAvailable">
<div class="search-item">
<div class="title-item"> {{ ajaxData.title }}</div>
<div class="result-item" v-for="result in ajaxData.results">
Name: {{ result.name }} - Phone: {{ result.phone }} - Edit name: <input v-model="result.name">
</div>
</div>
</div>
</div>
In this example, we have:
A method fetchUsers that will perform the ajax call;
The fetchUsers method is bound to the click event of the <button> via #click="methodName" (which is a shorthand to v-on:click="methodName").
A v-if (v-if="ajaxDataAvailable") that makes the .search-container div hidden until the ajaxDataAvailable property is true.
The rendering of some data in the template using interpolation: {{ ajaxData.title }}, note that this picks the value from the objects declared in the data: part of the Vue instance (the new Vue({... code) below and is automatically updated whenever ajaxData.title changes (what happens inside the fetchUsers method when the Ajax call completes.
The rendering of a list using v-for: v-for="result in ajaxData.results". This iterates in the ajaxData.results array, that is too updated inside the fetchUsers method.
The use of an <input> element with the v-model directive, which allows us to edit the result.name value directly (which also updates the template automatically).
There's much more to Vue, this is just an example. If needed, more elaborated demos can be made.
As far as integrating into an existing application, you could paste this very code into any HTML page and it would be already working, no need for webpack/babel whatsoever.

So, there are two things you could do to re-use your code:
I would recommend React for sharing code as components. This page from Official docs explains how to use react with jquery.
Additional resources for integrating react and jquery jquery-ui with react, Using react and jquery together, react-training
Or, use some template engine so you do not need to go through the trouble of integrating a new library and making it work alongside jquery. This SO answer provides lot of options for doing this.
Unless your planning on migrating your jquery app to react in the long run, I would not recommend using react just for one page. It would be easier to go with the template engine route.

From the looks of the requirement, you just need:
Dynamic, data driven HTML blocks
Which need to be reusable
In this case, since we don't need state, having an entire framework like react/vue/angular may be overkill.
My recommendation would be to go for a templating engine like jQuery Templates Plugin or Handlebars
You can even store the HTML blocks as separate reusable modules which you can invoke as needed across your pages.
Sample using Handlebars:
var tmpl = Handlebars.compile(document.getElementById("comment-template").innerHTML);
function simulatedAjax(cb){
setTimeout(function(){
cb(null, {title:'First Comment', body: 'This is the first comment. Be sure to add yours too'});
},2000);
}
simulatedAjax(function(err, data){
if(err){
// Handle error
return;
}
document.getElementById('target').innerHTML = tmpl(data);
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<div id="target">
Loading...
</div>
<script id="comment-template" type="text/x-handlebars-template">
<div class="comment">
<h1>{{title}}</h1>
<div class="body">
{{body}}
</div>
</div>
</script>
NOTE:
One disadvantage of using this approach (as opposed to a framework like react/vue/angular) is that you will need to handle event binding and unbinding manually. You can mitigate this to some extent by wrapping your templates in a function which mounts and unmounts it automatically.
// Single reuable JS file for each component
(function(namespace) {
var _tmpl = Handlebars.compile('<div class="comment"><h1>{{title}}</h1><p>{{body}}</p></div>');
function titleClickHandler(ev) {
alert(ev.target.innerHTML)
}
function CommentWidget(target, data) {
var self = this;
self.data = data;
self.target = document.querySelector(target);
}
CommentWidget.prototype.render = function() {
var self = this;
self.target.innerHTML = _tmpl(self.data);
// Register Event Listeners here
self.target.querySelector('h1').addEventListener('click', titleClickHandler)
}
CommentWidget.prototype.unmount = function() {
var self = this;
// Unregister Event Listeners here
self.target.querySelector('h1').removeEventListener('click', titleClickHandler);
self.target.innerHTML = '';
}
window._widgets.CommentWidget = CommentWidget;
})(window._widgets = window._widgets || {})
// Usage on your page
var w = new _widgets.CommentWidget('#target', {
title: 'Comment title',
body: 'Comment body is remarkably unimaginative'
});
// Render the widget and automatically bind listeners
w.render();
// Unmount and perform clean up at a later time if needed
//w.unmount();
<script src="https://cdnjs.cloudflare.com/ajax/libs/handlebars.js/4.0.11/handlebars.min.js"></script>
<div id="target"></div>

Related

Are dynamic arguments in Vue.js templates possible?

Here is my setup, and I know its not ideal (I inherited this project from a couple of developers that are no longer working on this web app). I'm trying to marry Vue.js and JQuery so that I can simply re-use JQuery based components (instead of rewriting them) on my Vue.js page (the two previous developers having design differences).
What I need is this: I am filling a table with components post page Mount. Everything about this table populates correctly except I have buttons in some of the cells in which I would like to use Vue.js to operate those buttons' functionality. I can add a v-on:click= attribute just fine but since these attributes are added to the buttons post Mount their Vue attributes are not being listened to. On the client side (Edge/Chrome in this case) I can see the vue attributes in the developers console when inspecting the element. I looked up examples such as this article:
How to make Vue js directive working in an appended html element regarding how to correctly re-render components after they are appended post Mount. Since these objects were created and appended to the web page after mounting they are not Reactive.
The article suggests creating a component that can then be mounted manually, using a template to create the component. I've tried this and found that using Vue.extend to create a new subclass/component now disallows me to access methods in my original app. Let me place some code and then I'll recap:
What I see on the client side console:
<button type="button" class="results_table_col_1_button" v-on:click="test()">200-715-122-306-ATP</button>
How the data table is being updated via JQUERY
display_search_results: function (data) {
data = JSON.parse(data);
let num = data.row_order.length;
if (num > 0) {
displayDataTable(data);
$("#tableContainer").show();
$("#test_count").text(num + " test" + (num === 1 ? '' : 's') + " found");
$('#dataDisplayTable tr').not(':first').each(function (i, el) {
let save_id = $(el).find('td.results_table_col_1').text()
$(el).find('button.results_table_col_1_button').attr("v-on:click", 'test()')
What I tried via the article
//How the new component is being created:
var testComp = new ATP_Button().$mount()
document.getElementsByClassName("results_table_col_1")[i].appendChild(testComp.$el)
//with the constructor:
var ATP_Button = Vue.extend({
template: '<button type="button" v-on:click="test()">Is this working</button>',
})
This creates the button but test is then undefined as it is not initialized or referenced to in the subclass object ATP_Button. How do I pass my 'main' Vue app's test() function to this component so that when the button $emits its event the parent's test() function is called?
I also tried adding props to the constructor so that I could maybe pass test() into the component upon creation like this:
var ATP_Button = Vue.extend({
props: ['test'],
template: '<button type="button" v-on:click="test_child()">Is this working</button>',
methods: {
test_child: function () {
this.test
alert(this.test)
}
}
})
And created the component as such:
var testComp = new ATP_Button(this.test_func).$mount()
Now however this.test inside the component is undefined as it would seem that passing the function through is not working properly (probably due to developer error aka me).
RECAP
A JQUERY table object is being populated via a backend process with HTML elements to be displayed on the front end view of the webpage
One element that is added is a button that needs to be able to call a Vue.js method from the main app
The elements are added to the table after the page has been mounted thus they are not reactive
Trying to manually mount the component creates a subclass object that does not have access to the main app's methods
Trying to pass the main app's method test() to the component during construction failed and left the test variable undefined
Eventually I need the buttons to call a method from the main app with dynamic variables inside as arguments
I appreciate anyone who is able to offer any assistance.

How would one convert a razor loop into a Vue.js component?

I'm attempting to take a very dumb vue component and move more of the code (written in razor) into the component itself (to keep it as modular as possible).
Currently the component is more of a container where razor loops through a list of communities and renders out some html server-side. That html is then passed through a slot into the component and styled.
Ideally, I would like to find a way to render only the bare minimum through razor, and instead move as much of the logic into the component as possible. For example, I know vue offers a mechanism for looping through data (v-for), however in this case, I'm not quite sure how to pass the data into the component in such a way as to utilize that.
Being new to vue (as I think most of us are), I'm still getting the hang of passing server-side data through to components. I've tried creating props, but the complexity of the data being generated has thrown me off a bit. I'm hoping someone here can shine a little light on the best way to approach this problem!
Thanks in advance!
index.cshtml:
<community-list>
#foreach (var region in regions)
{
var regPage = region.Pages.FirstOrDefault();
if (regPage == null)
{
continue;
}
string isActive = "";
int communityCount = 0;
var cities = region.Children;
for (int i = 0; i < cities.Count; i++) {
communityCount += cities[i].Pages.Count;
}
if (HttpContext.Current.Request.IsAuthenticated && !Model.IsPreview) {
var email = HttpContext.Current.User.Identity.Name;
isActive = CompanyName.Core.Models.Users.IsRegionSubscribed(email, region.Name) ? "active" : "";
}
<div class="community container primary #isActive" data-name="#region.Name">
<figure style="background-image: url('/assets/images/#regPage.GetAttributeValue("HeroImage")');"></figure>
<h3>#regPage.GetAttributeValue("Title")</h3>
<p>#(communityCount.ToString()) New Home Communities</p>
<custom-button destination="#regPage.URL" appearance="primary" type="link" size="extra-large" text="Find New Homes in #stateName"></custom-button>
</div>
}
</community-list>
CommunityList.vue
<template>
<section class="community-list">
<slot></slot>
</section>
</template>
<script>
export default {
name: 'CommunityList',
};
</script>
<style lang="scss" scoped>
-- styling here --
</style>
Update
------
To clarify further:
Assuming it's the proper approach, I would like to pull the html in the razor loop out of the slot and place it into the component. Remove the slot altogether, and take the data from razor and populate the html in the component with the values that are returned from razor.
It seems to me the more html and logic I can move into the component the better.
Perhaps something like:
index.cshtml:
#foreach (var region in regions) {
var foo = region.foo;
var bar = region.bar;
}
<community-list foo="#foo" bar="#bar"></community-list>
CommunityList.vue
<template>
<section v-for="region in regions">
<p>{{ foo }}</p>
<p>{{ bar }}</p>
</section>
</template>
I'm not quite sure how best to approach this from a vue-centric manner. If I converted the razor to simply spit out a list of vars to use like above, how would I then take that data, pass it into the component, and loop through it within the vue component itself (something like the example above)? In the case above, I'm sure this won't work - but I imagine there must be some way to accomplish what I'm thinking of somehow?
(Or am I just approaching this the wrong way altogether?)
You would have to render the data into a tag with razor, into a global variable. When you set up your vue component, use that global variable to set up the data function.

vue JS not propagating changes from parent to component

I am pretty new to Vue Framework. I am trying to propagate the changes from parent to child whenever the attributes are added or removed or, at a later stage, updated outside the component. In the below snippet I am trying to write a component which shows a greeting message based on the name attribute of the node which is passed as property from the parent node.
Everything works fine as expected if the node contains the attribute "name" (in below snippet commented) when initialized. But if the name attribute is added a later stage of execution (here for demonstration purpose i have added a set timeout and applied). The component throws error and the changes are not reflected . I am not sure how I can propagate changes for dynamic attributes in the component which are generated based on other events outside the component.
Basically I wanted to update the component which displays different type of widgets based on server response in dynamic way based on the property passed to it .Whenever the property gets updated I would like the component update itself. Why the two way binding is not working properly in Vuejs?
Vue.component('greeting', {
template: '#treeContainer',
props: {'message':Object},
watch:{
'message': {
handler: function(val) {
console.log('###### changed');
},
deep: true
}
}
});
var data = {
note: 'My Tree',
// name:"Hello World",
children: [
{ name: 'hello' },
{ name: 'wat' }
]
}
function delayedUpdate() {
data.name='Changed World';
console.log(JSON.stringify(data));
}
var vm = new Vue({
el: '#app',
data:{
msg:data
},
method:{ }
});
setTimeout(function(){ delayedUpdate() ;}, 1000)
<script src="https://vuejs.org/js/vue.js"></script>
<div id="app">
<greeting :message="msg"></greeting>
</div>
<script type="text/x-template" id="treeContainer">
<h1>{{message.name}}</h1>
</script>
Edit 1: #Craig's answer helps me to propagate changes based on the attribute name and by calling set on each of the attribute. But what if the data was complex and the greeting was based on many attributes of the node. Here in the example I have gone through a simple use case, but in real world the widget is based on many attributes dynamically sent from the server and each widget attributes differs based on the type of widget. like "Welcome, {{message.name}} . Temperature at {{ message.location }} is {{ message.temp}} . " and so on. Since the attributes of the node differs , is there any way we can update complete tree without traversing through the entire tree in our javascript code and call set on each attribute .Is there anything in VUE framework which can take care of this ?
Vue cannot detect property addition or deletion unless you use the set method (see: https://v2.vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats), so you need to do:
Vue.set(data, 'name', 'changed world')
Here's the JSFiddle: https://jsfiddle.net/f7ae2364/
EDIT
In your case, I think you are going to have to abandon watching the prop and instead go for an event bus if you want to avoid traversing your data. So, first you set up a global bus for your component to listen on:
var bus = new Vue({});
Then when you receive new data you $emit the event onto the bus with the updated data:
bus.$emit('data-updated', data);
And listen for that event inside your component (which can be placed inside the created hook), update the message and force vue to re-render the component (I'm using ES6 here):
created(){
bus.$on('data-updated', (message) => {
this.message = message;
this.$forceUpdate();
})
}
Here's the JSFiddle: https://jsfiddle.net/9trhcjp4/

How do you refer to a VueJS component's name within the template's raw HTML / x-referenced ID element?

I'm essentially trying to add a CSS class to my VueJS components based on the component-name it's registered under (to give all those specific types of components the same style).
For example:
Vue.component('dragdropfile', {
// This refers to a unique template element (see HTML below)
template: "#dragdropfile-tmp",
props: ['action']
});
And in the Vue component template:
<template id="dragdropfile-tmp">
<form action="{{action}}" method="post" enctype="multipart/form-data">
<div class="fallback">
<input name="file" type="file" multiple />
</div>
<div class="dz-message" data-dz-message>
<div class="dz-default">
<!--
According to VueJS docs / forums, "slot" gets replaced by any innerHTML
passed from the incoming component usage.
-->
<slot></slot>
</div>
</div>
</form>
</template>
And finally, how it's used in the "index.html" page is like this:
<dragdropfile id="someDragDropFiles" action="/upload-please">
Do you have files to upload?<br/>
<b>Drop it here!</b>
</dragdropfile>
Now, although I could put in the component-name manually for each component HTML templates, I want to automate this.
Are there any special built-in {{binding}} names that Vue uses internally so that I can inject the component-name into the resulting component on the page?
To result something like so:
<form class="dragdropfile" id="someDragDropFiles" action="/upload-please" ... > ...</form>
Or do I simply need to pass it myself as a new component property? As in:
Manually call it like props: ["componentName", ...] and;
Refer to it in the template as <form class='{{componentName}}' ...>
Is this the only feasible way?
Using VueJS version: 1.0.17
(https://cdnjs.cloudflare.com/ajax/libs/vue/1.0.17/vue.js)
Solution #1
After a few minutes of inspecting what the object holds using the create: function() { console.log(this); } in the Vue.component(...) registration call, I found the name in it's this.$options.name property.
In other words:
<form class="{{this.$options.name}}" ...> ... </form>
Or even shorter:
<form class="{{$options.name}}" ...> ... </form>
Come to think of it, it's still a bit of manual work to enter on each component templates, but there's probably a way to auto-append the class via the created method.
Solution #2
This is the automated approach I was looking for!
Here it goes, basically I made a wrapper function to call whenever I need to register new components, which internally calls the usual Vue.component(...) method.
NOTE: This example depends on jQuery to add the class and underscore.js for object merging via _.assign, but could probably be replaced by a direct *.classList.addClass() call instead. These are just the helper methods I'm familiar with, use what you like! :)
makeVueComponent(name, params)
/*
* Creates a Vue Component with a standardized template ID name (ie: #tagname-tmp)
* combined with given params.
*/
function makeVueComponent(name, params) {
//Ensure params is assigned something:
params = params || {};
//Backup a reference of an existing *.created() method, or just an empty function
var onCreated = params.created || function(){};
/*
Delete the original `created` method so the `_.assign()` call near the end
doesn't accidentally merge and overwrite our defaultParams.created() method.
*/
delete params.created;
var defaultParams = {
//Standardized template components to expect a '-tmp' suffix
// (this gets auto-generated by my NodeJS/Express routing)
template: "#"+name+"-tmp",
// This part auto-adds a class name matching the registered component-name
created: function() {
var $el = $(this.$options.el);
$el.addClass(this.$options.name);
//Then forward this to any potential custom 'created' methods we had in 'params':
onCreated.apply(this, arguments);
}
};
//Merge default params with given custom params:
params = _.assign(defaultParams, params);
Vue.component(name, params);
}
And then just use it like so:
//Register your Vue Components:
makeVueComponent("dragdropfile", {props:['action']});
You can then leave out those {{$options.name}} from your actual component templates that I mentioned in Solution 1.

<require> inside a globalResource component called with enhance in Aurelia

So, I'm trying to setup Aurelia in my Angular 1 web application so I can slowly upgrade. I need to do that since the application is too big and migrating everything at once would be impossible.
So, in my Aurelia folder I created a component folder with two components (aurelia-component.js and another-component.js with their views aurelia-component.html and another-component.html), I won't put the javascript as they are just two classes with one property, the html for both is the same, the only thing that changes is the text property value so I can differentiate them:
<template>
<div>${text}</div>
</template>
My entry point main.js looks like this:
export function configure(aurelia) {
aurelia.use
.basicConfiguration()
.developmentLogging()
.globalResources('components/aurelia-component')
.globalResources('components/another-component');
//window.aurelia = aurelia;
aurelia.start()
.then(a => {
window.aurelia = a;
});
}
As you can see, this puts Aurelia in the window object so I can access it from my Angular app, I'll improve this later.
In my angular app I have this directive:
'use strict';
function AureliaContainer() {
function Link($scope, element, attrs) {
window.aurelia.enhance(element[0]);
}
//
return {
restrict: 'A',
link: Link
};
}
module.exports = AureliaContainer;
I set this up in my app root with:
app.directive('aureliaContainer', require('./directives/aurelia.container'));
And in my Angular View I have these divs with my directive that calls the enhance function from Aurelia:
<div aurelia-container>
<aurelia-component></aurelia-component>
</div>
<div aurelia-container>
<another-component></another-component>
</div>
The reason I have two aurelia-container in the html is that I know I'll have to have more than one when I'm migrating the application.
And this works fine, both components load normally in the screen.
The problem is when I try to call another component from within one of those components.
What I did was, I created a new component called test-component.js with its view test-component.html. The html for this is just:
<template>
<h1>Header</h1>
</template>
And then, from the aurelia-component.html I called it using:
<template>
<require from="./test-component"></require>
<div>${text}</div>
<test-component></test-component>
</template>
Now, when I load the page, the test-component actually loads but the <div>${text}</div part of aurelia-component doesn't and I get this error in the console:
Uncaught (in promise) TypeError: Cannot read property 'behaviorInstructions' of undefined
I really don't understand why this error is happening, I should be able to load a custom element from within another one normally, shouldn't I. Or is there a limitation when you use enhance?
I also tried to use setRoot in both divs with no success, just one of them is loaded.
Maybe there's a better approach for this?
Again, I can't migrate my entire application at once, it's just no feasible.
Thanks in advance for the help.
First off, I know nothing about progressive enhancement in Aurelia. And cannot comment about its suitability for your scenario.
But I am wondering if maybe you missed some Au dependencies (like binding or templating?)
http://aurelia.io/hub.html#/doc/article/aurelia/framework/latest/app-configuration-and-startup/8
aurelia.use
.defaultBindingLanguage()
.defaultResources()
.developmentLogging()
.globalResources('resources/my-component');
That might explain why it fails when you want it to render a template?

Categories

Resources