MarionetteJS: who is responsible for showing sub-applications? - javascript

I am building a quite simple Marionette application; I am not using Marionette.Module since it's deprecated and since I want to use ES2015 with Webpack.
I have just a couple of "pages": one is the Welcome screen and the other one is the Playground. Each one of those pages are Applications, plus there is a root application with a Layout with just three regions: header, main and footer.
The view (or layout) of those applications are meant to fill the main region of the root layout.
Now, whenever I want to show one of those sub-applications, I don't know exactly how (or, I am not finding the most satisfying way) to ask the Layout of the root application to showChildView of the view/layout of those sub-apps.
So far, I came up with two approaches (none of which is fantastic):
EDIT: I added another approach at the end of the Q
on the controller of each sub-application, trigger the event "application:show" passing in the view. The root application is listening to this event and showChildView as soon as it receives the message
whenever I start a sub-application, I pass it the root application instance. Whenever the sub-application needs to show itself, it will call the showChildView inside the main region
The first approach is weak, because it's totally asynchronous (fire and forget). I'd like to know when my application is shown (attached to the DOM) but relying again on another event seems cumbersome
The second approach is more robust but it sucks, because of course I don't want the sub-application's views to be responsible of their own rendering in the root layout. The root application knows better.
Some code follows to try to show both ideas:
// Approach #1
// In the root app
welcomeApp.on('app:show', (view) => {
rootApp.rootView.showChildView('main', view);
});
// In the Welcome sub-app
var Controller = {
show() {
app.trigger('app:show', new WelcomeView());
}
};
// Approach #2
// In the root application
const welcomeApp = new WelcomeApp({
rootApp: this
});
// In the Welcome sub-app
var Controller = {
show() {
app.rootApp.rootLayout.showChildView('main', new WelcomeView());
}
};
EDIT: 12 Jan.
OK, digging a bit more in the documentation I found what I think is the correct way to do that. The idea is that the root Application will listen for Commands from the subapplications.
So, in my root view I will have:
this.channel.commands.setHandler("application:show", function(view) {
this.rootView.showChildView('main', view);
}.bind(this));
In all the other subapplications, I will have (for example, in the WelcomeView):
getController() {
const greet = function () {
this.channel.commands.execute('application:show', new WelcomeView());
}.bind(this);
return {
greet: greet
};
}

Personally I wouldn't use multiple applications, this seems to be just getting around the issue of modules being removed. Have you tried using a single application with a LayoutView, a Region for each component or (module) and the base of the component either being a singleton JS object, or you could try Backbone.Service to split it up. If you are using multiple routes, you could have a look at Backbone.Routing, with each Route being the base 'controller' for the 'page'.
I find a brilliant architecture for large scale Marionette applications is James Kyle's Marionette Wires. This uses Service for reusable components, and routers/routes for different data types.
EDIT
Another way of architecting using services, but if you don't want the root application's regions show methods to be called from child components, would be to import the instantiated application into the child component and use the app.addRegions method to add regions within the child. EG
//app.js
import { Application } from 'backbone.marionette';
const App = Application.extend({
onStart() {
//doSomething();
}...
});
export const app = new App();
//submodule/service.js
import { Service } from 'backbone.service';
import { SubmoduleController } from './controller';
const SubmoduleService = Service.extend({
requests() {
show: 'show',
hide: 'hide'
},
setup(options = {}) {
this.el = options.el
},
start() {
this.controller = new SubmoduleController({
el: this.el
});
},
show() {
this.controller.show();
},
hide() {
this.controller.destroy();
}
});
export const submoduleService = new SubmoduleService();
//submodule/controller.js
import { Object as Controller, Region } from 'backbone.marionette';
import { View } from './view';
import { app } from '../app';
export const SubmoduleController = Controller.extend({
initialize(options = {}) {
this.el = options.el;
this._addRegions();
},
_addRegions() {
const region = new Region({
el: this.el
});
app.addRegions({
submoduleRegion: region
});
},
show() {
const view = new View();
app.submoduleRegion.show(view);
},
onDestroy() {
app.submoduleRegion.reset();
}
});
//main.js
import { submoduleService } from './submodule/service';
const config = {
submodule: {
el: '#submodule';
},
...
};
submoduleService.setup(config.submodule);
submoduleService.show();
A gotcha with doing it this way is the submodule's region needs it's element to exist in the DOM when it's region's show method is called. This can either be achieved by having a root view on the application, whose template contains all component root elems, or to have a root submodule - page or something.

Related

vueJS mixin trigger multiple times in laravel 5.7

I am new in Vue jS [version 2]. There are 3 component in my page. I want to use a axios get data available in all pages. I have done as follows in my app.js
const router = new VueRouter({mode: 'history', routes });
Vue.mixin({
data: function () {
return {
pocketLanguages: [],
}
},
mounted() {
var app = this;
axios.get("/get-lang")
.then(function (response) {
app.pocketLanguages = response.data.pocketLanguages;
})
}
})
const app = new Vue({
router,
}).$mount('#app');
and using this pocketLanguages in a component like
{{ pocketLanguages.login_info }} this. Its working fine but My Issue is axios.get('') triggering 4 times when page load [in console]
Now how can I trigger this only once or anything alternative suggestion will be appreciated to do this if explain with example [As I am new in Vue]
You are using a global mixin, which means that every component in your app is going to make that axios get call when it's mounted. Since your page has several components in it, no wonder the call is being made several times. What you need to do here is either:
Create a normal mixin and only use it in the master/container/page component in every route that actually needs to fetch the data by providing the option mixins: [yourMixinsName]. That component can then share the data with the other components in the page.
If your data is common between pages then it's better to use a global store such as Vuex to simplify state management.
On a side note: It is usually better to handle your data initialization in the created hook. Handling it in the mounted hook can lead to some pitfalls that include repeated calls, among other things, due to parent/child lifecycle hooks execution order. Please refer to this article for more information on the subject.
Finally problem solved
In resources/js/components/LoginComponent.vue file
<script>
import translator from '../translation';
export default {
mixins:[translator],
beforeCreate: function() {
document.body.className = 'login-list-body';
},
.....
mounted() {
this.langTrans();
}
and my translation.js file at /resources/js
export default {
data: function() {
return {
pocketLanguages: []
};
},
methods: {
langTrans: function() {
var self = this;
axios.get('/get-lang')
.then(function (response) {
self.pocketLanguages = response.data.pocketLanguages;
});
}
}
};

React: Programmatically opening modals (and cleaning up automatically)

I have a site with lots of modals which can be opened from anywhere (for example LoginModal). The challenge I'm running into is if I open one programmatically with something like ReactDOM.render, how do I clean it up automatically when the parent component is unmounted without putting it (and all possible modals) in the template.
For example, something like this to open it:
openLoginModal() {
ReactDOM.render(<LoginModal />, document.body);
}
LoginModal can clean itself up when closed. However, if the DOM from the component which opened it is unmounted, how do I let LoginModal know to unmount as well.
One thought I've had is to use an Rx.Subject to notify it when to unmount, but this also sounds like a bit of a wrong approach and a possible anti-pattern.
For example:
// modules/User.js
openLoginModal(unmountSubj) {
const container = document.createElement('div');
ReactDOM.render(<LoginModal />, container);
unmountSubj.subscribe(() => {
ReactDOM.unmountComponentAtNode(container);
});
}
// components/RandomView.jsx
unmountSubject = new Rx.Subject();
componentWillUnmount() {
this.unmountSubject.next();
}
login() {
User.openLoginModal(this.unmountSubject);
}
I'd like to avoid having all the possible modal components in each JSX template they might be used in.
How would you approach this?
Here's the solution I've come up with so far: There's a modal manager module, which will render a modal into the DOM (via ReactDOM.render) and return a function which will unmount it.
Here's a simplified version:
// modalManager.js
export default modalManager = {
openModal(modalClass, props) {
// Create container for the modal to be rendered into
const renderContainer = document.createElement('div');
document.body.appendChild(renderContainer);
// Create & render component
const modalInst = React.createElement(modalClass, { ...props, renderContainer });
ReactDOM.render(modalInst, renderContainer);
// Return unmounting function
return () => this.unmountModal(renderContainer);
},
unmountModal(renderContainer) {
ReactDOM.unmountComponentAtNode(renderContainer);
renderContainer.parentNode.removeChild(renderContainer);
},
}
// TestThing.jsx
class TestThing extends React.Component {
unmountLogin = null;
componentWillUnmount() {
this.unmountLogin();
}
login() {
this.unmountLogin = modalManager.openModal(Login, {});
}
}
You'll also notice that renderContainer is passed to the modal component. This way the modal can call modalManager.unmountModal itself when closed.
Let me know what you think.
In my current React project, I've addressed this by having a multilayered component architecture.
<App>
// the standard starting point for React apps
<DataLayer>
// this is where I make API calls for data that is shared between all components
<DisplayLayer>
// this is where I put the methods to launch display elements that are shared
// by all components (e.g., modals, alerts, notifications, etc.)
<Template>
// this is the first layer that is actually outputting HTML content
<ModuleX>
<ModuleY>
<ModuleZ>
// these modules control the main display area of the screen, they encompass
// major UI functions (e.g., UsersModule, TeamsModule, etc.)
// when one of these modules needs to launch a shared UI element (like a
// modal), they call a method in the <DisplayLayer> template - this means
// that if I have a commonly-used modal (like, LoginModal), it doesn't need
// to be included in every one of the core modules where someone might need
// to initiate a login; these modules are mounted-and-unmounted as the user
// navigates through the app
So when the app loads, <App>, <DataLayer>, <DisplayLayer>, and <Template> all load up (and they will only load one time). As the user navigates around, the <ModuleX/Y/Z/etc> components are mounted-and-unmounted, but all of the "common stuff" stays in place that was mounted/loaded in the higher layers of the app.

Dynamically mount an engine in the router

How can I update the router dynamically based on the state in a service during the initializing of the router? Let's say I have 4 engines out of which one must be mounted in an app based on the state in a service and whatever the state may be, the engine mounted must have a specific name irrespective of the fact whatever engine may be mounted.
This is not supported at this time, and would likely fall under this issue in the ember-engines repo: https://github.com/ember-engines/ember-engines/issues/99
A temporary workaround supposedly exists as of v0.5 of ember-engines (requires ember 2.12 and ember-cli 2.12). You have to create a helper, like load-engine, via ember g helper load-engine.
Its contents would be:
export default Ember.Helper.extend({
assetLoader: Ember.inject.service(),
compute([name]) {
if (this.engineName === name && this._resolved) { return name; }
this.engineName = name;
let assetLoader = this.get('assetLoader');
assetLoader.loadBundle(name)
.then(() => {
this._resolved = true;
this.recompute();
});
return null;
}
});
Then you'd use it like so:
{{mount (load-engine boundEngineName)}}
Note: Originally posted by https://github.com/mike183 in the Ember slack channel

Rendering a ReactJS component in memory before placing it in the DOM

I have the concept of a tile in my application.
Tiles are dynamically loaded. They can then be initialized against a DOM element using an init method, with the tile taking care of rendering itself.
features/my-tile/tile.js
import contentsComponentFactory from './components/contents/factory'
const tile = {
init,
};
// `el` is a DOM element
function init(el) {
// Render a "contents" ReactJS component (see below) to the DOM - how?
// Is the following possible?
el.appendChild(contentsComponentFactory.create({ props }).render());
}
export default tile;
A tile has a component contents which renders the tile contents to the screen.
features/my-tile/components/contents/factory.js
const factory = {
create
};
function create(options) {
const component = Object.create(React.Component.prototype);
component.props = options.props;
component.state = { message: 'This is a tile' };
component.render = function() {
return <div>{this.state.message}</div>;
};
return component;
}
export default factory;
In AngularJS, in init I would render the contents in memory and insert the result into the DOM. Can I do this in ReactJS?
I am new to ReactJS and so I may be completely misunderstanding something.
You should be able to utilize React.createElement to create the element in memory, and then ReactDOM.render() in order to insert it into the DOM.
const element = React.createElement('div', null, 'Hello, World!');
ReactDOM.render(element, document.getElementById('content'));
http://codepen.io/mikechabot/pen/PGXwxa?editors=1010
However, createElement doesn't return a native DOM element, but rather an instance of ReactElement. Not sure if this suits your needs.
This seems a pretty complicated way to do things in AngularJS, maybe you should rethink your design?
It's even worse in React, are you intending on bypassing ReactDOM and inserting it manually?
I'd recommend at least going through the React tutorial before you attempt this.

Aurelia inject mock dependency

I have this aurelia component for displaying a feed to the user which depends on a custom API service class called Api for fetching the feed. The Api class has a get() function which in turn uses HttpClient to fetch the data.
Trying to test the component I want to mock the service class, specifically the get function, to return suitable test data and have this mock injected into the component via aurelia's DI container. The DI part I am having trouble with.
Here is the relevant part of component's js file
import {bindable, inject} from 'aurelia-framework';
import {Api} from 'services/api';
#inject(Api)
export class Feed {
events = null;
constructor(api) {
console.info('feed.js constructor, api:', api)
this.api = api;
}
And the relevant code from my test
beforeEach(done => {
...
let mockApi = new Api();
spyOn(mockApi, 'get').and.returnValue(mockGetResponse);
const customConfig = (aurelia) => {
let conf = aurelia.use.standardConfiguration().instance("Api", mockApi);
console.info('Registering Api:', conf.container.get("Api"));
return conf;
}
const ct = new ComponentTester();
ct.configure = customConfig;
sut = ct.withResources('activityfeed/feed');
sut.inView('<feed username.bind="username"></feed>')
.boundTo({username: TEST_USER});
sut.create(bootstrap).then(() => {
done();
});
});
This code is actually working the way I intended as far as I can tell. On creation of the component my customConfig function is called and the mockApi instance is logged to the console.
However later in the bootstrapping process the component constructor still receives an instance of the actual Api service class instead of my mock instance which was registered to the container.
Spent the last couple of hours trying to dig up any documentation or examples for doing things like this without success so if anyone can assist I would greatly appreciate it.
Or if there is / are alternative ways to accomplish this that would work just as well.
When testing a standard component that consists of both a view and a view model, using the aurelia-testing package, I find that a cleaner approach might be to let Aurelia create both the view and view-model, and use mocked classes for all view model dependencies.
export class MockApi {
response = undefined;
get() { return Promise.resolve(this.response) }
}
describe("the feed component", () => {
let component;
let api = new MockApi();
beforeEach(() => {
api.response = null;
component = StageComponent
.withResources("feed/feed")
.inView("<feed></feed>");
component.bootstrap(aurelia => {
aurelia.use
.standardConfiguration();
aurelia.container.registerInstance(Api, api);
});
});
it("should work", done => {
api.response = "My response";
component.create(bootstrap).then(() => {
const element = document.querySelector("#selector");
expect(element.innerHTML).toBe("My response, or something");
done();
});
});
});
This approach lets you verify the rendered HTML using the normal view model class, mocking the dependencies to control the test data.
Just to answer my own question, at least partially, if it can be useful to someone.
By using the actual Api class constructor as the key instead of the string "Api" the mock seems to be correctly injected.
I.e.
import {Api} from 'services/api';
...
let conf = aurelia.use.standardConfiguration().instance(Api, mockApi);

Categories

Resources