I have a pretty simple thing I want to accomplish but I cannot figure out how or if it is even possible. I am using the Hot Towel template to start with. In the shell viewmodel I have a user observable. I would like to be able to reference that user observable from other pages on my site. For example from the home page. I tried a couple of things but it doenst appear as though I can access the shell from the composed view. I have a working solution at the moment that uses event pub/sub calls from the shell to pass the user data to anyone listening whenever the data changes (home view in this example). This works it just seems a little clunky and not really the ideal way to handle this. This user observable will need to be used all throughout the site to determine when certain features should be available and to show a particular users projects.
Is there a way to data bind to a knockout observable contained in the shell viewmodel from the home view?
You might consider having a global.js that returns a singleton, which you include in view models as needed.
define(function () {
return {
sharedObservable: ko.observable(),
sharedObservableArray: ko.observableArray(),
...
};
});
Using global in a viewmodel.
define([..., global], function (..., global) {
...
global.sharedObservable('updated');
// As an alternative use a local var for easier access
// var localVar = global.sharedObservable;
// localVar('updated')
...
});
The easiest way is to import the shell module into your viewmodel with requirejs and then expose the shell viewmodel as a variable on the module's viewmodel.
Somehting like this:
// Some viewmodel
define(['shell'], function(shell){
var vm = {
shell: shell,
...
};
return vm;
});
Then in your view, you can bind using $root.shell
<span data-bind="text: $root.shell.shellObservable"></span>
Non of previous answers worked for me. Thus, I tried another variant of access to shell and it made the job done.
I've got this in my shell.js:
define(['plugins/router', 'durandal/app'], function (router, app) {
return {
// I need an access to this array in my view
breadcrumbs: ko.observableArray([]),
...
};
});
And this in my view:
define(function () {
var ctor = function () {
this.pageTitle = 'Preview';
this.activate = function () {
require('viewmodels/shell').breadcrumbs([...]);
};
};
return ctor;
});
So, require('viewmodels/shell') actually returns reference to a shell object.
Related
QueryRenderer takes a “query” prop, which contains a topmost query of the application made of fragments for downstream components:
const LinkListPage = () => (<QueryRenderer
query={ rootQuery }
{ ...otherProps }
render={
(error, props) =>
<LinkList viewer={ props.viewer } />
}
/>)
/* ... */
const rootQuery = graphql`
query LinkListPageQuery {
viewer {
...LinkList_viewer
}
}
`
In the above example, the fragment “LinkList_viewer” is self-sufficient, and it tells us which container it supplies data to, and which prop it fills in.
Why the relay compiler does not assemble that root query on its own? Why do we need to repeat the typing of props.viewer, when it's immediately obvious and unambiguous what to pass where? Is there any case when manual construction of the root query helps us?
The root query is used to distinguish asking for data that is idempotent (query) from asking for data that will mutate the state (mutations) from data the behaves in other ways (subscriptions).
I think the philosophy in the Relay library is to not try and have too much magic in the implementation of using it, hence that lack of automatically passing the data in a query with only one node.
In my controller, via service, I get from DB a list of the names of widgets (eg. chart, calendar, etc). Every widget implements WidgetInterface and may need other services as its own dependencies. The list of widgets can be different for each user, so I don't know which widgets / dependencies I will need in my controller. Generally, I put dependencies via DI, using factories, but in this case I don't know dependencies at the time of controller initialization.
I want to avoid using service locator directly in controller. How can I manage that issue? Should I get a list of the names of widgets in controller factory? And depending on widgets list get all dependencies and put them to controller?
Thanks, Tom
Solution
I solved my issue in a way that suggested Kwido and Sven Buis, it means, I built my own Plugin Manager.
Advantages: I do not need use service locator directly in controller and I have clear and extensible way to get different kinds of widgets.
Thank you.
Create your own Manager, like some sort of ServiceManager, for your widgets.
class WidgetManager extends AbstractPluginManager
Take a look at: Samsonik tutorial - pluginManager. So this way you can inject the WidgetManager and only retrieve the widgets from this manager as your function: validatePlugin, checks whether or not the fetched instance is using the WidgetInterface. Keep in mind that you can still call the parent ServiceManager.
Or keep it simple and build a plugin for your controller that maps your widget names to the service. This plugin can then use the serviceLocator/Manager to retrieve your widget(s), whether they're created by factories or invokableFactories. So you dont inject all the widget directly but only fetch them when they're requested. Something realy simplistic:
protected $map = [
// Widget name within the plugin => Name or class to call from the serviceManager
'Charts' => Widget\Charts::class,
];
public function load($name)
{
if (array_key_exists($name, $this->map)) {
return $this->getServiceManager()->get($this->map[$name]);
}
return null;
}
Injecting all the Widgets might be bad for your performance so you might consider something else, as when the list of your widgets grow so will the time to handle your request.
Hope this helped you and pushed you in some direction.
This indeed is a interesting question. You could consider using Plugins for the widgets, which can be loaded on the fly.
Depency injection is a good practise, but sometimes, with dynamic content, impossible to implement.
Another way to do this, is to make your own widget-manager. This manager then can load the specific widgets you need. The widget-manager can be injected into the controller.
Edit:
As you can see above, same idea from #kwido.
I would use a separate service and inject that into the controller.
interface UserWidgetServiceInterface
{
public function __construct(array $widgets);
public function getWidget($name);
}
The controller factory
class MyControllerFactory
{
public function __invoke(ControllerManager $controllerManager, $name, $requestedName)
{
$serviceLocator = $controllerManager->getServiceLocator();
$userWidgetService = $serviceLocator->get('UserWidgetService');
return new MyController($userWidgetService);
}
}
Then the logic to load the widgets would be moved to the UserWidgetServiceFactory.
public function UserWidgetServiceFactory
{
public function __invoke(ServiceManager $serviceLocator, $name, $requestedName)
{
$userId = 123; // Load from somewhere e.g session, auth service.
$widgetNames = $this->getWidgetNames($serviceLocator, $userId);
$widgets = $this->loadWidgets($serviceManager, $widgetNames);
return new UserWidgetService($widgets);
}
public function getWidgetNames(ServiceManager $sm, $userId)
{
return ['foo','bar'];
}
public function loadWidgets(serviceManager $sm, array $widgets)
{
$w = [];
foreach($widgets as $widgetName) {
$w[$widgetName] = $sm->get($widgetName);
}
return $w;
}
}
The call to loadWidgets() would eager load all the widgets; should you wish to optimise this you could register your widgets as LazyServices
I was able to follow the instruction on adding data, that part was easy and understandable. But when I tried to follow instructions for editing data, I'm completely lost.
I am following the todo sample, which works quite well, but when I tried to add to my own project using the same principle, nothing works.
in my controller, I have the following:
function listenForPropertyChanged() {
// Listen for property change of ANY entity so we can (optionally) save
var token = dataservice.addPropertyChangeHandler(propertyChanged);
// Arrange to remove the handler when the controller is destroyed
// which won't happen in this app but would in a multi-page app
$scope.$on("$destroy", function () {
dataservice.removePropertyChangeHandler(token);
});
function propertyChanged(changeArgs) {
// propertyChanged triggers save attempt UNLESS the property is the 'Id'
// because THEN the change is actually the post-save Id-fixup
// rather than user data entry so there is actually nothing to save.
if (changeArgs.args.propertyName !== 'Id') { save(); }
}
}
The problem is that any time I change a control on the view, the propertyChanged callback function never gets called.
Here's the code from the service:
function addPropertyChangeHandler(handler) {
// Actually adds any 'entityChanged' event handler
// call handler when an entity property of any entity changes
return manager.entityChanged.subscribe(function (changeArgs) {
var action = changeArgs.entityAction;
if (action === breeze.EntityAction.PropertyChange) {
handler(changeArgs);
}
});
}
If I put a break point on the line:
var action = changeArgs.entityAction;
In my project, it never reaches there; in the todo sample, it does! It completely skips the whole thing and just loads the view afterwards. So none of my callback functions work at all; so really, nothing is subscribed.
Because of this, when I try to save changes, the manager.hasChanges() is always false and nothing happens in the database.
I've been trying for at least 3 days getting this to work, and I'm completely dumbfounded by how complicated this whole issue has been for me.
Note: I'm using JohnPapa's HotTowel template. I tried to follow the Todo editing functionality to a Tee.. and nothing is working the way I'd like it to.
Help would be appreciated.
The whole time I thought the problem was in the javascript client side end of things. Turned out that editing doesn't work when you created projected DTOs.
So in my server side, I created a query:
public IQueryable<PersonDTO> getPerson(){
return (from _person in ContextProvider.Context.Queries
select new PersonDTO
{
Id = _person.Id,
FirstName = _person.FirstName,
LastName = _person.LastName
}).AsQueryable();
}
Which just projected a DTO to send off to the client. This did work with my app in fetching data and populating things. So this is NOT wrong. Using this, I was able to add items and fetch items, but there's no information that allowed the entitymanager to know about the item. When I created an item, the entitymanager has a "createEntity" which allowed me to tell the entitymanager which item to use.. in my case:
manager.createEntity(person, initializeValues);
Maybe if there was a "manager.getEntity" maybe that would help?
Anyways, I changed the above query to get it straight from the source:
public IQueryable<Person> getPeople(){
return ContextProvider.Context.People;
}
Note ContextProvider is:
readonly EFContextProvider<PeopleEntities> ContextProvider =
new EFContextProvider<PeopleEntities>();
So the subscribe method in the javascript checks out the info that's retrieved straight from the contextual object.. interesting. Just wish I didn't spend 4 days on this.
I'm trying out Silex and I'm having a bit of a problem, or I might say, more of an inconvenience...
I'm trying to load 2 routes from 2 separate yaml files, but for some reason the mounting ($app->mount(...)) doesn't work with closures.
Here's some code:
// load configuration
$loader->load('core.yml');
$loader->load('api.yml');
function bla($app, $container, $key) {
$myApp = $app['controllers_factory'];
foreach ($container->getExtensionConfig('routes')[$key] as $name => $route) {
$controller = $myApp->match($route['pattern'], $route['controller']);
$controller->method($route['requirements']['_method']);
$controller->bind($name);
}
return $myApp;
}
$app->mount('/core', bla($app, $container, 0));
$app->mount('/api', bla($app, $container, 1));
This works.
What doesn't work is if I do the exact same thing with closures, like this:
$app->mount('/core', function ($app, $container, $key) {
return $app['controllers_factory'];
});
Gives the following error:
LogicException: The "mount" method takes either a ControllerCollection or a ControllerProviderInterface instance.
But
var_dump($app['controllers_factory']);
spits out an object of type Silex\ControllerCollection.
I'm obviously missing something.
Thank you for your help.
The problem
In your first example, you're mounting the result of a function. In your second example, you're mounting the function itself.
Function bla() returns the controller collection when it's called. When you do
$app->mount('/core', bla($app, $container, 0));
the function is executed first and then the returned ControllerCollection is mounted.
But when you do
$app->mount('/core', function ($app, $container, $key) {...});
the function is not executed. It is treated as an object and mounted. Since the function itstelf is not a ControllerCollection or a ControllerProviderInterface, you get the error.
Two alternatives
Use PHP routing
This is how I like to do it. I don't know if this is "the Silex way", but it works well for me.
You mount each controller collection like so:
$app->mount('/core', include 'controllers/core.php');
$app->mount('/api', include 'controllers/api.php');
Each controller collection goes in a separate file in the controllers folder. So api.php might look like this:
$controllers = $app['controllers_factory'];
$controllers->get('/version', function() use ($app) {
// do whatever you want
return 'version 1.2';
});
return $controllers;
There may even be a way of doing this using the YML loader and keeping your routes in yml files, but I don't like mixing yml and php in general. Why use two technologies when you can just use one.
A fancier way
Take a look at this article. His technique is way more elegant than mine, but also more complicated. It's probably better for larger projects. Maybe it will work better for you.
I'm using the HotTowel SPA template which makes use of Durandal. In my Durandal ViewModels I am using Breeze to get some data from the database.
I have a datacontext class that I put all my breeze queries in and the queries all follow the pattern like the following:
getAthletes: function (queryCompleted) {
var query = breeze.EntityQuery.from("Athletes");
return manager
.executeQuery(query)
.then(queryCompleted)
.fail(queryFailed)
}
Since I'm doing an asynchronous call in the activate method of the view model, I have to return the promise that comes back from these calls in the activate method.
Using a single query works great like this:
function activate() {
datacontext.getAthlete(loadAthlete);
}
However, if I need to perform two queries I run into problems, but only in the release version of my application. I have tried doing this with the following syntax:
function activate() {
datacontext.getAthlete(loadAthlete).then(datacontext.getOtherData(loadOtherData));
}
This will work fine in debug mode, but when I deploy it out to the server and my scripts get bundled, I get an exception which isn't very clear.
t is not a function
I've also tried chaining them together in my datacontext class like below, but I still get the same error.
getAthleteAndEfforts: function (athleteId, athleteQueryCompleted, effortsQueryCompleted) {
var athleteQuery = breeze.EntityQuery.from("Athletes").where("id", "==", athleteId);
var effortsQuery = breeze.EntityQuery.from("BestEfforts").where("athleteId", "==", athleteId);
return manager.executeQuery(athleteQuery).then(athleteQueryCompleted)
.then(manager.executeQuery(effortsQuery).then(effortsQueryCompleted))
.fail(queryFailed);
}
So I'm assuming I just don't understand the Q.defer() enough to use it properly or there is something else going on.
What is the correct syntax to accomplish this?
Ok, thanks to RainerAtSpirit for pointing me in the right direction to find this. I looked at John Papa's jumpstarter examples and he has a datacontext that does this under the primeData function.
So using the syntax he used there I was able to get it to work correctly like this:
getAthleteAndEfforts: function (athleteId, athleteQueryCompleted, effortsQueryCompleted) {
return Q.all([
datacontext.getAthlete(athleteId, athleteQueryCompleted),
datacontext.getAthleteEfforts(athleteId, effortsQueryCompleted)]);
}
I had seen the Q.all in the Q documentation but wasn't sure how to use it, but this example helped. I tested this and it works both in debug and release modes.
Not sure why the first version is working at all, but you'd return a promise when datacontext is making async calls.
function activate() {
return datacontext.getAthlete(loadAthlete);
}
or
function activate() {
return datacontext.getAthlete(loadAthlete).then( return datacontext.getOtherData(loadOtherData));
}
Check #John Papa's jumpstarter for more examples: https://github.com/johnpapa/PluralsightSpaJumpStartFinal/search?q=activate