MithrilJS: Routing a component inside top level component - javascript

I just started reading about Mithril. Fascinating..
Just one thing that puzzles me after first read.
How can I route one component inside another (top-level component)? I mean, how do I emulate angulars ng-view or embers outlet?
I understand that I can get m.route to attach components to any dom node.
But how can I render say top level component App, which generates m("#view") among other things, and then all other routable components go inside App's #view div? Is this possible? Otherwise I have to repeatedly include header and footer with every route transition to a subcomponent, right? Am I missing something?
Thank you.

Otherwise I have to repeatedly include header and footer with every route transition to a subcomponent, right? Am I missing something?
I don't think you're missing anything. Mithril has as little magic as possible, so it's hard to miss things. Yet it's still somehow more convenient than frameworks with magic.
I simply wrap my views in a template function. I'm a lazy guy, but even I don't mind doing this because it's flexible and not confusing.
http://codepen.io/farzher/pen/vOjjEB
function viewTemplate(content) {
return function() {return [
m('#header', [
'my site',
m('a', {config:m.route, href:'/'}, 'home'),
m('a', {config:m.route, href:'/1'}, 'page 1'),
m('a', {config:m.route, href:'/2'}, 'page 2'),
]),
m('hr'),
m("#view", content),
m('#footer', 'copyright my site'),
]}
}
component1 = {
view: viewTemplate([
m('h1', 'component 1 page')
])
}
component2 = {
view: viewTemplate([
m('h1', 'component 2 page')
])
}
m.route(document.body, '/', {
'/': {view: viewTemplate()},
'/1': component1,
'/2': component2,
})

I ended up going with on of the Leo's suggestions I found googling around.
I can only have "one-layer" wrap and no named outlets with this solution but it works and does the job for now.
At the end of the day, Angular has only one ng-view and people get by somehow.
So this is the outer component.
var Layout = {
controller(subcomp) {
this.own = {
slide: false
};
this.subctrl = new subcomp.controller();
this.subview = subcomp.view;
},
view(ctrl) {
return bubble(ctrl.own, ctrl.subview(ctrl.subctrl));
},
wrap(routes) {
var map = {};
Object.keys(routes).map((r) => {
map[r] = {
controller() {
return new Layout.controller(routes[r]);
},
view: Layout.view
};
});
return map;
}
};
This is the outer view where you insert your component.
function bubble(vm, subview) {
return m("main", [
m("#outlet",[ subview ])
]);
}
And then you route all your subcomponents inside the layout.
m.route.mode = "pathname";
m.route(document.body, "/articles/create", Layout.wrap({
"/articles/create": CreateArticle
}));
Hope this helps someone in the same situation.

I tried several solutions:
With m.component for route handler -
http://jsfiddle.net/0xwq00zm/1/
With internal method of the App component, that wraps the inner component. This is somewhat better, cause I'm able to pass aplication state to other components - http://jsfiddle.net/0xwq00zm/11/
With simple external function, that wraps the inner component with
other elements - http://jsfiddle.net/0xwq00zm/12/
More or less complex - with all of them I have the feeling that the apps redraws itself, and not only the inner component.
Simply select all the elements - Ctrl+A in the result pane in JsFiddle - and then navigate. It's virtual DOM and it shouldn't re-render everything, but with all solutions above - it happens.
(I tried also with context.retain = true; on some parts, but still after a few navigations I get to a point where nothing gets selected.)
========
Hope these variants help someone ... but also - I'll be happy to see solution of the total re-rendering.

Related

Can I force angular to give error if a component doesn't exist?

If I have a template in a component that references non-existant components, angular 1.6 seems perfectly happy to render it as nothing at all. For example I have a route currently that looks like:
when('/something',{
title: 'Something',
template: "<mycomponent></mycomponent>",
})
If I forget to register mycomponent on my application, this route renders nothing. Is there some mode I can use in angular that will cause a harder error in a case like that? Or at least print something to the console when it occurs?
To be perfectly clear, I have this issue with both top level components referenced by the router, as well as other child components that those reference recursively.
No, there is no option for that. By the way "non rendered" components are a benefit IMO, because you could override this slot later.
A short example:
when('/something',{
title: 'Something',
template: "<slot><mycomponent></mycomponent></slot>",
})
assume you want to override the ui-part of mycomponent, just define a component for "slot"
There was a routeProvider.otherwise before. Not sure if it’s still supported. I’m on a phone so limited. Let me know how it goes.
UI Router supports components for routes, this means that it is capable of triggering an error if a component doesn't exist.
AngularJS currently doesn't offer component router. This means that route template is compiled as any other template with $compile, and unknown selectors are just ignored.
It is possible to patch ngRoute ngView directive to additionally check if a route that is expected to route to a component actually has it compiled.
// the whole list can be retrieved from commonly available sources
var htmlTags = ['p', 'div', ...];
app.directive('ngView', function () {
return function($scope, $element, $attrs) {
if (!DEBUG_MODE)
return;
if ($element.children().length !== 1)
return;
var routeComponent = angular.element($element.children()[0]);
var routeComponentTagName = routeComponent.prop('tagName').toLowerCase();
if (htmlTags.indexOf(routeComponentTagName) >= 0)
return;
var routeComponentName = $attrs.$normalize(routeComponentTagName);
if (!routeComponent.controller(routeComponentName)) {
console.warn(routeComponentTagName + ' element is non-standard yet not a component');
}
}
});
AngularJS already has $normalize() which is primarily intended for attributes and strips some specific prefixes, but as long as the components names don't have x or data prefix, it can work as generic kebab to camel case transformer.
There may be other ways to detect if a component was compiled on child element. The code above will trigger false negative if there already is parent routeComponentName component, because controller() travels up the hierarchy.
And the proper way to handle this problem is to provide test coverage for the application. Router-related cases should likely be handled in integration or e2e tests.

Observing a property on a service from a route in EmberJS

I think I'm not understanding a concept here. As far as I know any Ember.object can observe properties on another Ember.object.
So, I have a service, a router, and a component. I need the component and the router to be able to observe a property on the service. It's entirely possible that I'm just structuring the solution in the wrong way, I'll include an overview of what I'm trying to do at the end.
Here is approximately what I have:
/services/thing-manager.js
export default Ember.Service.extend({
observedProperty: 'original value'
});
/components/thing-shower.js
export default Ember.Component.extend({
thingManager: Ember.inject.service(),
myObserver: Ember.observer(
'thingManager.observedProperty',
function() {
// This shows up as anticipated, unlike the one in the routes
console.log('THING SHOWER COMPONENT observed change on thingManager')
}
),
actions: {
changeObservedProperty: function() {
let thingManager = this.get('thingManager')
let newText = thingManager.get('observedProperty') + '!'
// here i am sure to call `set` to make sure observers fire
thingManager.set('observedProperty', newText)
}
}
});
/routes/things.js
export default Ember.Route.extend({
thingManager: Ember.inject.service(),
underObservation: Ember.observer('thingManager.observedProperty', function() {
// This is what I expect to fire, but does not.
console.log('THINGS ROUTE observed change on thingManager')
}),
});
As you can see, I'm expecting console output from both observers in the component and router. Why doesn't this work?
Twiddle here!
My Overall Goals
This is perhaps a separate question, but I'd like to know if there is a better way to accomplish what I'm trying to do. I've been learning about 'data down, actions up', which led me to this approach. I'm building a website that load a json file with a bunch of GPS coordinates and sticks them on a map.
The goal is to click a map marker, and have that load the corresponding data. This should also change the route. So, my thinking was, to keep track of my markers in a service, and when the selected marker changes, the router would observe that and transition to the next route. The component would also notice the changed property and update the map.
Thanks folks!
In things.js route file you haven't used accessed/used thing-manager service, so observer will not be triggered.
routes/thing.js
init(){
this._super(...arguments);
this.get('thingManager');
},
introducing this will make your observer to be fired.
I would say, if you are following the DDAU priniciple, then your component should not mutate the thing-manager service properties. it should send action to service and mutate it.
Note: You can have observers and computed properties inside any Ember.Object which means you have it thing-manager service too.

Vue components with partial reusability

I'm working on a large app with legacy code. I've run into this issue twice now and am realizing there must be a better solution than what I've done to solve it. The issue is as follows.
There are 3 separate pages where I need to add very similar Vue functionality. However, these pages have significantly different HTML/Blade templates. Therefore, it's like I have to pass in separate HTML/Blade templates to the component in addition to component props.
I can kind of accomplish this using Vue inline-templates, which takes care of the significantly different HTML/Blade template problem.
However, the remaining issue is that I have 3 .js Vue components, one for each page. This would be fine, except the Vue code in each file is very similar.
It's also possible that at some point I will need to add more unique Vue code to each component, and would like to keep that possibility open.
What I would like to do is find a way to reuse the Vue code that is very similar in each component.
I have tried thinking of a way to nest the same child component within each of these 3 separate components, but I don't see how that would be possible due to the differences in the HTML/Blade in each file.
Any suggestions would be most appreciated, as I feel like I'm duplicating too much Vue code!
Thanks to user thanksd for providing the solution in the comments above. Mixins indeed were the way to go for me. That way, instead of this:
Vue.component('first-component', {
template: // something unique
methods : {
functionNumber1: function () {
// do something
},
});
Vue.component('second-component', {
template: // something totally different
methods : {
functionNumber1: function () {
// do same something
},
}
});
I can essentially do this:
const myMixin = {
methods : {
functionNumber1: function () {
// do same something
},
}
Vue.component('first-component', {
template: // something unique
mixins: ['myMixin']
});
Vue.component('second-component', {
template: // something totally different
mixins: ['myMixin']
});

Aurelia doesn't 'refresh' vm when navigating

Jolly good evening! In my Aurelia-App I'm using a viewModel to deal with various views via an navigationStrategy (reading out route-parameters and setting the view accordingly).
Navigation works baiscally well, there is one problem however:
When I keep navigating between routes that are based on the same viewModel, the viewModel doesn't 'refresh'. Only when navigating to a different route with a different viewModel first, and then back to the intended route, the contents are shown as expected.
It seems like the lifecycle-hooks of the component are not kicking in. Is there any way to trigger unbind() and detached() manually? Or is there a better way to do things generally?
Also the Route-Configuration seems a bit weird. When I'm taking away moduleId the app crashes, and when I'm taking away layoutViewModel the Data is not bound to the view. My Workaround for now is to assign an empty viewModel + an empty template. Am I using this wrong?
Big thanks!
configureRouter(config, Router) {
var getModelStrat = (instruction) => {
instruction.config.layoutView = "pages/templates/"+instruction.params.model+".html"
}
config.addAuthorizeStep(AuthorizeStep);
config.title = 'Aurelia';
config.map([
{
route: 'detail/:model/:id?',
name: 'detail',
moduleId: 'pages/empty',
layoutViewModel: 'pages/detail',
auth: true,
navigationStrategy: getModelStrat
},
{...}
]);
}
This is by design. Router will try to reuse existing view models.
If you need to override this per view model, then create determineActivationStrategy() method on it and return activationStrategy.replace:
import { activationStrategy } from 'aurelia-router';
export class SomeViewModel {
// ...
determineActivationStrategy() {
return activationStrategy.replace;
}
// ...
}
If you need to override this for each view model / route then take a look at Marton Sagi's answer for a similar question. Basically, all of your routes need to define activationStrategy: 'replace'.

Rendering part of component outside of the component itself

I have JSFiddle like this:
https://jsfiddle.net/ownvjjow/
Basically the problem is that I'd like to render some part of component x outside of the element(x) itself. Like in the outer scope, but with preserving the scope of it in other component(y). I have part of the component that may be wanted to be rendered with targetElement set, but it seems like ng-repeat have some problem with it. The fiddle returns some other error, the one I get in my app is:
I could propably break the part that I want to be "moving" as another component then conditionally render it in other place providing the bindings: {... , controller: '<'} then expose the scope from component x to the x.y but I was wondering if there is an option to compile this part of component x and inject it in other place with everything working correctly.
I am not sure if my approach is right or maybe I should think about something different, if you know any other solution/idea then I would be grateful if you let me know about it.
fiddle
if(this.options.targetElement) {
var parentElem = angular.element((this.options.targetElement));
var childElem = $compile(this.container)($scope)[0];
$timeout( function(){
parentElem.append(childElem);
}, 0, false );
}
This will give AngularJS time to finish its compile.
Hope this help.

Categories

Resources