Load AngularJS Template within page, dynamically - dynamic

I have a page containing 2 controllers: one which manages a list of so-called 'apps', and another that is to place the new Angular template into the innerHTML of its Div element.
<div ng-controller="appList"></div>
<div ng-controller="appPane"> Dynamic content should be loaded here! </div>
I have tried using the standard {{expression}} bindings, but they do not work with html, I have also tried the ng-bind-html-unsafe directive (Binding the innerhtml to that of the App request's return) but controllers are not executed within this new code.
The problem seems to be that by using a Binding, Angular is not re-parsing the contents of the html in question to use it as an angular app. Any ideas of how to get it to parse dynamic content?

It appears that the $compile service, when fed the elements you wish to recompile along with your current scope, does what I was looking for.
Example from my source:
var appPane = $('#AppPane');//JQuery request for the app pane element.
appPane.html(data);//The dynamically loaded data
$compile(appPane.contents())($scope);//Tells Angular to recompile the contents of the app pane.
This should help anyone experiencing my problem, I hope.

Look at $routes and ngView in angularjs.
Here's a very basic example:
http://jsfiddle.net/pXpja/3/

Take a look at the uiRouter module (from the AngularUI project). It adds the concept of states, which are pretty similar to routes, but with other goodies, such as the ability to nest them. For example:
$stateProvider
.state('myState', {
url: '/mystate',
templateUrl: '...',
controller: 'MyCtrl'
})
.state('myState.substate', {
url: '/{substate}',
templateUrl: '...',
controller: 'MySubCtrl'
});
MySubCtrl will be activated whenever you go to /mystate/something and you can use that "something" as a parameter (see $stateParams). You can nest states to any amount of levels.

Related

Aurelia - dynamically create custom element in a view-model

I have an Aurelia app where a user can click on a button and create a new tab. The tab and its content (a custom element) do not exist on the page before the user clicks the button. I am generating the HTML for the content at runtime (via Javascript) in my view model.
I keep seeing mention of using the template engine's compose or enhance functions, but neither are working for me. I don't know how I would use the <compose> element (in my HTML) since I am creating the element based on the user clicking a button.
My thought was that the button has a click.delegate to a function that does ultimately does something like
const customElement = document.createElement('custom-element');
parentElement.appendChild(customElement);
const view = this.templatingEngine.enhance({
element : customElement,
container : this.container, // injected
resources : this.viewResources, // injected
bindingContext: {
attrOne: this.foo,
attrTwo: this.bar,
}
});
view.attached();
But all this does is create an HTML element <custom-element></custom-element> without actually binding any attributes to it.
How can I create a custom element analogous to <custom-element attr-one.bind="foo" attr-two.bind="bar"></custom-element> but via Javascript?
As you pointed out in your own answer, it's the missing resources that caused the issue. One solution is to register it globally. That is not always the desired behavior though, as sometimes you want to lazily load the resources and enhance some lazy piece of HTML. Enhance API accepts an option for the resources that you want to compile the view with. So you can do this:
.enhance({
resources: new ViewResources(myGlobalResources) // alter the view resources here
})
for the view resources, if you want to get it from a particular custom element, you can hook into the created lifecycle and get it, or you can inject the container and retrieve it via container.get(ViewResources)
I found the problem =\ I had to make my custom element a global resource.

Server Side Rendering Vue with ASP.NET Core 2

I'm trying to understand the usage and limitations of server side rendering with vuejs when using aspnet core.
I used this starter kit for aspnet core and vuejs to setup a simple vue site, which is running based on the code here: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/master
I then modified the project to update the aspnet-prerendering and added vue-server-renderer, compiling a hodgepodge of sources to cobble together this update: https://github.com/selaromdotnet/aspnet-vue-ssr-test/tree/ssr
If I run this project, the site appears to load fine, and if I turn off the javascript in the browser, I can see that it does appear that the server-side rendering executed and populated the html result:
however, because JavaScript is disabled, the content isn't moved into the dom as it looks like it is trying to...
My understanding of server-side rendering is that it would populate the html entirely and serve a completed page to the user, so that even if JS was disabled, they'd at least be able to see the page (specifically for SEO purposes). Am I incorrect?
Now I believe modern search engines will execute simple scripts like this to get the content, but I still don't want a blank page rendered if js is disabled...
Is this a limitation of server-side rendering, or perhaps specifically ssr with vue and/or aspnet core?
or am I just missing a step somewhere?
Edit: more information
I looked at the source code for what I believe is the method that prerenders the section here: https://github.com/aspnet/JavaScriptServices/blob/dev/src/Microsoft.AspNetCore.SpaServices/Prerendering/PrerenderTagHelper.cs
The line
output.Content.SetHtmlContent(result.Html);
has a null value for result.Html. However, when I manually edit this value to put a test value, it also doesn't render to the output html, and the app div tag is still empty...
If I'm doing something wrong to populate the result.Html value with the expected output, that's one thing, and I would appreciate some help in doing that, especially since the output html appears to be found, since it's in the script that immediately follows...
However, even if I were to populate it, it appears it's being skipped, as evidenced by me manually changing the value. is this a bug in the code or am I doing somethigng wrong, or perhaps both?
As you correctly noticed, for your project, result.Html inside the tag helper is null. So that line cannot be the location where the output is being generated. Since the HTML output from your prerendering script also does not include a script tag, it is clear that something has to generate that. The only other line that could possible do this is the following from the PrerenderTagHelper:
output.PostElement.SetHtmlContent($"<script>{globalsScript}</script>");
That would fit the observed output, so we should figure out where the globalsScript comes from.
If you look at the PrerenderTagHelper implementation, you can see that it will call Prerenderer.RenderToString which returns a RenderToStringResult. This result object is deserialized from JSON after calling your Node script.
So there are two properties of interest here: Html, and Globals. The former is responsible for containing the HTML output that finally gets rendered inside the tag helper. The latter is a JSON object containing additional global variables that should be set for the client side. These are what will be rendered inside that script tag.
If you look at the rendered HTML from your project, you can see that there are two globals: window.html and window.__INITIAL_STATE__. So these two are set somewhere in your code, although html shouldn’t be a global.
The culprit is the renderOnServer.js file:
vue_renderer.renderToString(context, (err, _html) => {
if (err) { reject(err.message) }
resolve({
globals: {
html: _html,
__INITIAL_STATE__: context.state
}
})
})
As you can see, this will resolve the result containing just a globals object with both html and __INITIAL_STATE__ properties. That’s what gets rendered inside of the script tag.
But what you want to do instead is have html not as part of globals but on the layer above, so that it gets deserialized into the RenderToStringResult.Html property:
resolve({
html: _html,
globals: {
__INITIAL_STATE__: context.state
}
})
If you do it like that, your project will properly perform server-side rendering, without requiring JavaScript for the initial view.

Can Vue-Router handle clicks from normal anchors rather than router-link?

I have a scenario where there are two major components on a page; a frame-like component that contains common functionality for many applications (including a bookmark/tab bar) and my actual application code.
Since the frame doesn't actually own the page that it's included on, it seems like it would be incorrect for it to define any routes, however the current page may define their own routes that may match one of those links. In that case, I'd like vue-router to handle those anchor clicks and navigate appropriately rather than doing a full page reload.
Here's a simplified template of what this looks like:
Frame (an external dependency for my app):
<Frame>
<TabStrip>
</TabStrip>
<slot></slot>
<Frame>
App1:
<Frame>
<App>You're looking at: {{ pageId }}!</App>
</Frame>
So when any of the app1 domain links are clicked from that tab strip, I want my route definitions in app1 to pick that up rather than it causing a page load. Since that component is owned by the frame, I don't have access to write <router-link> since links to many different apps may co-exist there.
Any thoughts?
Whoo, this is an old one! However, since this question was high in my search results when I was researching this problem, I figured I should answer it.
My use-case was similar to the one in the comments: I needed to capture normal <a> links within rendered v-html and parse them through the router (the app is rendering Markdown with a light modification that generates internal links in some cases).
Things to note about my solution:
I'm using Vue3, not Vue2; the biggest difference is that this is the new Vue3 composition-style single page component syntax, but it should be easy to backport to Vue2, if necessary, because the actual things it's doing are standard Vue.
I stripped out the markdown logic, because it doesn't have anything to do with this question.
Note the code comment! You will very likely need to design your own conditional logic for how to identify links that need to be routed vs. other links (e.g. if the application in the original question has same-origin links that aren't handled by the Vue app, then copy/pasting my solution as-is won't work).
<script setup>
import { useRouter } from "vue-router"
const router = useRouter()
const props = defineProps({
source: {
type: String,
required: true,
},
})
function handleRouteLink(event) {
const target = event.target
// IMPORTANT! This is where you need to make a decision that's appropriate
// for your application. In my case, all links using the same origin are
// guaranteed to be internal, so I simply use duck-typing for the
// properties I need and compare the origins. Logic is inverted because I
// prefer to exit early rather than nest all logic in a conditional (pure
// style choice; works fine either way, and a non-inverted conditional is
// arguably easier to read).
if (!target.pathname || !target.origin || target.origin != window.location.origin) {
return
}
// We've determined this is a link that should be routed, so cancel
// the event and push it onto the router!
event.preventDefault()
event.stopPropagation()
router.push(target.pathname)
}
</script>
<template>
<div v-html="source" #click="handleRouteLink"></div>
</template>

Aurelia Routing: Append Views into Tabbed Interface

I'm practically brand new to Aurelia, but over the course of a few days I've picked up the starter template and gone through some video training in Pluralsight. I have a unique vision that I can't seem to decide whether compose element, custom element, or router is best to use for this scenario - or if I need to write something completely custom.
I prefer to continue using the router because it gives you the
URLs and history state. Linking deep within the web app may be necessary.
When a view / viewmodel is initialized, I want the view appended to the DOM, NOT replaced. The <router-view> element works by replacing the view.
With each view appended to the DOM, I would like to create a set of tabs representing every view that has been opened so far. Think of any modern text editor, IDE, or even a web browser shows tabs.
Sometimes it would be necessary to detect whether a view is already rendered in the DOM (viewmodel + parameter) and just bring that view to the front -vs- appending the new one.
Do you have any suggestions, examples, etc for someone relatively new to Aurelia, SPAs, and MVVM?
Thank you.
I believe the easiest way is using the compose element. You would need an array containing all screens, and another array to hold the opened screens. Something like this:
screens = [
{ id: 1, name: 'Test 1', view: './test-1.html', viewModel: './test-1' },
{ id: 2, name: 'Test 2', view: './test-2.html', viewModel: './test-2' }
];
_activeScreens = [];
get activeScreens() {
return this.screens.filter((s) => this._activeScreens.indexOf(s.id) !== -1);
}
In the HTML you just have to use <compose></compose> for each iteration of activeScreens.
I made this example https://gist.run/?id=c32f322b1f56e6f0a83679512247af7b to show you the idea. In my case, I've used an html table. In your case, you could use a tab plugin, like Bootstrap or jQuery.
Hope this helps!

How to get a data on the client side with javascript from the render express method?

I use to work with MVC javascript framework such as Backbone or Angular, i would like to fully use express and there is something i don't know how to do.
When i used javascript client framework, i made AJAX GET calls to get my entities from express with res.send(), but only with the express rendering engine, how to get the entity you send with res.render('index', { User: { name: 'bob' }}) on the client side in your javascript code?
I tried on the client side to directly call:
<script>
console.log(JSON.stringify(User));
</script>
But i get a Uncaught ReferenceError: User is not defined
However, i can access the object in my html using ejs:
<h1><%= User.name %></h1>
I know how to get it in the html using the ejs view engine, but how to get it directly from the javascrit ?
Are you looking for something like client side rendering? Then RequireJS is the thing for you. res.send sends the data back to the client javascript framework and res.render renders a page or a template. Do let me know if you require something else.
Answering this much later, but I ran across this issue and had to work with it for quite a while before I got it, thought I would share for future visitors of this question.
Like V31 said earlier, res.render renders the template without passing the data first in JavaScript. The variables like <%= User.name %> only refer to their server-side definition as long as your EJS template is actually an EJS template -- which it stops being once it's been rendered as the page. As the Express docs say, render "returns the rendered HTML of a view." In other words, saying console.log(User.name) client-side is trying to access an object that only exists in the server.
You can get around this like this:
<script>
console.log('<%= JSON.stringify(User) %>');
</script>
With the quotes required around the <%= JSON.stringify(User) %> because this result will be a string rather than a variable -- you don't want your console.log treating the entire stringified user data as a variable, you just want to log it.
Taking this concept further, this is how one would make variables you're passing into your render accessible to client-side JavaScript files as well. For example, say you have a document.onLoad function that requires a variable from the server to decide whether or not to make a particular DOM manipulation.
<script>
const bob = '<%= User.name %>';
const alice = '<%= User.bestfriend %>';
</script>
<script src='/javascripts/myscript.js'></script>
myscript.js now has access to User.name and User.bestfriend as they were defined in the server's User object, and can use that data client-side in whatever manner you see fit.