32 | ```
33 |
34 | A pagination is simply an un-ordered list with the `pagination` class.
35 | Each item (`
`) inside the pagination widget can be in one of 3 states, marked with a CSS class:
36 | * default (no additional CSS class)
37 | * `active` - indicating that a give item is selected
38 | * `disabled` - indicating that a give item is disabled and can't be selected
39 |
40 | See index.html which is already wired to use the component you are about to write.
41 |
--------------------------------------------------------------------------------
/src/01_widgets/exercise/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Custom widgets
5 |
6 |
7 |
8 |
9 |
16 |
17 |
18 |
19 |
15 |
--------------------------------------------------------------------------------
/src/01_widgets/exercise/solution/pagination.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.pagination', [])
2 | .directive('bsPagination', function () {
3 | return {
4 | restrict: 'E',
5 | scope: {
6 | selectedPage: '=',
7 | collectionSize: '=',
8 | itemsPerPage: '='
9 | },
10 | templateUrl: '/src/01_widgets/exercise/solution/pagination.html',
11 | link: function (scope, iElement, iAttrs) {
12 |
13 | function updatePagesModel() {
14 |
15 | //re-calculate new length of pages
16 | var pageCount = Math.ceil(scope.collectionSize / (scope.itemsPerPage || 10));
17 |
18 | //fill-in model needed to render pages
19 | scope.pageNumbers.length = 0;
20 | for (var i = 1; i <= pageCount; i++) {
21 | scope.pageNumbers.push(i);
22 | }
23 |
24 | //make sure that the selected page is within available pages range
25 | scope.selectPage(scope.selectedPage);
26 | }
27 |
28 | scope.pageNumbers = [];
29 |
30 | scope.hasPrevious = function () {
31 | return scope.selectedPage > 1;
32 | };
33 |
34 | scope.hasNext = function () {
35 | return scope.selectedPage < scope.pageNumbers.length;
36 | };
37 |
38 | scope.selectPage = function (pageNumber) {
39 | scope.selectedPage = Math.max(Math.min(pageNumber, scope.pageNumbers.length), 1);
40 | };
41 |
42 | //re-render pages on collection / page size changes
43 | scope.$watch('collectionSize', updatePagesModel);
44 | scope.$watch('itemsPerPage', updatePagesModel);
45 |
46 | //make sure that page is within available pages range on model changes
47 | scope.$watch('selectedPage', scope.selectPage);
48 | }
49 | };
50 | });
51 |
--------------------------------------------------------------------------------
/src/01_widgets/exercise/solution/pagination.spec.js:
--------------------------------------------------------------------------------
1 | describe('pagination', function () {
2 | var $scope, $compile;
3 |
4 | beforeEach(module('bs.pagination'));
5 | beforeEach(module('templates'));
6 |
7 | beforeEach(inject(function ($rootScope, _$compile_) {
8 | $scope = $rootScope;
9 | $compile = _$compile_;
10 | }));
11 |
12 | beforeEach(function() {
13 | this.addMatchers({
14 | toHavePageStates: function(requiredStates) {
15 |
16 | var states = [];
17 | var pages = this.actual.find('li');
18 |
19 | for (var i=0; i', $scope);
51 | expect(elm).toHavePageStates([0, 0, 1, 0, 0]);
52 |
53 | $scope.$apply(function(){
54 | $scope.myPage = 0;
55 | });
56 | expect(elm).toHavePageStates([-1, 1, 0, 0, 0]);
57 | });
58 |
59 |
60 | it('should re-render pages in response to collection size change', function () {
61 |
62 | $scope.myPage = 5;
63 | $scope.myCollectionLen = 50;
64 | var elm = compileElement('', $scope);
65 | expect(elm).toHavePageStates([0, 0, 0, 0, 0, 1, -1]);
66 |
67 | $scope.$apply(function(){
68 | $scope.myCollectionLen = 30;
69 | });
70 |
71 | expect(elm).toHavePageStates([0, 0, 0, 1, -1]);
72 | expect($scope.myPage).toBe(3);
73 | });
74 |
75 |
76 | it('should re-render pages in response to selected page change', function () {
77 |
78 | $scope.myPage = 5;
79 | $scope.myCollectionLen = 50;
80 | var elm = compileElement('', $scope);
81 |
82 | $scope.$apply(function(){
83 | $scope.myPage = 4;
84 | });
85 | expect(elm).toHavePageStates([0, 0, 0, 0, 1, 0, 0]);
86 | });
87 |
88 |
89 | it('should correct selected page to be within available pages range', function () {
90 |
91 | $scope.myPage = -5;
92 | $scope.myCollectionLen = 50;
93 | var elm = compileElement('', $scope);
94 |
95 | expect(elm).toHavePageStates([-1, 1, 0, 0, 0, 0, 0]);
96 | expect($scope.myPage).toBe(1);
97 |
98 | $scope.$apply(function(){
99 | $scope.myPage = 10;
100 | });
101 | expect(elm).toHavePageStates([0, 0, 0, 0, 0, 1, -1]);
102 | expect($scope.myPage).toBe(5);
103 | });
104 | });
105 |
106 |
107 | describe('Ui to model', function () {
108 |
109 | it('should update selected page on page no click', function () {
110 |
111 | $scope.myPage = 4;
112 | $scope.myCollectionLen = 50;
113 | var elm = compileElement('', $scope);
114 |
115 | //select
116 | elm.find('li > a').eq(1).click();
117 | expect($scope.myPage).toBe(1);
118 | });
119 |
120 |
121 | it('should update selected page on page arrow clicks', function () {
122 |
123 | $scope.myPage = 1;
124 | $scope.myCollectionLen = 20;
125 | var elm = compileElement('', $scope);
126 |
127 | elm.find('li > a').eq(3).click();
128 | expect(elm).toHavePageStates([0, 0, 1, -1]);
129 |
130 | elm.find('li > a').eq(0).click();
131 | expect(elm).toHavePageStates([-1, 1, 0, 0]);
132 | });
133 | });
134 | });
135 |
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 |
3 | This demo covers an alert directive as seen on Bootstrap's [demo page](http://getbootstrap.com/components/#alerts).
4 | It is typical example of a "widget with a hole", that can wrap over other HTML markup with AngularJS directives.
5 |
6 | Example usage:
7 |
8 | ```html
9 | Hey, it worked!
10 | ```
11 |
12 | This simple directive also demonstrates how AngularJS directives can be used to create your own HTML vocabluary and
13 | remove HTML markup duplication.
14 |
15 | ## Covered topics
16 |
17 | * Understanding transclusion:
18 | * conceptually it takes content from the directive element and puts it in the final template,
19 | * content can contain other AngularJS directives so it is all recursive.
20 | * Indicating where the transcluded content should go:
21 | * we can mark a place where the content should be placed by using the `ngTransclude` directive,
22 | * if a more fine-grained control is needed we could use a transclusion function.
23 | * Transclusion scope and its pitfalls (to be demonstrated with an input)
24 | * Multiple elements asking for transclusion on the same element
25 | * Reminders:
26 | * A widget usually means element-level directive
27 | * We need an isolated scope here since a widgets has its own model
28 | * Don't hesitate to create such simple directives as those remove markup duplication and create your own DSL
29 |
30 | ## Bootstrap CSS
31 |
32 | Bootstrap 3 is using the following HTML structure to render the alert widget:
33 |
34 | ```html
35 |
36 |
37 |
Alert's content goes here...
38 |
39 | ```
40 |
41 | where the `[alert type]` class can be one of: `alert-success`, `alert-info`, `alert-warning`, `alert-danger`.
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/demo/alert.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/demo/alert.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.alert', [])
2 | .directive('bsAlert', function () {
3 | return {
4 | restrict: 'E', // trigger on Element e.g. ``
5 | templateUrl: '/src/02_widgets_with_holes/demo/alert.html', // template location
6 | transclude: true, // enable transclusion of contents of the template element
7 | scope: { // isolate scope variable mappings
8 | type: '@', // one-way data-binding from the current `type` attribute value to scope
9 | close: '&' // expose function that will evaluate expression specified by `close` attribute
10 | },
11 | link: function (scope, iElement, iAttrs) {
12 | scope.closeable = "close" in iAttrs;
13 | }
14 | };
15 | });
16 |
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/demo/alert.spec.js:
--------------------------------------------------------------------------------
1 | describe("alert", function () {
2 |
3 | var $scope, $compile;
4 |
5 | beforeEach(module('bs.alert'));
6 | beforeEach(module('templates'));
7 |
8 | beforeEach(inject(function ($rootScope, _$compile_) {
9 | $scope = $rootScope;
10 | $compile = _$compile_;
11 | }));
12 |
13 | function compileElement(elementString, scope) {
14 | var element = $compile(elementString)(scope);
15 | scope.$digest();
16 | return element;
17 | }
18 |
19 | function findCloseButton(element) {
20 | return element.find('button.close');
21 | }
22 |
23 | it('should set "warning" CSS class by default', function () {
24 | var element = compileElement('Default', $scope);
25 | expect(element.find('div.alert')).toHaveClass('alert-warning');
26 | });
27 |
28 | it('should set appropriate CSS class based on the alert type', function () {
29 | var element = compileElement('Info', $scope);
30 | expect(element.find('div.alert')).toHaveClass('alert-info');
31 | });
32 |
33 | it('should not show close buttons if no close callback specified', function () {
34 | var element = compileElement('No close', $scope);
35 | expect(findCloseButton(element).is(':visible')).toBeFalsy();
36 | });
37 |
38 | it('should fire callback when closed', function () {
39 | $scope.removeAlert = function() {
40 | $scope.removed = true;
41 | };
42 | var element = compileElement('Has close', $scope);
43 |
44 | findCloseButton(element).click();
45 | expect($scope.removed).toBeTruthy();
46 | });
47 |
48 | });
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - using $render()
5 |
6 |
7 |
8 |
9 |
26 |
27 |
28 |
29 |
30 |
31 | Hello, {{model.name}}!
32 |
33 | Hey, this is just a default warning, you can't close me. I can contain markup and even other
34 | directive (although bound to a different scope that you might expect).
35 |
36 |
37 |
38 |
39 | {{alert.msg}}
40 |
41 |
42 |
43 |
44 |
45 |
--------------------------------------------------------------------------------
/src/02_widgets_with_holes/exercise/README.md:
--------------------------------------------------------------------------------
1 | ## Exercise
2 |
3 | Based on the experience gained from analyzing the `bs-alert` directive, create a collapse directive
4 | that could be used to quickly create collapsible sections with a title, similar to the one on the
5 | [Bootstrap 3 demo page](http://getbootstrap.com/javascript/#collapse).
6 |
7 | Example usage:
8 |
9 | ```html
10 |
11 | So I can show and hide this content...
12 |
13 | ```
14 |
15 | It should be possible to interpolate the `heading` attribute so the following syntax could be used:
16 | `heading="A title: {{title}}"`.
17 |
18 |
19 | ## Bootstrap CSS
20 |
21 | Bootstrap 3 is using the following HTML structure to render the alert widget:
22 |
23 | ```html
24 |
16 |
17 |
18 | Anim pariatur cliche reprehenderit, enim eiusmod high life accusamus terry richardson ad squid. 3 wolf moon
19 | officia aute, non cupidatat skateboard dolor brunch. Food truck quinoa nesciunt laborum eiusmod. Brunch 3 wolf
20 | moon tempor, sunt aliqua put a bird on it squid single-origin coffee nulla assumenda shoreditch et. Nihil anim
21 | keffiyeh helvetica, craft beer labore wes anderson cred nesciunt sapiente ea proident. Ad vegan excepteur
22 | butcher vice lomo. Leggings occaecat craft beer farm-to-table, raw denim aesthetic synth nesciunt you probably
23 | haven't heard of them accusamus labore sustainable VHS.
24 |
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/03_attribute_directives/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 |
3 | This demo shows how to build a simple tooltip directive,
4 | similar to the one shown on the Bootstrap's [demo page](http://getbootstrap.com/javascript/#tooltips)
5 |
6 | ## Covered topics
7 |
8 | * creating directive basics
9 | * declaring directives - `.directive()` factory method
10 | * factory method vs. compile vs. link
11 | * prefix directives to avoid collisions, ideally with your own project identifier (2-3 letters)
12 | * DOM manipulation goes into directives
13 | * direct DOM manipulation in a directive - jQuery can be useful for low-level DOM routines
14 | * element passed to a directive is already jQuery / jqLite - wrapped
15 | * observing attributes vs. attribute value straight from the DOM
16 | * registering DOM event handlers
17 | * normalization of directive / attribute names
18 | * tests
19 | * introduction to the DOM-based directives testing
20 | * jQuery is useful for:
21 | * matching rendered HTML
22 | * triggering events
23 |
24 | ## Bootstrap CSS
25 |
26 | Bootstrap 3 uses the following markup to create tooltip elements:
27 |
28 | ```html
29 |
30 |
I'm tooltip's content
31 |
32 |
33 | ```
34 |
35 | Tooltips, after being created are inserted after the host element in the DOM tree.
36 | Tooltip's text goes into the `div.tooltip-inner` element.
37 |
38 | There are 2 additional important CSS classes at play as well:
39 | * - one of `top`, `bottom`, `left`, `right` - needs to be added to `div.tooltip` to indicate positioning
40 | * - `in` - to actually show a tooltip
41 |
42 | Tooltip can be seen in action on Bootstrap's [demo page](http://getbootstrap.com/javascript/#tooltips)
43 |
--------------------------------------------------------------------------------
/src/03_attribute_directives/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - using $render()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
';
11 |
12 | return {
13 |
14 | compile: function compileFunction(tElement, tAttrs) {
15 |
16 | // prepare directive template!
17 | // this executes only once for each occurrence of `tooltip` *in template*
18 |
19 | var placement = tAttrs.bsTooltipPlacement || 'top';
20 | var tooltipTplEl = angular.element(tooltipTpl);
21 | tooltipTplEl.addClass(placement);
22 |
23 | return function linkingFunction(scope, iElement, iAttrs) {
24 |
25 | // instantiate directive!
26 | // this executes once for each occurrence of `tooltip` *in the view*
27 |
28 | var tooltipInstanceEl = tooltipTplEl.clone();
29 |
30 | //observe interpolated attributes and update tooltip's content accordingly
31 | iAttrs.$observe('bsTooltip', function (newContent) {
32 | tooltipInstanceEl.find('div.tooltip-inner').text(newContent);
33 | });
34 |
35 | iElement.on('mouseenter', function () {
36 |
37 | //attach tooltip to the DOM to get its size (needed to calculate positioning)
38 | iElement.after(tooltipInstanceEl);
39 |
40 | //calculate position
41 | var ttipPosition = calculatePosition(iElement, tooltipInstanceEl, placement);
42 | tooltipInstanceEl.css(ttipPosition);
43 | //finally show the tooltip
44 | tooltipInstanceEl.addClass('in');
45 | });
46 |
47 | iElement.on('mouseleave', function () {
48 | tooltipInstanceEl.remove();
49 | });
50 | };
51 | }
52 | };
53 | });
54 |
--------------------------------------------------------------------------------
/src/03_attribute_directives/demo/tooltip.spec.js:
--------------------------------------------------------------------------------
1 | describe('tooltip', function () {
2 |
3 | var $scope, $compile;
4 |
5 | beforeEach(module('bs.tooltip'));
6 | beforeEach(inject(function ($rootScope, _$compile_) {
7 | $scope = $rootScope;
8 | $compile = _$compile_;
9 | }));
10 |
11 | function compileElement(elementString, scope) {
12 | var element = $compile(elementString)(scope);
13 | scope.$digest();
14 | return element;
15 | }
16 |
17 |
18 | it('should show and hide tooltip on mouse enter / leave', function () {
19 | var elm = compileElement('', $scope);
20 |
21 | elm.find('button').mouseenter();
22 | expect(elm.find('.tooltip').length).toEqual(1);
23 |
24 | elm.find('button').mouseleave();
25 | expect(elm.find('.tooltip').length).toEqual(0);
26 | });
27 |
28 |
29 | it('should observe interpolated content', function () {
30 | $scope.content = 'foo';
31 | var elm = compileElement('', $scope);
32 |
33 | elm.find('button').mouseenter();
34 | expect(elm.find('.tooltip-inner').text()).toEqual('foo');
35 |
36 | $scope.content = 'bar';
37 | $scope.$digest();
38 | expect(elm.find('.tooltip-inner').text()).toEqual('bar');
39 | });
40 |
41 |
42 | describe('placement', function () {
43 |
44 | it('should be placed on top by default', function () {
45 | var elm = compileElement('', $scope);
46 |
47 | elm.find('button').mouseenter();
48 | expect(elm.find('.tooltip')).toHaveClass('top');
49 | });
50 |
51 |
52 | it('should accept placement attribute', function () {
53 | var elm = compileElement('', $scope);
54 |
55 | elm.find('button').mouseenter();
56 | expect(elm.find('.tooltip')).toHaveClass('right');
57 | expect(elm.find('.tooltip')).not.toHaveClass('top');
58 | });
59 | });
60 | });
61 |
--------------------------------------------------------------------------------
/src/03_attribute_directives/exercise/README.md:
--------------------------------------------------------------------------------
1 | ## Exercise
2 |
3 | Create a popover directive similar to the one seen on the
4 | Bootstrap's [demo page](http://getbootstrap.com/javascript/#popovers)
5 |
6 | You don't need to spend time on DOM-positioning logic - there is an utility function -
7 | `calculatePosition(hostEl, elToPosition, placement)` -
8 | that can position a DOM element in relation to another one (`/lib/positioning.js`).
9 | Check its jsDoc for more details.
10 |
11 | ## Bootstrap CSS
12 |
13 | Bootstrap 3 uses the following markup to create popover elements:
14 |
15 | ```html
16 |
17 |
18 |
I'm a title!
19 |
Content goes here...
20 |
21 | ```
22 |
23 | **Heads up!** Popover elements needs to get `display: block` styling to have their position
24 | calculated correctly and assure proper display.
25 |
26 | Popovers's content goes into the `div.popover-content` element while its title to the `div.popover-title` element.
27 | There is one more, important CSS classes at play here:
28 | one of `top`, `bottom`, `left`, `right` - needs to be added to `div.popover` to indicate positioning.
29 |
30 |
31 | Popovers, after being created are inserted after the host element in the DOM tree.
32 | By default popovers are shown / hidden in response to the DOM click events.
33 |
34 | See index.html which is already wired to use the component you are about to write.
35 |
--------------------------------------------------------------------------------
/src/03_attribute_directives/exercise/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - using $render()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
--------------------------------------------------------------------------------
/src/04_inter_directive_communication/exercise/solution/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Custom widgets
5 |
6 |
7 |
8 |
9 |
12 |
13 |
14 |
15 |
16 |
17 | Raw denim you probably haven't heard of them jean shorts Austin. Nesciunt tofu stumptown aliqua, retro synth
18 | master cleanse. Mustache cliche tempor, williamsburg carles vegan helvetica. Reprehenderit butcher retro
19 | keffiyeh dreamcatcher synth. Cosby sweater eu banh mi, qui irure terry richardson ex squid. Aliquip placeat
20 | salvia cillum iphone. Seitan aliquip quis cardigan american apparel, butcher voluptate nisi qui.
21 |
22 |
23 | Food truck fixie locavore, accusamus mcsweeney's marfa nulla single-origin coffee squid. Exercitation +1 labore
24 | velit, blog sartorial PBR leggings next level wes anderson artisan four loko farm-to-table craft beer twee. Qui
25 | photo booth letterpress, commodo enim craft beer mlkshk aliquip jean shorts ullamco ad vinyl cillum PBR. Homo
26 | nostrud organic, assumenda labore aesthetic magna delectus mollit. Keytar helvetica VHS salvia yr, vero magna
27 | velit sapiente labore stumptown. Vegan fanny pack odio cillum wes anderson 8-bit, sustainable jean shorts beard
28 | ut DIY ethical culpa terry richardson biodiesel. Art party scenester stumptown, tumblr butcher vero sint qui
29 | sapiente accusamus tattooed echo park.
30 |
31 |
32 | Etsy mixtape wayfarers, ethical wes anderson tofu before they sold out mcsweeney's organic lomo retro fanny pack
33 | lo-fi farm-to-table readymade. Messenger bag gentrify pitchfork tattooed craft beer, iphone skateboard locavore
34 | carles etsy salvia banksy hoodie helvetica. DIY synth PBR banksy irony. Leggings gentrify squid 8-bit cred
35 | pitchfork. Williamsburg banh mi whatever gluten-free, carles pitchfork biodiesel fixie etsy retro mlkshk vice
36 | blog. Scenester cred you probably haven't heard of them, vinyl craft beer blog stumptown. Pitchfork sustainable
37 | tofu synth chambray yr.
38 |
39 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 |
3 | A demo consist of 2 time fields (hours and minutes) that can be used to build
4 | time-picker widgets.
5 |
6 | ## Covered topics
7 |
8 | * Role of parsing and formatting pipelines:
9 | * parse: ui->model
10 | * format: model->ui
11 | * validation
12 | * Patterns:
13 | * failed parsing binds `undefined` to the model
14 | * failed formatting should result in `undefined` being returned
15 | * Testing:
16 | * DOM-based testing requires a bit of gymnastic to trigger input changes (different on different browsers)
17 | * it is good to test parsing / formatting functions in isolation (see minutesfield.spec.js)
18 |
19 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/demo/hourfield.spec.js:
--------------------------------------------------------------------------------
1 | describe('hour field', function () {
2 |
3 | var $scope, $compile;
4 | var $sniffer;
5 |
6 | beforeEach(module('bs.timefields'));
7 | beforeEach(inject(function (_$rootScope_, _$compile_, _$sniffer_) {
8 | $scope = _$rootScope_;
9 | $compile = _$compile_;
10 | $sniffer = _$sniffer_;
11 | }));
12 |
13 | function compileElement(elementString, scope) {
14 | var element = $compile(elementString)(scope);
15 | scope.$digest();
16 | return element;
17 | }
18 |
19 | function changeInputValueTo(element, value) {
20 | var inputEl = element.find('input');
21 | inputEl.val(value);
22 | inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
23 | $scope.$digest();
24 | }
25 |
26 | describe('format', function () {
27 |
28 | it('should format a valid hour', function () {
29 | ($scope.model = new Date()).setHours(15);
30 | var elm = compileElement('', $scope);
31 |
32 | expect(elm.find('input').val()).toEqual('15');
33 | expect($scope.f.i.$valid).toBeTruthy();
34 | });
35 |
36 | it('should leave an input field blank and mark a field as invalid for invalid hour', function () {
37 | var elm = compileElement('', $scope);
38 |
39 | expect(elm.find('input').val()).toEqual('');
40 | expect($scope.f.i.$invalid).toBeTruthy();
41 | });
42 |
43 | });
44 |
45 | describe('parsing', function () {
46 |
47 | it('should correctly parse hour in the 24 hour format', function () {
48 | var elm = compileElement('', $scope);
49 | changeInputValueTo(elm, '20');
50 |
51 | expect($scope.model.getHours()).toEqual(20);
52 | expect($scope.f.i.$valid).toBeTruthy();
53 | });
54 |
55 | it('should not change hour value and mark a field as invalid if parsing fails', function () {
56 |
57 | $scope.model = new Date();
58 | var elm = compileElement('', $scope);
59 | changeInputValueTo(elm, '40');
60 |
61 | expect($scope.model.getHours()).toEqual($scope.model.getHours());
62 | expect($scope.f.i.$invalid).toBeTruthy();
63 | });
64 |
65 | });
66 | });
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - parsing and formatting
5 |
6 |
7 |
8 |
9 |
15 |
16 |
17 |
18 |
19 |
20 | {{ myDate | date:'medium'}}
21 |
22 |
23 |
31 |
32 |
33 |
34 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/demo/minutesfield.spec.js:
--------------------------------------------------------------------------------
1 | describe('minutes field', function () {
2 |
3 | var $scope, ngModelCtrl;
4 |
5 | beforeEach(module('bs.timefields'));
6 | beforeEach(inject(function (_$rootScope_) {
7 | $scope = _$rootScope_;
8 | ngModelCtrl = {
9 | $setValidity: function(key, isValid) {
10 | ngModelCtrl[key] = isValid;
11 | }
12 | };
13 | }));
14 |
15 | describe('parsing', function () {
16 |
17 | var parser;
18 |
19 | beforeEach(inject(function (minutesParserFactory) {
20 | parser = minutesParserFactory(ngModelCtrl, 'key');
21 | }));
22 |
23 | it('should parse valid minutes and set validation key accordingly', function () {
24 | expect(parser('5').getMinutes()).toEqual(5);
25 | expect(ngModelCtrl.key).toBeTruthy();
26 | });
27 |
28 | it('invalid minutes should not change date values and set validation key accordingly', function () {
29 | ngModelCtrl.$modelValue = new Date(2*60*1000);
30 | expect(parser('foo').getMinutes()).toEqual(2);
31 | expect(ngModelCtrl.key).toBeFalsy();
32 | });
33 | });
34 |
35 | describe('formatting', function () {
36 |
37 | var formatter;
38 |
39 | beforeEach(inject(function (minutesFormatterFactory) {
40 | formatter = minutesFormatterFactory(ngModelCtrl, 'key');
41 | }));
42 |
43 | it('should format minutes and set validity of a valid date', function () {
44 | expect(formatter(new Date(2*60*1000))).toEqual(2);
45 | expect(ngModelCtrl.key).toBeTruthy();
46 | });
47 |
48 | it('should return undefined for non-model dates and mark field as invalid', function () {
49 | expect(formatter('not a date')).toBeUndefined();
50 | expect(ngModelCtrl.key).toBeFalsy();
51 | });
52 | });
53 |
54 | });
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/demo/timefields.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.timefields', [])
2 |
3 | .directive('bsHourfield', function () {
4 | return {
5 | require: 'ngModel',
6 | link: function (scope, element, attrs, ngModelCtrl) {
7 |
8 | ngModelCtrl.$parsers.push(function (viewValue) {
9 |
10 | var newDate = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
11 |
12 | //convert string input into hours
13 | var hours = parseInt(viewValue);
14 | var isValidHour = hours >= 0 && hours < 24;
15 |
16 | //toggle validity
17 | ngModelCtrl.$setValidity('hourfield', isValidHour);
18 |
19 | //return model value
20 | if (isValidHour) {
21 | newDate.setHours(hours);
22 | }
23 |
24 | return newDate;
25 | });
26 |
27 | ngModelCtrl.$formatters.push(function (modelValue) {
28 |
29 | var isModelADate = angular.isDate(modelValue);
30 | ngModelCtrl.$setValidity('hourfield', isModelADate);
31 |
32 | return isModelADate ? modelValue.getHours() : undefined;
33 | });
34 | }
35 | };
36 | })
37 |
38 |
39 | /**
40 | * Let's have a look at an alternative way of defining parsers / formatters. What we are trying
41 | * to do here is to decouple parsing / formatting functions from any DOM manipulation
42 | * in order to ease unit-testing.
43 | */
44 | .directive('bsMinutefield', function (minutesParserFactory, minutesFormatterFactory) {
45 | return {
46 | require: 'ngModel',
47 | link: function (scope, element, attrs, ngModelCtrl) {
48 | ngModelCtrl.$parsers.push(minutesParserFactory(ngModelCtrl, 'minutefield'));
49 | ngModelCtrl.$formatters.push(minutesFormatterFactory(ngModelCtrl, 'minutefield'));
50 | }
51 | };
52 | })
53 |
54 | .factory('minutesFormatterFactory', function () {
55 | return function(ngModelCtrl, validationKey) {
56 | return function minutesFormatter(modelValue) {
57 | var isModelADate = angular.isDate(modelValue);
58 | ngModelCtrl.$setValidity(validationKey, isModelADate);
59 |
60 | return isModelADate ? modelValue.getMinutes() : undefined;
61 | };
62 | };
63 | })
64 |
65 | .factory('minutesParserFactory', function () {
66 | return function(ngModelCtrl, validationKey) {
67 | return function minutesParser(viewValue) {
68 | var newDate = ngModelCtrl.$modelValue ? new Date(ngModelCtrl.$modelValue) : new Date();
69 |
70 | //convert string input into minutes
71 | var minutes = parseInt(viewValue);
72 | var isValidMinute = minutes >= 0 && minutes < 60;
73 |
74 | //toggle validity
75 | ngModelCtrl.$setValidity(validationKey, isValidMinute);
76 |
77 | //return model value
78 | if (isValidMinute) {
79 | newDate.setMinutes(minutes);
80 | }
81 |
82 | return newDate;
83 | };
84 | };
85 | });
86 |
87 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/README.md:
--------------------------------------------------------------------------------
1 | ## Exercise
2 |
3 | Create a date field that parses and formats a date according to a specified format.
4 | A date field should work with model of type `Date` and a desired format should be specified as `String`.
5 | Example usage:
6 |
7 | ```html
8 |
9 | ```
10 |
11 | ### Use moment.js library for data parsing and formatting
12 |
13 | While doing the exercise you can use the [moment.js](http://momentjs.com/) library. Some hints:
14 | * parsing date with moment.js:
15 | * parse string to a moment: `var parsedMoment = moment(viewValue, dateFormat);`
16 | * check parsing status: `parsedMoment.isValid()`
17 | * get date object from a moment: `parsedMoment.toDate()`
18 | * formatting:
19 | * `moment(modelValue).format(dateFormat)`
20 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/datefield.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.datefield', [])
2 | .directive('bsDatefield', function () {
3 | return {
4 | require: 'ngModel',
5 | link: function (scope, element, attrs, ngModelCtrl) {
6 |
7 |
8 | }
9 | };
10 | });
11 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/datefield.spec.js:
--------------------------------------------------------------------------------
1 | xdescribe('datefield', function () {
2 |
3 | var $scope, $compile;
4 | var $sniffer;
5 |
6 | beforeEach(module('bs.datefield'));
7 | beforeEach(inject(function (_$rootScope_, _$compile_, _$sniffer_) {
8 | $scope = _$rootScope_;
9 | $compile = _$compile_;
10 | $sniffer = _$sniffer_;
11 | }));
12 |
13 | function compileElement(elementString, scope) {
14 | var element = $compile(elementString)(scope);
15 | scope.$digest();
16 | return element;
17 | }
18 |
19 | function changeInputValueTo(element, value) {
20 | var inputEl = element.find('input');
21 | inputEl.val(value);
22 | inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
23 | $scope.$digest();
24 | }
25 |
26 | describe('format', function () {
27 |
28 | it('should format a valid date with a default format (YYYY/MM/DD)', function () {
29 | $scope.model = new Date(0);
30 | var elm = compileElement('', $scope);
31 |
32 | expect(elm.find('input').val()).toEqual('1970/01/01');
33 | expect($scope.f.i.$valid).toBeTruthy();
34 | });
35 |
36 | it('should format a valid date with a specified format (YYYY-MM-DD)', function () {
37 | $scope.model = new Date(0);
38 | var elm = compileElement('', $scope);
39 |
40 | expect(elm.find('input').val()).toEqual('1970-01-01');
41 | expect($scope.f.i.$valid).toBeTruthy();
42 | });
43 |
44 | it('should leave an input field blank and mark a field as invalid for invalid date', function () {
45 | $scope.model = 'invalid';
46 | var elm = compileElement('', $scope);
47 |
48 | expect(elm.find('input').val()).toEqual('');
49 | expect($scope.f.i.$invalid).toBeTruthy();
50 | });
51 |
52 | });
53 |
54 | describe('parse', function () {
55 |
56 | it('should correctly parse date in the default format', function () {
57 | var elm = compileElement('', $scope);
58 | changeInputValueTo(elm, '2013/11/02');
59 |
60 | expect($scope.model.getFullYear()).toEqual(2013);
61 | expect($scope.model.getMonth()).toEqual(10);
62 | expect($scope.model.getDate()).toEqual(2);
63 | expect($scope.f.i.$valid).toBeTruthy();
64 | });
65 |
66 |
67 | it('should correctly parse date in the specified format', function () {
68 | var elm = compileElement('', $scope);
69 | changeInputValueTo(elm, '2013-11-02');
70 |
71 | expect($scope.model.getFullYear()).toEqual(2013);
72 | expect($scope.model.getMonth()).toEqual(10);
73 | expect($scope.model.getDate()).toEqual(2);
74 | expect($scope.f.i.$valid).toBeTruthy();
75 | });
76 |
77 | it('should bind undefined to the model and mark a field as invalid if parsing fails', function () {
78 |
79 | var elm = compileElement('', $scope);
80 | changeInputValueTo(elm, 'gibberish');
81 |
82 | expect($scope.model).toBeUndefined();
83 | expect($scope.f.i.$invalid).toBeTruthy();
84 | });
85 |
86 | });
87 |
88 | });
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - parsing and formatting
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 | {{ myDate | date:'medium'}}
22 |
23 |
24 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/solution/datefield.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.datefield', [])
2 | .directive('bsDatefield', function () {
3 | return {
4 | require: 'ngModel',
5 | link: function (scope, element, attrs, ngModelCtrl) {
6 |
7 | var dateFormat = attrs.bsDatefield || 'YYYY/MM/DD';
8 |
9 | ngModelCtrl.$parsers.push(function (viewValue) {
10 |
11 | //convert string input into moment data model
12 | var parsedMoment = moment(viewValue, dateFormat);
13 |
14 | //toggle validity
15 | ngModelCtrl.$setValidity('datefield', parsedMoment.isValid());
16 |
17 | //return model value
18 | return parsedMoment.isValid() ? parsedMoment.toDate() : undefined;
19 | });
20 |
21 | ngModelCtrl.$formatters.push(function (modelValue) {
22 |
23 | var isModelADate = angular.isDate(modelValue);
24 | ngModelCtrl.$setValidity('datefield', isModelADate);
25 |
26 | return isModelADate ? moment(modelValue).format(dateFormat) : undefined;
27 | });
28 | }
29 | };
30 | });
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/solution/datefield.spec.js:
--------------------------------------------------------------------------------
1 | describe('datefield', function () {
2 |
3 | var $scope, $compile;
4 | var $sniffer;
5 |
6 | beforeEach(module('bs.datefield'));
7 | beforeEach(inject(function (_$rootScope_, _$compile_, _$sniffer_) {
8 | $scope = _$rootScope_;
9 | $compile = _$compile_;
10 | $sniffer = _$sniffer_;
11 | }));
12 |
13 | function compileElement(elementString, scope) {
14 | var element = $compile(elementString)(scope);
15 | scope.$digest();
16 | return element;
17 | }
18 |
19 | function changeInputValueTo(element, value) {
20 | var inputEl = element.find('input');
21 | inputEl.val(value);
22 | inputEl.trigger($sniffer.hasEvent('input') ? 'input' : 'change');
23 | $scope.$digest();
24 | }
25 |
26 | describe('format', function () {
27 |
28 | it('should format a valid date with a default format (YYYY/MM/DD)', function () {
29 | $scope.model = new Date(0);
30 | var elm = compileElement('', $scope);
31 |
32 | expect(elm.find('input').val()).toEqual('1970/01/01');
33 | expect($scope.f.i.$valid).toBeTruthy();
34 | });
35 |
36 | it('should format a valid date with a specified format (YYYY-MM-DD)', function () {
37 | $scope.model = new Date(0);
38 | var elm = compileElement('', $scope);
39 |
40 | expect(elm.find('input').val()).toEqual('1970-01-01');
41 | expect($scope.f.i.$valid).toBeTruthy();
42 | });
43 |
44 | it('should leave an input field blank and mark a field as invalid for invalid date', function () {
45 | $scope.model = 'invalid';
46 | var elm = compileElement('', $scope);
47 |
48 | expect(elm.find('input').val()).toEqual('');
49 | expect($scope.f.i.$invalid).toBeTruthy();
50 | });
51 |
52 | });
53 |
54 | describe('parse', function () {
55 |
56 | it('should correctly parse date in the default format', function () {
57 | var elm = compileElement('', $scope);
58 | changeInputValueTo(elm, '2013/11/02');
59 |
60 | expect($scope.model.getFullYear()).toEqual(2013);
61 | expect($scope.model.getMonth()).toEqual(10);
62 | expect($scope.model.getDate()).toEqual(2);
63 | expect($scope.f.i.$valid).toBeTruthy();
64 | });
65 |
66 |
67 | it('should correctly parse date in the specified format', function () {
68 | var elm = compileElement('', $scope);
69 | changeInputValueTo(elm, '2013-11-02');
70 |
71 | expect($scope.model.getFullYear()).toEqual(2013);
72 | expect($scope.model.getMonth()).toEqual(10);
73 | expect($scope.model.getDate()).toEqual(2);
74 | expect($scope.f.i.$valid).toBeTruthy();
75 | });
76 |
77 | it('should bind undefined to the model and mark a field as invalid if parsing fails', function () {
78 |
79 | var elm = compileElement('', $scope);
80 | changeInputValueTo(elm, 'gibberish');
81 |
82 | expect($scope.model).toBeUndefined();
83 | expect($scope.f.i.$invalid).toBeTruthy();
84 | });
85 |
86 | });
87 |
88 | });
--------------------------------------------------------------------------------
/src/05_ngmodelctrl_parse_format/exercise/solution/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - parsing and formatting
5 |
6 |
7 |
8 |
9 |
10 |
16 |
17 |
18 |
19 |
20 |
21 | {{ myDate | date:'medium'}}
22 |
23 |
24 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/06_ngmodelctrl_buttons/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 |
3 | Create checkbox buttons where each button in a group can be clicked to toggle model values.
4 | Multiple buttons can be checked in a given group.
5 |
6 | ## Covered topics
7 |
8 | * Requiring other directive's mandatory controller (on the same element)
9 | * Understanding and plugging into the `NgModelController.$render()` infrastructure:
10 | * `NgModelController.$render()` is invoked every time model changes lead to `NgModelController.$viewValue` changes
11 | * default implementation of the `$render` method is empty, custom implementation should update DOM
12 | * no need to observe model, it is already observed in `NgModelController`
13 | * Understanding and using `NgModelController.$setViewValue()` to propagate control state from the DOM to the model
14 | * Using `Scope.$eval()` to get just-in-time value of an attributes' expression (no need to use `Scope.$watch`)
15 | * No need to create an isolated scope as there is no model that is internal to this directive
16 |
17 | ## Bootstrap CSS
18 |
19 | Bootstrap uses the following HTML structure to render a group of buttons:
20 |
21 | ```html
22 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/src/06_ngmodelctrl_buttons/exercise/README.md:
--------------------------------------------------------------------------------
1 | ## Exercise
2 |
3 | Based on the buttons-checkbox demo create a similar directive for radio buttons. Such buttons should
4 | work in a way that only one button in a given group (bound to the same model variable) is checked at
5 | any given time. The example usage should look like:
6 |
7 | ```html
8 |
9 |
10 |
11 |
12 |
13 | ```
14 |
15 | ## Bootstrap CSS
16 |
17 | Bootstrap uses the following HTML structure to render a group of buttons:
18 |
19 | ```html
20 |
21 |
22 |
23 |
24 |
25 | ```
26 |
27 | Notable CSS classes:
28 | * `btn` - default styling of Bootstrap buttons
29 | * `active` - added to a button element to mark it as "checked"
--------------------------------------------------------------------------------
/src/06_ngmodelctrl_buttons/exercise/buttons-radio.js:
--------------------------------------------------------------------------------
1 | angular.module('bs.buttons-radio', [])
2 |
3 | .directive('bsBtnRadio', function () {
4 |
5 | return {
6 | require: 'ngModel',
7 | link: function (scope, element, attrs, ngModelCtrl) {
8 |
9 |
10 | }
11 | };
12 | });
13 |
--------------------------------------------------------------------------------
/src/06_ngmodelctrl_buttons/exercise/buttons-radio.spec.js:
--------------------------------------------------------------------------------
1 | xdescribe('buttons', function () {
2 |
3 | var $scope, $compile;
4 |
5 | beforeEach(module('bs.buttons-radio'));
6 | beforeEach(inject(function (_$rootScope_, _$compile_) {
7 | $scope = _$rootScope_;
8 | $compile = _$compile_;
9 | }));
10 |
11 | var compileButtons = function (markup, scope) {
12 | var el = $compile('
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/07_manual_compilation/demo/README.md:
--------------------------------------------------------------------------------
1 | ## Demo
2 |
3 | This demo builds on top of the previously seen tooltip directive. This time the
4 | tooltip directive gets extended in the way that its content can contain HTML
5 | markup as well as other AngularJS directives! A template for the content is
6 | fetched using XHR request.
7 |
8 | ## Covered topics
9 |
10 | * fetching templates over $http (using $templateCache)
11 | * manual compilation with $compile
12 | * tests: filling in $templateCache in tests to avoid mocking $http
13 |
14 | ## Bootstrap CSS
15 |
16 | Bootstrap 3 uses the following markup to create tooltip elements:
17 |
18 | ```html
19 |
20 |
I'm tooltip's content
21 |
22 |
23 | ```
24 |
25 | Tooltips, after being created are inserted after the host element in the DOM tree.
26 | Tooltip's text goes into the `div.tooltip-inner` element.
27 |
28 | There are 2 additional important CSS classes at play as well:
29 | * - one of `top`, `bottom`, `left`, `right` - needs to be added to `div.tooltip` to indicate positioning
30 | * - `in` - to actually show a tooltip
31 |
32 | Tooltip can be seen in action on Bootstrap's [demo page](http://getbootstrap.com/javascript/#tooltips)
--------------------------------------------------------------------------------
/src/07_manual_compilation/demo/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - using $render()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
17 |
18 |
19 |
20 |
21 |
24 |
25 |
';
9 |
10 | return {
11 |
12 | compile: function compileFunction(tElement, tAttrs) {
13 |
14 | var placement = tAttrs.bsTooltipPlacement || 'top';
15 | var tooltipTplEl = angular.element(tooltipTpl);
16 | tooltipTplEl.addClass(placement);
17 |
18 | return function linkingFunction(scope, iElement, iAttrs) {
19 |
20 | //fetch a template with content over $http, making sure that it is
21 | //retrieved only once (note usage of $templateCache)
22 | $http.get(iAttrs.bsTooltipTpl, {
23 | cache: $templateCache
24 | }).then(function (response) {
25 |
26 | var tooltipTemplateElement = tooltipTplEl.clone();
27 | tooltipTemplateElement.find('div.tooltip-inner').html(response.data.trim());
28 |
29 | var tooltipLinker = $compile(tooltipTemplateElement);
30 | var tooltipScope;
31 | var tooltipInstanceEl;
32 |
33 | //register DOM handlers only when a template is fetched and ready to be used
34 | iElement.on('mouseenter', function () {
35 |
36 | tooltipScope = scope.$new();
37 | scope.$apply(function(){
38 | tooltipInstanceEl = tooltipLinker(tooltipScope);
39 | });
40 |
41 | //attach tooltip to the DOM to get its size (needed to calculate positioning)
42 | iElement.after(tooltipInstanceEl);
43 |
44 | //calculate position
45 | var ttipPosition = calculatePosition(iElement, tooltipInstanceEl, placement);
46 | tooltipInstanceEl.css(ttipPosition);
47 | //finally show the tooltip
48 | tooltipInstanceEl.addClass('in');
49 | });
50 |
51 | iElement.on('mouseleave', function () {
52 | tooltipScope.$destroy();
53 | tooltipInstanceEl.remove();
54 | });
55 | });
56 | };
57 | }
58 | };
59 | });
--------------------------------------------------------------------------------
/src/07_manual_compilation/demo/tooltipTpl.spec.js:
--------------------------------------------------------------------------------
1 | describe('tooltipTpl', function () {
2 |
3 | var $scope, $compile;
4 | var $templateCache;
5 |
6 | beforeEach(module('bs.tooltipTpl'));
7 | beforeEach(inject(function ($rootScope, _$compile_, _$templateCache_) {
8 | $scope = $rootScope;
9 | $compile = _$compile_;
10 | $templateCache = _$templateCache_;
11 | }));
12 |
13 | function compileElement(elementString, scope) {
14 | var element = $compile(elementString)(scope);
15 | scope.$digest();
16 | return element;
17 | }
18 |
19 | it('should show and hide tooltip on mouse enter / leave', function () {
20 | $templateCache.put('content.html', 'some content');
21 | var elm = compileElement('', $scope);
22 |
23 | elm.find('button').mouseenter();
24 | expect(elm.find('.tooltip').length).toEqual(1);
25 |
26 | elm.find('button').mouseleave();
27 | expect(elm.find('.tooltip').length).toEqual(0);
28 | });
29 |
30 | it('should allow HTML and directives in content templates', function () {
31 | $scope.content = 'foo';
32 | $templateCache.put('content.html', '');
33 |
34 | var elm = compileElement('', $scope);
35 |
36 | elm.find('button').mouseenter();
37 | var contentEl = elm.find('div.tooltip-inner>span>i');
38 |
39 | expect(contentEl.text()).toEqual('foo');
40 |
41 | $scope.$apply(function(){
42 | $scope.content = 'bar';
43 | });
44 | expect(contentEl.text()).toEqual('bar');
45 | });
46 | });
--------------------------------------------------------------------------------
/src/07_manual_compilation/exercise/README.md:
--------------------------------------------------------------------------------
1 | ## Exercise
2 |
3 | Build on top of the previously seen popover directive and extended in the way
4 | that its content can contain HTML markup as well as other AngularJS directives.
5 | A template for the content is to be fetched using XHR request.
6 |
7 | ## Bootstrap CSS
8 |
9 | Bootstrap 3 uses the following markup to create popover elements:
10 |
11 | ```html
12 |
13 |
14 |
I'm a title!
15 |
Content goes here...
16 |
17 | ```
18 |
19 | Popovers, after being created are inserted after the host element in the DOM tree.
20 |
21 | Popovers's content goes into the `div.popover-content` element while its title to the `div.popover-title` element.
22 | There is one more, important CSS classes at play here:
23 | one of `top`, `bottom`, `left`, `right` - needs to be added to `div.popover` to indicate positioning.
24 | Additionally the popover elements needs to get `display: block` styling to have its position
25 | calculated and be displayed properly.
26 |
27 | By default popovers are shown / hidden in response to the DOM click events.
28 |
29 | Popover can be seen in action on Bootstrap's [demo page](http://getbootstrap.com/javascript/#popovers)
--------------------------------------------------------------------------------
/src/07_manual_compilation/exercise/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Integration with NgModelController - using $render()
5 |
6 |
7 |
8 |
9 |
10 |
11 |
18 |
19 |
20 |
21 |
22 |
25 |
26 |