This is for an AngularJS app. I have a custom directive that depends on a service.
What I'm really curious about is the "angular way" to deal with a user action that impacts both model and DOM. Some example code:
HTML:
<form foo-places>
<!--other stuff -->
<span ng-repeat="place in places">
<button ng-click="removePlace(place)">remove {{place}}</button>
</span>
</form>
JS:
angular.module('foo.directives', []).directive('fooPlaces',
function(placesService) {
return {
controller : function($scope) {
$scope.places = placesService.places;
$scope.removePlace = function(name) {
placesService.removePlace(name);
};
$scope.$on('placesChanged', function() {
$scope.places = placesService.places;
});
},
link : function($scope, element, attrs) {
//code to do stuff when user removes a place
}
}
})
When a user removes a place (by clicking a button), I also need to do stuff to mess with the DOM, for example, scroll the window to the top, etc. It feels weird to have a function in the controller that deals with the model and then another function in the directive that does the DOM stuff...but both based on the same user action.
Am I over-thinking this or really missing something? How should I handle a single user action that deals with both model and DOM?
When you are dealing with AngularJS you might have heard the phrase "The model is the single source of truth". If you understand this part, then the rest of the things fall easily into place. This is the "Angular way".
When the user interacts - he is not interacting with the DOM or the view. He is interacting with the model. The view itself is just a "view" of the model. There could be other views of the same model - which is why the model is the single source of truth. Now, what angular allows you to do is make changes to the model when the user interacts. You make these changes and because the model has changed, the view's start reflecting the changed state of the model.
Also, just to emphasize the separation of concerns - a directive should rarely, deal with a service directly. A directive is a piece of the DOM, which means it is a piece of the view. A service generally has something to do with business logic or represents a model. In MVC or MVVM you dont directly make the View interact with the Model. You always use the ViewModel or Controller in between. This keeps the dependencies to a minimum.
Your ScrollToTop could be a service that you call from your controller (look at $anchorScroll which is a service in Angular ). It doesnt do what you want, but its a scrolling service, which is how you need to implement yours too.
EDIT :
To clarify, you dont generally do DOM manipulately stuff in services. The scenario where you could consider DOM manipulatey stuff in the service, is when, what you are trying to do does not belong to any particular html element, but something that needs to happen on your app level.
Let me explain that. For example, if you are attempting to do something like a dialog / modal window - In angularJS, you would think, the ideal place for something like this is a directive since it is a generic UI component. But if you think about it, a directive in AngularJS is something associated with an element. You always associate a directive with a html element. But as we have seen, a dialog isnt something that you attach to an element, but rather something that is global in nature. This is probably an exception.
The same also holds true to some $window and $document related stuff ( scrolling for example ). These dont belong to any particular element (if you want to scroll inside a div, it should be a directive ), hence they need to be a service. Also, this is a service you could probably inject into a directive. Say each time your directive is triggered you want to scrollToTop or open a dialog. You could inject these kind of services into your directives. The kind of services that you probably should not inject into a directive are services that are associated with business logic. Treat a directive as a re-usable UI component.
Ofcourse, you could create a higher level component ( the stuff you are trying ) which creates a DSL, but then you need to know exactly what you are doing. Until then, I suggest you stick with the plain old controller, directive and services and each managing their own concerns.
Related
We are working on tracking a site which has components built using Shadow DOM concepts, when we are creating a rule in launch to add tagging to these components it’s not working.
Can you guide us with best practice on tagging components in Shadow DOM?
I found unanswered questions about google analytics Google analytics inside shadow DOM doesn't work is this true for adobe analytics also?
Best Practice
Firstly, the spirit of using Shadow DOM concepts is to provide scope/closure for web components, so that people can't just go poking at them and messing them up. In principle, it is similar to having a local scoped variable inside a function that a higher scope can't touch. In practice, it is possible to get around this "wall" and have your way with it, but it breaks the "spirit" of shadow DOM, which IMO is bad practice.
So, if I were to advise some best practice about any of this, my first advice is to as much as possible, respect the spirit of web components that utilize shadow DOM, and treat them like the black box they strive to be. Meaning, you should go to the web developers in charge of the web component and ask them to provide an interface for you to use.
For example, Adobe Launch has the ability to listen for custom events broadcast to the (light) DOM, so the site developers can add to their web component, create a custom event and broadcast it on click of the button.
Note: Launch's custom event listener will only listen for custom event broadcasts starting at document.body, not document, so make sure to create and broadcast custom events on document.body or deeper.
"But the devs won't do anything so I have to take matters into my own hands..."
Sadly, this is a reality more often than not, so you gotta do what you gotta do. If this is the case, well, Launch does not currently have any native features to really make life easier for you in this regard (for the "core" part of the below stuff, anyways), and as of this post, AFAIK there are no public extensions that offer anything for this, either. But that doesn't mean you're SoL.
But I want to state that I'm not sure I would be quick to call the rest of this answer "Best Practice" so much as "It's 'a' solution..". Mostly because this largely involves just dumping a lot of pure javascript into a custom code box and calling it a day, which is more of a "catch-all, last resort" solution.
Meanwhile, in general, it's best practice to avoid using custom code boxes when it comes to tag managers unless you have to. The whole point of tag managers is to abstract away the code.
I think the TL;DR here is basically me reiterating this something that should ideally be put on the site devs' plate to do. But if you still really need to do it all in Launch because ReasonsTM, keep on reading.
'A' Solution...
Note: This is a really basic example with a simple open-mode shadow DOM scenario - in reality your scenario is almost certainly a lot more complex. I expect you to know what you're doing with javascript if you're diving into this!
Let's say you have the following on the page. Simple example of a custom html element with a button added to its shadow DOM.
<script>
class MyComponent extends HTMLElement {
constructor() {
super();
this._shadowRoot = this.attachShadow({
mode: 'open'
});
var button = document.createElement('button');
button.id = 'myButton';
button.value = 'my button value';
button.innerText = 'My Button';
this._shadowRoot.appendChild(button);
}
}
customElements.define('my-component', MyComponent);
</script>
<my-component id='myComponentContainer'></my-component>
Let's say you want to trigger a rule when a visitor clicks on the button.
Quick Solution Example
At this point I should probably say that you can get away with doing a Launch click event rule with query selector my-component#myComponentContainer with a custom code condition along the lines of:
return event.nativeEvent.path[0].matches('button#myButton');
Something like this should work for this scenario because there are a lot of stars aligned here:
The shadow dom is open mode, so no hacks to overwrite things
There are easily identifiable unique css selectors for both light and shadow DOM levels
You just want to listen for the click event, which bubbles up and
acts like a click happened on the shadow root's light DOM root.
In practice though, your requirements probably aren't going to be this easy. Maybe you need to attach some other event listener, such as a video play event. Unfortunately, there is no "one size fits all" solution at this point; it just depends on what your actual tracking requirements are.
But in general, the goal is pretty much the same as what you would have asked the devs to do: create and broadcast a custom (light) DOM event within the context of the shadow DOM.
Better Solution Example
Using the same component example and requirement as above, you could for example create a rule to trigger on DOM Ready. Name it something like "My Component Tracking - Core" or whatever. No conditions, unless you want to do something like check if the web component's root light DOM element exists or whatever.
Overall, this is the core code for attaching the event listener to the button and dispatching a custom event for Launch to listen for. Note, this code is based on our example component and tracking requirements above. It is unique to this example. You will need to write similar code based on your own setup.
Add a custom js container with something along the lines of this:
// get the root (light dom) element of the component
var rootElement = document.querySelector('#myComponentContainer');
if (rootElement && rootElement.shadowRoot) {
// get a reference to the component's shadow dom
var rootElementDOM = rootElement.shadowRoot;
// try and look for the button
var elem = rootElementDOM.querySelector('button#myButton');
if (elem) {
// add a click event listener to the button
elem.addEventListener('click', function(e) {
// optional payload of data to send to the custom event, e.g. the button's value
var data = {
value: e.target.value
};
// create a custom event 'MyButtonClick' to broadcast
var ev = new CustomEvent('MyButtonClick', {
detail: data
});
// broadcast the event (remember, natively, Launch can only listen for custom events starting on document.body, not document!
document.body.dispatchEvent(ev);
}, false);
}
}
From here, you can create a new rule that listens for the custom event broadcast.
Custom Event Rule Example
Rule name: My Button clicks
Events
Extension: Core
Event Type: Custom Event
Name: MyButtonClick
Custom Event Type: MyButtonClick
Elements matching the CSS selector: body
Conditions
*None for this scenario*
From here, you can set whatever Actions you want (set Adobe Analytics variables, send beacon, etc.).
Note:
In this example, I sent a data payload to the custom event. You can reference the payload in any custom (javascript) code box with event.detail, e.g. event.detail.value. You can also reference them in Launch fields with the % syntax, e.g. %event.detail.value%.
I would like to create a jumpbox block with some form and put it within the layout sidebar. The form will be have an entity select and exactly the Go button.
Base on the documentation I need to render the form template by using {{ render(controller(...)) }}, but I really don't where tu put the form logic.
It is good to create a method in the controller that use entities from the select? but it looks I need to create two methods, the first one for form rendering (without any route) and the second one for the form submit request (with route for "POST" method)?
Can somebody provide me some tips how to do it right way?
First you will need a controller as you sad, this will render this part of the sidebar, and the action should be the same controller, so just simply create a router for it.
And you were also sad right, create a hidden field and set there the current route. But this is the tricky part, cause when you call this "sub" render, inside the controller the route will be always, what is the route for the controller, so what you need to do, when you render the controller you need to pass in a variable what is the current route, what you can do easily by passing the {{ app.request.attributes.get('_route') }} variable value, what is the NAME of the route, and then in your controller, at the end you return a new RedirectResponse($this->generateUrl($url)).
And both, the render of the form and the "process" can be in the same controller, or if you prefer it, you can take it apart, but I would use only one, and you can test from the request, what is the current method, if POST then you will search for the variable and set the session/cookie/ what do you have.
EDIT:
Even though you didn't like, you have to say it's a good answer, but here is an other one.
The action should be always the current route, and basically you need to set up a request event listener. Check there if it's a post method and if yes, then look for your specific key, and there you go. Both is equally good and I used both of them.
I am facing one issue and I am not sure if what i would like to do makes sense.
In fact I would like to set layout's body onload from one particular view. In my approach onload should not be modified in case user is elsewhere than one view.
Do you know if it is possible at all?
Kind Regards,
You could do that with Route Context. You can add unique class for actions/action that you want the onload to be added on. And then use some javascript library depending on which class the body tag has.
Once you have body classes that are unique for various modules, controllers, and actions you can use those as part of your selectors in jQuery (or whatever JavaScript library you're
using).
Ref: Route Context
Hope it helps
Having worked with .net in both winforms and ASP.net for a few years I am now starting to get into MVC (a little late I know). One major confusion for me is the concept of reusable 'components', similar to the concept of a usercontrol in webforms.
For example, I would like to have a number of 'widgets' within the members area of my site, one of which is the details of the logged in users account manager. I can create this as a partial however when the page loads the data needs to be passed in as part of the ViewModel / View Data. I would like to use this widget in a number of different sections which would then mean that I need to put the code to pass the data in into a number of different controllers. This seems to violate the DRY principle, or am I missing something here? I would ideally like everything to be encapsulated within the 1 partial which can then be used in any page.
You can go three ways:
1) For simple controls without much logic, you can create new instance of the custom view model for the control:
Html.RenderPartial("YourControl", new YourControlViewModel () { Param1="value1", Param2 = Model.AnotherValue });
2) If you need some back end logic for the control, you can use
Html.RenderAction("ActionName", "SomeControllerName", RouteValuesDictionary);
It will call standard controller action, use the view, and insert the resulting output back to the page. You can add [ChildActionOnly] atribute to the controller method to ensure that the method will be available only from the Html.RenderPartial. It is slightly violating the MVC principle (view shouldn't call controller), but its great for widgets, and it is used in the Ruby on Rails world without much issues. You can check great article from Haacked
3) Create custom html helper for tasks like custom date formatting, calculating etc..
In your case, I would choose the number two.
I've been working with Zend Framework (using Doctrine as the ORM) for quite a while now, and done a few projects with it.
In a few upcoming projects I am requiring the need for widgets similar to how Wordpress does them. You have a post/page, which could look like:
Subscribe to my newsletter:
[subscribe/]
View my events
[events limit=5 sort=date/]
View this page's comments
[comments/]
Where say the subscribe widget would be replaced with Blog::subscribeWidget, and the events could be replaced with Events::eventsWidget, etc.
Now it has done my head in the past few weeks about how on earth do I do this??? I've come up with the following options:
I could place the widgets within controllers, and then call them like actions. Problem here is that code could be flying between controllers, and I have read this is expensive due to the amount of dispatches.
I could place the widgets as view helpers. So within the view I could have $this->renderPage($Page), which would then attend to all the widgets. Problem here is that what if the widgets need to do some business logic, like for example posting a new comment, that really shouldn't be within the view, should it?
The other option is to place widgets within the model? But then how on earth do they then render content for display?
Extra complications come when:
Say the comments widget would also handle posting, deleting of comments etc.
Say for the events listing, if I want to do an ajax request to the next page of events, using method #2 (view helpers) how would this work?
If I understand you correctly your widgets will need their own action controllers, which is where their logic for fetching data to be displayed, parsing form submissions, etc. should go. The difference between a widget and a page in this case is in how it's rendered, i.e. as an HTML fragment instead of as a whole page; you can use the Action View Helper to achieve this.
If your widget includes a form it should probably use AJAX to submit the form data back to the server, so that using the widget doesn't cause the user to accidentally navigate away from the page. You can inject the required JavaScript into the page you've included the widget into by using the Head Script Helper in your widget's view and/or action.
I left Richards reply, the problem, and further use cases cook in my brain for a while longer and ended up coming to a solution.
I will have the following view helpers and methods:
Content; with methods: render, renderWidgets, renderWidget, renderCommentsWidget (comments).
Event; with methods: renderEventsWidget (many events), renderEventWidget (one event)
Subscription; with methods: renderSubscribeWidget (subscription form).
I will have inside my configuration file:
app.widgets.comments.helper = content
app.widgets.subscribe.helper = subscription
app.widgets.events.helper = event
I will also have the following models:
Content for use for all pages.
Event for use for all events.
Subcriber for use for subscriptions to content
So inside my view I will do something like this:
echo $this->content()->render($this->Content)
Content::render() will then perform any content rendering and then perform rendering of the widgets by passing along to Content::renderWidgets(). Here we will use the configuration of app.widgets to link together the widget bbcode tag to it's appropriate view helper (using the naming convention 'render'.ucfirst($tag).'Widget'). So for example Content::renderCommentsWidget() would then proceed to render the comments.
Perhaps later on I will decide to have a Widget View Helper, and individual view helpers for each widget eg. ContentCommentsWidget View Helper. But for now that would just add additional unrequired complexity.
Now to answer the AJAX problem I mentioned. Say for the comments widget allowing for comments to be posted via ajax. It would simply have an appropriate method inside the Content Controller for it. So pretty much we also have a Event and Subscription controllers too - corresponding with the view helpers. Interaction between the view helper and controller will all be hard coded, there is no purpose for it to be soft coded.
I hope this helps someone else, and the current plan is to make the project where all this is used to be an open-source project. So maybe one day you can see it all in action.
Thanks.
Update:
You can find the source code of these ideas in action in the following repositories:
BalCMS - this is the actual CMS which contains the widgets in /application/modules/balcms/view/helpers and contains the configuration in /application/modules/config/application/balcms.yaml
BalPHP - this is the resource library which contains the widget view helper at /lib/Bal/View/Helper/Widget.php