html javascript 混杂在一起,开起来是一个大杂烩
什么是好的代码,应当把你的应用解藕成一系列相互平等且独立的页面
使用类、继承、对象和设计模式
MVC:数据(模型) 展示层(视图) 用户交互层(控制器)
用来存放所有的数据对象
模型不必知晓视图和控制器的细节,只需要包行数据及直接和这些数据相关的逻辑
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | // model.js // 用户字段 var User = Backbone.Model.extend({ defaults: { identity: UserType.SelfEmployed, canSpouseGuarantee: UserMap.CanSpouseGuarantee.Canot, socialSecurityFundmonth: -1, }, isCompany: function(){ var identity = this.get('identity'); return identity != UserType.WageEarner && identity != UserType.Other; } }); // 记录页面信息 var PageInfo = Backbone.Model.extend({ defaults: { permission: [ UserType.BusinessOwner, UserType.SelfEmployed, UserType.WageEarner, UserType.Other ], current: false, // 是否当前页 showInNav: true, // 时候要显示在侧边栏上,针对 Index forInput: true, // 是否要显示出来以便用户填写 complete: 0, // 显示填写百分比 completeForCompany: 0, // 针对企业显示的百分比 validateKeys: [], // 必须填写的字段,需要验证的字段,如有特殊情况,重写 isAllFilled validateDepends: [] // 在一定条件下必须填写的字段 参考 UserInfo.js }, initialize: function( properties ){ _.extend( this, properties ); } } |
视图层是呈现给用户的,用户与之产生交互。视图不必知晓模型和控制器中的细节
Html 页面, html 模板
1 2 3 4 | <script type="text/template" id="navTemplate"> <i></i> <span>${name}</span> </script> |
控制器是模型和视图之间的纽带。控制器从视图获得事件和输入,对它们进行处理, 并相应的更新视。当页面加载时,控制器会给视图添加事件监听。
不使用类库和框架也能实现视图
1 2 3 | $('').click(function(){ // any code here }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | // views.js (function(){ var models = $.app.models; var PageInfos = models.PageInfos; // 每个页面的原型 var PageView = Backbone.View.extend({ initialize: function() { _.bindAll(this, 'display'); this.setElement( this.model.get('el') ); this.model.set({view: this}); // 如果是当前页,则显示出来 this.model.bind('change:current', this.display); this.display(); if(this.init) this.init(); }, show: function(){ this.beforeEnter(); this.$el.show(); }, hide: function(){ this.$el.hide(); this.afterLeave(); }, // 在显示页面之前,需要如何,比如 slogan 隐藏侧边栏 beforeEnter: function(){ }, // 在离开页面之后需要如何 afterLeave: function(){}, display: function(model, current) { if( current ){ this.show(); } else { this.hide(); } }, // 因为要大量用到, 所以抽出来一个方法 // 将页面显示,绑定到用户变量 bindWithUser: function( user, field, event, selector, params){ var self = this; // 用户点击的时候, 回调函数 params = params || {}; var changeCallback = params.changeCallback; // 当同步的时候, 回调函数 var syncCallback = params.syncCallback; var change = function(e){ var $target = $(e.currentTarget); var type = $target.data('type'); user.set(field, type); // console.log(changeCallback); if(changeCallback) changeCallback(); }; var sync = function(){ self.$(selector).removeClass('cur'); self.$(selector + '[data-type=' + user.get(field) + ']').addClass('cur'); if(syncCallback) syncCallback(); }; sync(); self.listenTo(user, 'change:' + field, sync); this.$el.on(event, selector, change); } }); // 侧边栏控制器 var NavItem = Backbone.View.extend({ tagName: 'li', // TODO 点击实现 events: { "click": "switchTo" }, initialize: function(){ _.bindAll(this, 'toggleCurrent'); this.template = $("#navTemplate").template(); this.listenTo(this.model, 'change:current', this.toggleCurrent); this.listenTo(this.model, 'change:forInput', this.toggleVisible); }, // 切换到当前页面 switchTo: function(){ // appView 监听事件 Backbone.trigger("switch", this.model); }, render: function () { var element = $.tmpl(this.template, this.model.toJSON()); this.$el.html(element); this.toggleCurrent(); this.toggleVisible(); return this; }, toggleCurrent: function(){ if(this.model.get('current')) this.$el.addClass('cur'); else this.$el.removeClass('cur'); }, toggleVisible: function(){ if(this.model.get('forInput')) this.$el.show(); else this.$el.hide(); } }); // 主控制器 var AppView = Backbone.View.extend({ events: { 'click footer.next a': "gotoNext" }, initialize: function(data){ _.bindAll(this, 'syncForView', 'pageSwitch'); this.$nav = this.$('nav ul'); this.$navIndicator = this.$('nav .cur-step'); this.user = data.user; this.cur = null; // 记录当前的 page this.addNav(); this.gotoNext(); this.syncForView(); this.listenTo(Backbone, 'switch', this.pageSwitch); this.listenTo(Backbone, 'goto', this.gotoPage); // 根据用户选择身份来刷新是否要填写的内容 this.listenTo(this.user, 'change:identity', this.syncForView); }, // 根据id导航 gotoPage: function(pageId){ var page = this.model.find( function(m){ return m.get('el') == pageId; }); if(page) this.setCurrent(page); }, // 点击导航栏切换 pageSwitch: function(page){ if(page == this.cur ) return; if( !this.chkIsFilled() ) { $.gritter.add({ // (string | mandatory) the text inside the notification text: '请将字段填充' }); return; } // 获得没有填写完整 切 index 比要切换的page 小的 var isNotFilled = this.model.find(function(m){ return page.get('index') > m.get('index') && m.isAllFilled(this.user); }); if(!isNotFilled) this.setCurrent(page); }, syncForView:function(){ var user = this.user; this.model.each(function(page){ page.refresh(user.get('identity')); }); }, addNav: function(){ this.$nav.html(''); this.model.each( function (page) { if(page.get('showInNav')) { var view = new NavItem({ model: page }); this.$nav.append(view.render().el); } }, this); }, // 检查当前页面是否全部正确填写 chkIsFilled: function(){ if( !this.cur) return true; return this.cur.isAllFilled(this.user); }, // 点击切换到下一个页面 gotoNext: function(){ if( !this.chkIsFilled() ) { $.gritter.add({ text: '请填充字段' }); // return; } this.setCurrent(this.getNextView()); }, // 获取下一个页面 getNextView: function(){ var curIndex = -1; if(this.cur) curIndex = this.cur.get("index"); var avaliables = this.model.filter(function(p) { var pIndex = p.get('index'); return pIndex > curIndex && p.get('forInput');; }); var next = _.min( avaliables , function(m){ return m.get('index'); }); if(next == Infinity) next = this.cur; return next || this.cur; }, // 设置当前选中页面 setCurrent: function(cur){ if(this.cur) { this.cur.set({current: false}); } cur.set({ current: true }); this.cur = cur; this.setIndicator(); }, // 设置百分数 setIndicator: function(){ var self = this; var p = this.model.reduce(function(percent, page){ if(page.isAllFilled(self.user) && !page.get("showInNav") && page.get("forInput")) { var tmp = -1; if( self.user.isCompany() ) { tmp = page.get("complete"); } else { tmp = page.get("completeForCompany"); } return percent > tmp ? percent : tmp; } else { return percent; } }, 0); this.$navIndicator.html( p + '%'); } }); $.app.views.AppView = AppView; $.app.views.PageView = PageView; }).call(this); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 | // 每个页面都有自己的控制器 (function(){ var models = $.app.models; var PageInfo = models.PageInfo; var UserType = models.UserType; var PageView = $.app.views.PageView; var telRegex = /^(0|86|17951)?(13[0-9]|15[012356789]|17[678]|18[0-9]|14[57])[0-9]{8}$/; $.app.pages.BasicInfo = function(user){ var info = new PageInfo({ el: "#partialBasicInfo", name: "基本信息", index: 1, complete: 0, // 如果所有的字段都要填写, 就不用重写 isAllFilled validateKeys: ['identity', 'userName', 'cellPhoneNumber'], // 重写 pageInfo 方法,因为并不是所有字段都要验证 isAllFilled: function(model) { // 调用父类方法 if(this.__proto__.isAllFilled(model)) { var id = model.get('identity'); // 如果是企业主那么 需要填写 share if(id == UserType.BusinessOwner || id == UserType.SelfEmployed) { return !this.isEmpty( model.get('share') ); } return true; } return false; } }); var BasicInfoView = PageView.extend({ init: function() { // 绑定作用域 _.bindAll(this, "syncIdentity", "syncShare"); // 缓存选择器 this.$telError = $('#telError'); this.$nameError = $('#nameError'); this.$nameInput = $("#username"); this.$telInput = $("#tel"); //初始化参数 this.syncIdentity(); this.syncShare(); this.$telInput.val(user.get('cellPhoneNumber')); this.$nameInput.val(user.get('userName')); // 监听变化 this.listenTo(user, 'change:identity', this.syncIdentity); this.listenTo(user, 'change:share', this.syncShare); this.listenTo(user, 'change:userName', function(model, val){ this.$nameInput.val(val); }); this.listenTo(user, 'change:cellPhoneNumber', function(model, val){ this.$telInput.val(val); }); }, events: { "change #tel": "chkTel", "change #username": "chkName", "click .form .identity li": "selectType", "click .form .info a": "selectShare" }, syncIdentity: function(){ var $cur = this.$('.identity li[data-type=' + user.get('identity') + ']'); $('.identity li').removeClass('cur'); $cur.addClass('cur'); }, syncShare: function(){ var $cur = this.$('.identity .info a[data-type=' + user.get('share') + ']'); $('.identity .info a').removeClass('cur'); $cur.addClass('cur'); }, chkTel: function(){ var tel = this.$telInput.val(); if( !telRegex.test(tel) ) { this.$telError.show(); } else { this.$telError.hide(); // 如果正确,就保存起来 user.set('cellPhoneNumber', tel); return true; } if( ret == "" ){ user.set('cellPhoneNumber', ""); } return false; }, chkName: function(){ var username = $.trim(this.$nameInput.val()); if(username=="") { this.showNameError("请输入用户名"); user.set('userName', ""); return false; } else if( /^\d.*$/.test( username ) ){ this.showNameError("用户名不能以数字开头"); return false; } else if(username.length<2 || username.length>15 ){ this.showNameError("合法长度为2-15个字符"); return false; } else { this.showNameError(""); } user.set('userName', username); return true; }, showNameError: function(notify) { this.$nameError.html(notify); if( notify != "" ) this.$nameError.show(); else this.$nameError.hide(); }, selectType: function(e){ var target = $(e.currentTarget); user.set('identity', target.data('type')); }, selectShare: function(e) { var target = $(e.currentTarget); user.set('share', target.data('type')); } }); var page = new BasicInfoView({ model: info }); return info; }; }).call(this); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | // app.js $(function(){ var views = $.app.views; var models = $.app.models; var pages = $.app.pages; var PageInfo = models.PageInfo; var PageInfos = models.PageInfos; var PageView = views.PageView; var AppView = views.AppView; var User = models.User; var app = app || {}; app.user = new User(); app.pages = new PageInfos(); app.pages.add( pages.Slogan(app.user) ); app.pages.add( pages.BasicInfo(app.user)); app.pages.add( pages.CompanyType(app.user)); app.pages.add( pages.IncomeType(app.user)); app.pages.add( pages.WorkingAge(app.user)); app.pages.add( pages.CompanyPosition(app.user)); app.pages.add( pages.CreditStanding(app.user)); app.pages.add( pages.SocialSecurityfund(app.user)); app.pages.add( pages.UserInfo(app.user) ); app.pages.add( pages.HouseInfo(app.user) ); app.pages.add( pages.MonthIncome(app.user) ); app.pages.add( pages.LoansInfo(app.user) ); app.pages.add(pages.CarInfo(app.user)); // TODO app.pages.add( pages.Region(app.user) ); app.pages.add( pages.Review(app.user) ); app.pages.add( pages.CompanyRegion(app.user) ); app.pages.add( pages.Bill(app.user) ); app.pages.add( pages.Complete(app.user) ); app.pages.add( pages.CreditInfo(app.user) ); var s = new AppView({model: app.pages, el: '.container', user: app.user}); }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | @import "susy"; body { @include container(80em); } nav { @include span(25%); } body { @include container; @include show-grid(overlay); } nav { @include span(3 of 12); } main { float: left; width: span(4); margin-left: span(2) + gutter(); margin-right: gutter(); } nav { @include span(3 of 12); } $susy: ( columns: 12, // The number of columns in your grid gutters: 1/4, // The size of a gutter in relation to a single column ); |
These two definitions are interchangeable
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | $susy: ( columns: 12, gutters: 1/4, math: fluid, output: float, gutter-position: inside, ); $shorthand: 12 1/4 fluid float inside; // 12-column grid $grid: 12; // 12-column grid with 1/3 gutter ratio $grid: 12 1/3; // 12-column grid with 60px columns and 10px gutters $grid: 12 (60px 10px); // asymmetrical grid with .25 gutter ratio $grid: (1 2 3 2 1) .25; // globle sets $susy: ( flow: ltr, // rtl | ltr The reading direction of your document. math: fluid, // static | fluid // Susy can produce either relative widths (fluid percentages) or static widths (using given units) output: float, gutter-position: after, container: auto, // <length> | auto container-position: center, // left | center | right | <length> [*2] columns: 4, // <number> | <list> gutters: .25, // 70px/10px column-width: false, global-box-sizing: content-box, last-flow: to, debug: ( image: hide, color: rgba(#66f, .25), output: background, toggle: top right, ), use-custom: ( background-image: true, background-options: false, box-sizing: true, clearfix: false, rem: true, ) ); |
A “layout” in Susy is made up of any combination of settings. Layouts are stored as maps, but can also be written as shorthand.
1 2 3 4 5 6 7 8 9 10 11 12 | // mixin: set a global layout @include layout(12 1/4 inside-static); $map1: 13 static; $map2: (6em 1em) inside; @include layout($map1 $map2); @include with-layout(8 static) { // Temporary 8-column static grid... } // Global settings are restored... |
The global keywords can be used anywhere, and apply to global default settings. The local keywords are specific to each individual use.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | $global-keywords: ( container : auto, math : static fluid, output : isolate float, container-position : left center right, flow : ltr rtl, gutter-position : before after split inside inside-static, debug: ( image : show hide show-columns show-baseline, output : background overlay, ), ); $local-keywords: ( box-sizing : border-box content-box, edge : first alpha last omega, spread : narrow wide wider, gutter-override : no-gutters no-gutter, clear : break nobreak, role : nest, ); // grid: (columns: 4, gutters: 1/4, column-width: 4em); // keywords: (math: fluid, gutter-position: inside-static, flow: rtl); $small: 4 (4em 1em) fluid inside-static rtl; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | // span: 3; // location: 4; // layout: (columns: 12, gutters: .25, math: fluid) $span: 3 at 4 of 12 .25 fluid; // Only $span is required in most cases $span: 30%; // arbitrary width .item { @include span(25%); } // float output (without gutters) .item { float: left; width: 25%; } // grid span .item { @include span(3); } // output (7-column grid with 1/2 gutters after) .item { float: left; width: 40%; margin-right: 5%; } // grid span @include span(last 3); // output (same 7-column grid) .item { float: right; width: 40%; margin-right: 0; } // 10-column grid .outer { @include span(5); .inner { @include span(2 of 5); } } // Grids with inside, inside-static, or split gutters don’t need to worry about the edge cases, but they do have to worry about nesting. // If an element will have grid-aligned children, you should mark it as a nest: .outer { @include span(5 nest); .inner { @include span(2 of 5); } } // grid span .narrow { @include span(2); } .wide { @include span(2 wide); } .wider { @include span(2 wider); } // width output (7 columns, .25 gutters) // (each column is 10%, and each gutter adds 2.5%) .narrow { width: 22.5%; } .wide { width: 25%; } .wider { width: 27.5%; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 | body { @include container(12 center static); } // inside gutters .item { @include gutters(3em inside); } // gutters after, in an explicit (10 1/3) layout context .item { @include gutters(10 1/3 after); } // default context margin-left: gutter(); // nested in a 10-column context margin-left: gutter(10); .item { width: span(2); margin-left: span(3 wide); margin-right: span(1) + 25%; } // the flexible version: @include global-box-sizing(border-box); // the shortcut: @include border-box-sizing; .first { @include first; } .last { @include full; } // depending on the flow direction. .example1 { @include pre(25%); } .example2 { @include push(2 of 7); } .example1 { @include post(25%); } .example2 { @include post(2 of 7); } .example1 { @include pull(25%); } .example2 { @include pull(2 of 7); } // equal pre and post .example1 { @include squish(25%); } // distinct pre and post .example2 { @include squish(1, 3); } .example1 { @include prefix(25%); } .example2 { @include prefix(2 of 7); } .example1 { @include suffix(25%); } .example2 { @include suffix(2 of 7); } // equal pre and post .example1 { @include pad(25%); } // distinct pre and post .example2 { @include pad(1, 3); } // input // Apply negative margins and equal positive padding, so that element // borders and backgrounds “bleed” outside of their containers, without the content be affected. .example1 { @include bleed(1em); } .example2 { @include bleed(1em 2 20px 5% of 8 .25); } // output .example1 { margin: -1em; padding: 1em; } .example2 { margin-top: -1em; padding-top: 1em; margin-right: -22.5%; padding-right: 22.5%; margin-bottom: -20px; padding-bottom: 20px; margin-left: -5%; padding-left: 5%; } // input .example { @include bleed-x(1em 2em); } // output .example { margin-left: -1em; padding-left: 1em; margin-right: -2em; padding-right: 2em; } // input .mixin { @include isolate(25%); } // output .mixin { float: left; margin-left: 25%; margin-right: -100%; } // each img will span 3 of 12 columns, // with 4 images in each row: .gallery img { @include gallery(3 of 12); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 | @include susy-breakpoint(30em, 8) { // nested code uses an 8-column grid, // starting at a 30em min-width breakpoint... .example { @include span(3); } } // min // --- @include susy-media(30em) { /*...*/ } @media (min-width: 30em) { /*...*/ } // min/max pair // ------------ @include susy-media(30em 60em) { /*...*/ } @media (min-width: 30em) and (max-width: 60em) { /*...*/ } // property/value pair // ------------------- @include susy-media(min-height 30em) { /*...*/ } @media (min-height: 30em) { /*...*/ } // map // --- @include susy-media(( min-height: 30em, orientation: landscape, )) { /*...*/ } @media (min-height: 30em) and (orientation: landscape) { /*...*/ } $susy-media: ( min: 20em, max: 80em 60em, string: 'screen and (orientation: landscape)', pair: min-height 40em, map: ( media: screen, max-width: 30em ), ); @include susy-media(min); |
不管有多少人共同参与同一项目,一定要确保每一行代码都像是同一个人编写的。
.css
. 共用 base.css
, 首页index.css
, 其他页面依实际模块需求命名。.js
. 共用 common.css
, 其他依实际模块需求命名.将你的编辑器按照下面的配置进行设置,以避免常见的代码不一致和差异:
UTF-8
utf-8
, 书写时利用IDE实现层次分明的缩进;<head></head>
之间;非特殊情况下javascript文件必须外链至页面底部;jquery-1.7.1.min.js
;
引入插件, 文件名格式为库名称+插件名称, 比如 jQuery.cookie.js
;”data-”
为前缀来添加自定义属性,避免使用”data:”
等其他命名方式;href=”http://www.example.com/”
, 即须在URL地址后面加上/
;style=”…”
,应该尽量使用class或者id来定义新的样式,
然后再对应的CSS文件里面修改;1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 | <!DOCTYPE html> <html> <head> <!-- 通过明确声明字符编码,能够确保浏览器快速并容易的判断页面内容的渲染方式。 --> <meta charset="utf-8"> <title>Page title</title> <meta name="keywords" content="xxxx, xxx, xxxxx" /> <meta name="description" content="xxxxxxxxxxxxxxxxxxxx" /> <!-- 通知ie采用最新模式 --> <meta content="IE=edge,chrome=1" http-equiv="X-UA-Compatible"> <!-- 如果是响应式页面,请加上这句 --> <meta content="width=device-width, initial-scale=1" name="viewport"> <!-- 让 360 等双核浏览器采用 webkit 进行页面渲染 --> <meta content="webkit" name="renderer" > <!-- 缓存头可酌情添加 --> <meta http-equiv="Cache-Control" content="max-age=7200" /> <!-- icon --> <link rel="shortcut icon" href="favicon.ico"> <!-- 根据html5规范,对于引入css和js可以不用指定type属性 --> <!-- External CSS --> <link rel="stylesheet" href="code-guide.css"> <!-- In-document CSS --> <style> </style> </head> <script type="text/javascript"> <!-- Google 统计代码 的位置在离</head>最近的位置 --> </script> <body> <img src="images/company-logo.png" alt="Company"> <h1 class="hello-world">Hello, world!</h1> <!-- 注释约定 --> <!-- @name: Drop Down Menu @description: Style of top bar drop down menu. @author: Andy Huang(andyahung@geekpark.net) --> <div id="header"> <div class="xxx"> <span>HTML行内注释格式</span> </div> </div><!-- #header end--> <!-- JavaScript 要加到html的最后,这样可以提高页面的加载速度 --> <script src="code-guide.js"></script> </body> </html> |
HTML 属性应当按照以下给出的顺序依次排列,确保代码的易读性。
1 2 3 4 5 | <a class="..." id="..." data-modal="toggle" href="#"> Example link </a> <input class="form-control" type="text"> <img src="..." alt="..."> |
class 用于标识高度可复用组件,因此应该排在首位。id 用于标识具体组件,应当谨慎使用(例如,页面内的书签),因此排在第二位。
尽量遵循 HTML 标准和语义,但是不要以牺牲实用性为代价。任何时候都要尽量使用最少的标签并保持最小的复杂度。
编写 HTML 代码时,尽量避免多余的父元素。很多时候,这需要迭代和重构来实现。请看下面的案例:
1 2 3 4 5 6 7 | <!-- Not so great --> <span class="avatar"> <img src="..."> </span> <!-- Better --> <img class="avatar" src="..."> |
通过 JavaScript 生成的标签让内容变得不易查找、编辑,并且降低性能。能避免时尽量避免。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | /* 推荐嵌套层级 */ <ul class="ui-nav"> <li class="ui-nav-item"> some text <ul class="ui-nav-item-child"> <li> some text <ul class="ui-list"> <li class="ui-list-item"> some text</li> </ul ... </ul> </li> <li ... </ul> |
base.css
(里面包括了css reset、常用的css间距,css字体,css大小等)firstName
topBoxList
footerCopyright
top-item
main-box
box-list-item-1
,
尽量使用语义明确的单词命名,避免 left bottom等方位性的词语1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | /* @name: Drop Down Menu @description: Style of top bar drop down menu. @require: reset.css @author: Andy Huang(andyahung@geekpark.net) */ /* CSS名称+冒号+属性 */ .box1 {float:left;} /*建议保留{左侧空格,字体名用\包含*/ .box1,.box2,.box3 {font-family:Courier,'Courier New';} /*避免中文,或使用转义,推荐前者如:*/ .box4 { font-family: "Microsoft YaHei"; font-family:\5fae\8f6f\96c5\9ed1; } /* 推荐嵌套层级 */ .ui-icon-rarr{} .ui-icon-larr{} .ui-title{} .ui-nav .ui-list{} .ui-table .ui-list{} /* 不推荐 */ .ui-icon-rarr{} .ui-icon-larr{} .ui-title{} .ui-list{} .ui-nav{} /* 媒体查询放在尽可能相关规则的附近。不要将他们打包放在一个单一样式文件中或者放在文档底部。*/ .element { ... } .element-avatar { ... } .element-selected { ... } @media (min-width: 480px) { .element { ...} .element-avatar { ... } .element-selected { ... } } |
index.html 全部样式附着于 class="xxx" 注: 此时文件中不包含任何一个 id="xxx"
为上一步书写 CSS style
[至此重构完成]
开始书写js交互文件,使用 ID 和 Class 定位被操作句柄
向 index.html 中需要的地方添加 id="xxx" 及 data-xxx="xxx"
[至此交互效果完成]
不做硬性规定
相关的属性声明应当归为一组,并按照下面的顺序排列:
由于定位(positioning)可以从正常的文档流中移除元素,并且还能覆盖盒模型(box model)相关的样式,因此排在首位。 盒模型排在第二位,因为它决定了组件的尺寸和位置。
其他属性只是影响组件的内部(inside)或者是不影响前两组属性,因此排在后面。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | .declaration-order { /* Positioning */ position: absolute; top: 0; right: 0; bottom: 0; left: 0; z-index: 100; /* Box-model */ display: block; float: right; width: 100px; height: 100px; /* Typography */ font: normal 13px "Helvetica Neue", sans-serif; line-height: 1.5; color: #333; text-align: center; /* Visual */ background-color: #f5f5f5; border: 1px solid #e5e5e5; border-radius: 3px; /* Misc */ opacity: 1; } |
对于只包含一条声明的样式,为了易读性和便于快速编辑,建议将语句放在同一行。对于带有多条声明的样式,还是应当将声明分为多行。
这样做的关键因素是为了错误检测 -- 例如,CSS 校验器指出在 183 行有语法错误。如果是单行单条声明,你就不会忽略这个错误;如果是单行多条声明的话,你就要仔细分析避免漏掉错误了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* Single declarations on one line */
.span1 { width: 60px; }
.span2 { width: 140px; }
.span3 { width: 220px; }
/* Multiple declarations, one per line */
.sprite {
display: inline-block;
width: 16px;
height: 15px;
background-image: url(../img/sprite.png);
}
.icon { background-position: 0 0; }
.icon-home { background-position: 0 -20px; }
.icon-account { background-position: 0 -40px; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | @charset "UTF-8"; html, body, div, span, applet, object, iframe, h1, h2, h3, h4, h5, h6, p, blockquote, pre, a, abbr, acronym, address, big, cite, code, del, dfn, em, img, ins, kbd, q, s, samp, small, strike, strong, sub, sup, tt, var, b, u, i, center, dl, dt, dd, ol, ul, li, fieldset, form, label, legend, table, caption, tbody, tfoot, thead, tr, th, td, article, aside, canvas, details, embed, figure, figcaption, footer, header, hgroup, menu, nav, output, ruby, section, summary, time, mark, audio, video { margin: 0; padding: 0; border: 0; font: inherit; font-size: 100%; vertical-align: baseline; } html { line-height: 1;} ol, ul { list-style: none; } table {border-collapse: collapse; border-spacing: 0;} caption, th, td {text-align: left; font-weight: normal; vertical-align: middle;} q, blockquote { quotes: none; } q:before, q:after, blockquote:before, blockquote:after {content: ""; content: none;} a img { border: none;} article, aside, details, figcaption, figure, footer, header, hgroup, main, menu, nav, section, summary { display: block;} a { color: inherit; text-decoration: inherit; cursor: inherit;} a:active, a:focus { outline: none; } input[type="button"], button, a { font-family: 'Microsoft YaHei', Verdana, Geneva, Arial, Helvetica, SimHei, sans-serif; } body {font-family: 'Microsoft YaHei', Verdana, Geneva, Arial, Helvetica, SimHei, sans-serif;} |
;
$
, 尽量避免全局变量.console.log()
及console.dir()
进行,避免使用弹出框,线上版需要注释所有调试代码1 2 3 4 5 6 7 | // 使用闭包来解决全局变量污染的问题 (function(){ // 对于事件的监听, // 不要这么写 <a onclick=""/> 可以这么写 $("selector").bind("click", function(e){ }); })(); |
1 2 | <a href="#mao"></a> <a name="mao"></a> |
frameset 能够使用的文档声明头: html 4.01 frameset
xhtml 1.0 frameset
target
_blank
_top
_parent
_self
1 2 3 4 5 6 | /*比例可能会失调*/ background-size: 100% 100%; /*图片比例不会变化, 但是一部分会看不见*/ background-size: cover; /*图片比例不会变化, 缩小呈递*/ background-size: cantain; |
1 2 3 4 5 6 7 8 | $('').draggable( { containment: "#xiaoming", axis: "y", drag: function(){ $(this).css('top'); } }); |
1 2 3 4 5 6 7 | xhr.onreadystatechange = function(){ if(xhr.readyState == 4) { if(xhr.state >= 200 && xhr.status < 300 || xhr.status == 304){ xhr.responseXML; } } } |
IE6 bug
1 2 3 4 5 6 7 8 | opacity: 0.6; _filter:alpha(opacity=60); height: 13px; _font-size:0; overflow:hidden; _zoom:1; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $("div:visibile"); $("div:hidden"); $("div:contains('content')"); $("").is("visibile"); $(this).is('#username'); $("").toggle(fn1, fn2); $("").filter(""); $("div[text|=''"]); $("div[text*=''"]); $("div[text^=''"]); $("div[text$=''"]); $("div[:checked]); $("div[:nth-child(2n)]); $("div[:nth-child(2n + 1)]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $("div").replaceWith(); $("div").replace(); $().wrap(); $().wrapAll(); $().after(); $().toggleClass(); A.insertAfter(B); // BA $().detach(); // 所有元素事件不解绑 $().clone(true); // 复制, 但是绑定事件 $('').offset(); // 相对视口 $('').position(); // 相对位置 this.defaultValue; // 获得默认值 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | $(document).ready(fn2); $(window).load(fn); $().bind('click mouseover', fn); $().toggle(fn1, fn2, fn3); function handler() { e.relatedTarget(); e.target(); e.witch(); return false; // e.stopPropagation(); e.preventDefault(); } $().animate({left: "+=50px"}).stop(clearQueue, gotoEnd); $().delay(200); |
1 2 | $(this).triggerHandler('blur'); Array.slice('abcd', -2); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | $().load(src, function(text, status)); $.getScript("*.js"); $.getJSON(, function(data){}); $.ajax({ type: 'GET', url: '', dataType: '', success: function(data){} }); $('#form').serialize(); $(':checkbox, :radio').serializeArray(); // {name, value} $.param({a:2, b:3}); // a=1&b=2&c=3 $.ajaxComplete(cb); $.ajaxError(cb); $.ajaxSend(cb); $.ajaxSuccess(cb); $.ajax({global: false}); // 不触发全局事件 |
1 2 3 4 5 6 7 8 9 10 | option = $.extend({}, options); (function(){ $.fn.extend({ "color": function(value){return this.css('color', value)} }); })(jQuery); // 全局 $.extend({ ltrim: function(text){}; }); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | $('table').delegate('td', 'hover', function()); $('span').data(); // {maxValue: 15, minValue: 5;} // input type='checkbox' checked $().attr('checked'); // '' $().prop('checked'); // true $(elements).on(events, selector, handler); $(elements).off(events, selector, handler); $.Defered $.isNumeric(); $.noop(); $.now(); $.localStorage; |
ID用于标识页面上特定的元素,以及持久的结构性元素,后一次性元素。
类适合标识内容的类型或其他相似的条目。
应该根据 '他们是什么' 来为元素命名,而不应该根据 '他们的外观如何' 来命名. 可以让代码更有意义, 并且避免代码与设计不同步.
外链式
1 2 | <style type="text/css"> </style> |
内部导入, 比链接慢
1 | @import url(/css/layout.css);
|
使用结构良好的单一CSS文件可以显著提高下载速度.
它指定元素如何显示以及如何相互交互.页面上每个元素都被看作一个矩形框,这个框由元素的内容 内边距 边框 和 外边框组成
全局 reset
1 | *{ maring: 0; padding: 0;}
|
css3 的 box-sizing
属性可以定义要使用哪种盒模型.
当两个或者更多个垂直外边距相遇时, 它们将形成一个外边距, 这个外边距的高度等于两个发生叠加的外边距的高度的较大者.
只有普通文档流中的盒子的垂直外边距才会发生外边距叠加. 行内框 浮动框 或绝对定位框不会发生
文本元素会被当作匿名框, 用:first-line
选中
1 2 3 4 | <div> some text <p>some test</p> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 | .clear:after { content: "."; height: 0; visibility: hidden; display: block; clear:both; } /* IE hack */ .clear {display: inline-block;} /*针对ie6*/ * html .clear {height: 1%;} .clear {display: block;} /*另一种方法*/ .clear{overflow:hidden; _zoom:1;} /* or */ .clear{overflow:auto; _height: 1%;} |
需要单独存放一个文件, 使用时用条件注释
1 2 3 4 | div { filter:progid:DXImageTransform.Microsoft.AlphaImageLoader(src='/img/xx.png',sizingMethod='crop'); background: none; } |
或者使用 .htc
文件, 在ie6专用样式表中引用它。
1 2 3 | img, div{ behavior: url(iepngfix.htc); } |
1 2 3 | a:link, a:visited { background: url(/img/button.png) left top no-repeat;} a:hover, a:focus {background-image: url(/img/button-over.png);} a:active {background-image: url(/img/button-active.png);} |
1 2 3 | html { filter: expression(document.execCommand("BackgroundImageCache", false, true)); } |
css 框架试图通过在标记和表现之间建立强耦合来简化css布局。
布局技术的根本是3个基本概念: 定位 浮动 和 外边距操纵
960/1250
1 2 3 | body { font-size: 62.5%; text-align: center;} .wrapper{width: 92em;} /*里面仍然用百分比*/ |
ie6中的width
更像是 min-width
1 | <a href="" rel="prev"></a> |
1 2 3 | ol.pagination a[rel="prev"], ol.pagination a[rel="next"] {border: none;} ol.pagination a[rel="prev"]:before { content: "\00AB";} ol.pagination a[rel="next"]:before { content: "\00BB";} |
双倍边距解决 display: inline;
1 2 3 4 5 6 | <!-- 方式一 --> <lable> email <input name="email" type="text"/> </lable> <!-- 方式二 --> <label for="email"> email <em class="required">(required)</em> </label> <input name="email" type="text"/> |
1 2 3 4 5 6 | input[type='text'], textrea { border-top: 2px solid #999; border-left: 2px solid #999; border-bottom: 1px solid #999; border-right: 1px solid #999; } |
提交按钮
1 2 3 4 5 | <div> <button type="submit"> <img src="/img/xx"/> </button> </div> |
1 | text-overflow: ellipsis; |
input 标签
1 | <input type="text" autocomplete="off" autofocus spellcheck='true' required/> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | box-sizing: content-box|border-box; display: box; box-flex: 1; // 改变元素显示位置 box-ordinal-group: 3; // 排列方向 box-orient: vertical; box-align: start | center | end; // hrizontal | vertical out-line: thin solid red; out-offset: ; |
We need to write end-to-end tests, which open the browser, navigate to a live running version of our web application, and click around using the application as a real-world user would. To accomplish this, we use Protractor.
TODO
Directory Structure
app
The app folder houses all the JavaScript code that you develop. We’ll talk about this
in more detail next.tests
Houses all your unit tests and possibly the end-to-end scenario tests as well.
data Anydata
that is common but not dynamic in your application can be stored here.scripts
Build scripts and other common utility scripts can be stored in this folder.Other files
The package.json
, bower.json
, and other files that don’t really need a directory can
be in the main folder.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 | — app.css — app.js — index.html — components // Reusable common components — datepicker — datepicker-directive.js — datepicker-directive_test.js — authorization — authorization.js — authorization-service.js — authorization-service_test.js — ui-widgets — ui-widgets.js — grid — grid.html — grid-directive.js — grid-directive_test.js — dialog — dialog-service.js — dialog-service_test.js — list — list.html — list.css — list-controller.js — list-controller_test.js — login — login.html — login-controller.js — search — search.html — search.css — search-controller.js — search-controller_test.js — detail — detail.html — detail-controller.js — detail-controller_test.js — admin — create — create.html — create-controller.js — create-controller_test.js — update — update.html — vendors // third-party dependencies go here — underscore — jquery — bootstrap — e2e // end-to-end scenario tests — runner.html — login_scenario.js — list_scenario.js — search_scenario.js — detail_scenario.js — admin — admin_create_scenario.js — admin_update_scenario.js |
Yeoman
, Yeoman is a workflow management tool that automates a lot of the routine,
chore-like tasks that are necessary in any project.ng-boilerplate
and angular-seed
A grunt task that is available for online use is ng-templates
,
which allows you to preload all the HTML templates that you use in
your application instead of making an XHR request for them when it is needed.
But if you have a large number of templates, you can consider preloading the most common templates and views in your application, and let the others load asynchronously as needed.
General
$timeout
service, and the
AngularJS version of setInterval
, which is the $interval
service.$timeout
or $interval
, should remember to
clean it up or cancel it when it is destroyed, to prevent it
from unnecessarily executing in the background.$broadcast
or $emit
events on their own scope, or inject the
$rootScope and fire events on $rootScope
.Batarang
, a Chrome extension to help debug and work with AngularJS projects
Optional Modules: ngCookies
ngSanitieze
ngResource
ngTouch
ngAnimate
Directives are of two major types in AngularJS
The ng-include
directive takes an AngularJS expression
and treats its value as the path to an HTML file.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | <!-- File: chapter11/ng-include/stock.html --> <div class="stock-dash"> Name: <span class="stock-name" ng-bind="stock.name"> </span> Price: <span class="stock-price" ng-bind="stock.price | currency"> </span> Percentage Change: <span class="stock-change" ng-bind="mainCtrl.getChange(stock) + '%'"> </span> </div> <body ng-app="stockMarketApp"> <div ng-controller="MainCtrl as mainCtrl"> <h3>List of Stocks</h3> <div ng-repeat="stock in mainCtrl.stocks"> <div ng-include="mainCtrl.stockTemplate"> </div> </div> </div> <script> // File: chapter11/ng-include/app.js angular.module('stockMarketApp', []) .controller('MainCtrl', [function() { var self = this; self.stocks = [ {name: 'First Stock', price: 100, previous: 220}, {name: 'Second Stock', price: 140, previous: 120}, {name: 'Third Stock', price: 110, previous: 110}, {name: 'Fourth Stock', price: 400, previous: 420} ]; self.stockTemplate = 'stock.html'; self.getChange = function(stock) { return Math.ceil(( (stock.price - stock.previous) / stock.previous) * 100); }; }]); </script> |
We can also do it like this <div ng-include="'views/stock.html'"></div>
Although we changed the name of the variable in the
main index.html
file, the stock.html
file still
expects a variable calledb stock for it to display.
The ng-switch is another directive that allows us to add some functionality to the UI for selectively displaying certain snippets of HTML. It gives us a way of conditionally including HTML snippets by behaving like a switch case directly in the HTML.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <div ng-controller="MainCtrl as mainCtrl"> <h3>Conditional Elements in HTML</h3> <button ng-click="mainCtrl.currentTab = 'tab1'"> Tab 1 </button> <button ng-click="mainCtrl.currentTab = 'tab2'"> Tab 2 </button> <button ng-click="mainCtrl.currentTab = 'tab3'"> Tab 3 </button> <button ng-click="mainCtrl.currentTab = 'something'"> Trigger Default </button> <div ng-switch="mainCtrl.currentTab"> <div ng-switch-when="tab1"> Tab 1 is selected </div> <div ng-switch-when="tab2"> Tab 2 is selected </div> <div ng-switch-when="tab3"> Tab 3 is selected </div> <div ng-switch-default> No known tab selected </div> </div> </div> |
ng-switch-when
does not understand AngularJS
expressions.
AngularJS converts dashes to camelCase. Thus, stock-widget (or STOCK-WIDGET or even Stock-Widget) in HTML becomes stockWidget in JavaScript.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <div ng-controller="MainCtrl as mainCtrl"> <h3>List of Stocks</h3> <div ng-repeat="stock in mainCtrl.stocks"> <div stock-widget> </div> </div> </div> <script> angular.module('stockMarketApp') .directive('stockWidget', [function() { return { templateUrl: 'stock.html' // template: <...> }; }]); </script> |
The restrict keyword defines how someone using the directive in their code might use it. The default way of using directives is via attributes of existing element
A
, The letter A in the value for restrict specifies that the directive can be used as an
attribute on existing HTML elements (such as <div stock-widget></div>
).E
, The letter E in the value for restrict specifies that the directive can be used as a
new HTML element (such as <stock-widget></stock-widget>
).C
, The letter C in the value for restrict specifies that the directive can be used as a
class name in existing HTML elements (such as <div class="stock-widget"></div>
).M
, The letter M in the value for restrict specifies that the directive can be used as
HTML comments 1 2 3 4 5 6 7 | angular.module('stockMarketApp') .directive('stockWidget', [function() { return { templateUrl: 'stock.html', restrict: 'AE' }; }]); |
Best Practice
ng-show
, ng-class
, and so on)The link function does for a directive what a controller does for a view—it defines APIs and functions that are necessary for the directive, in addition to manipulating and working with the DOM.
AngularJS executes the link function for each instance of the directive, so each instance
can get its own, fully contained business logic while not affecting any other instance of
the directive. link: function($scope, $element, $attr){}
If we need to add functionality to our instance of the directive, we can add it to the scope of the element we’re working with.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | // File: chapter11/directive-with-link/directive.js angular.module('stockMarketApp') .directive('stockWidget', [function() { return { templateUrl: 'stock.html', restrict: 'AE', link: function($scope, $element, $attrs) { $scope.getChange = function(stock) { return Math.ceil(((stock.price - stock.previous) / stock.previous) * 100); }; } }; }]); |
By default, each directive inherits its parent’s scope, which is passed to it in the link function. This can lead to the following problems:
AngularJS gives us the scope key in the directive definition object to have complete control over the scope of the directive element.
false
This is the default value, which basically tells AngularJS that the directive scope is
the same as the parent scope, whichever one it is. So the directive gets access to all
the variables and functions that are defined on the parent scope, and any
modifications it makes are immediately reflected in the parent as well.true
This tells AngularJS that the directive scope inherits the parent
scope, but creates a child scope of its own. The directive thus gets
access to all the variables and functions from the parent scope.object
We can also pass an object with keys and values to the scope. This tells AngularJS
to create what we call an isolated scope.In particular, we can specify three types of values that can be passed in, which AngularJS will directly put on the scope of the directive:
=
, the value of the attribute in HTML to be treated as a
JSON object, which will be bound to the scope of the directive so that any changes
done in the parent scope will be automatically available in the directive.@
, the value of the attribute in HTML is to be treated as a
string, which may or may not have AngularJS binding expressions ({\{ }}
).&
, the value of the attribute in HTML is a function in some
controller whose reference needs to be available to the directive.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <div ng-controller="MainCtrl as mainCtrl"> <h3>List of Stocks</h3> <div ng-repeat="s in mainCtrl.stocks"> <div stock-widget stock-data="s"> </div> </div> </div> <script> // File: chapter11/directive-with-scope/directive.js angular.module('stockMarketApp') .directive('stockWidget', [function() { return { templateUrl: 'stock.html', restrict: 'A', scope: { stockData: '=' }, link: function($scope, $element, $attrs) { $scope.getChange = function(stock) { return Math.ceil(((stock.price - stock.previous) / stock.previous) * 100); }; } }; }]); </script> <!-- File: chapter11/directive-with-scope/stock.html --> <div class="stock-dash"> Name: <span class="stock-name" ng-bind="stockData.name"> </span> Price: <span class="stock-price" ng-bind="stockData.price | currency"> </span> Percentage Change: <span class="stock-change" ng-bind="getChange(stockData) + '%'"> </span> </div> |
For such cases, AngularJS offers the replace key as part of the directive definition object. The replace key takes a Boolean, and it defaults to false. If we specify it to true, AngularJS removes the element that the directive is declared on, and replaces it with the HTML template from the directive definition object.
With AngularJS version 1.3 forward, the replace keyword in the directive definition object has been deprecated.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // File: chapter12/stockDirective.js angular.module('stockMarketApp', []) .directive('stockWidget', [function() { return { templateUrl: 'stock.html', restrict: 'A', scope: { stockData: '=', stockTitle: '@', whenSelect: '&' }, link: function($scope, $element, $attrs) { $scope.getChange = function(stock) { return Math.ceil(((stock.price - stock.previous) / stock.previous) * 100); }; $scope.onSelect = function() { $scope.whenSelect({ stockName: $scope.stockData.name, stockPrice: $scope.stockData.price, stockPrevious: $scope.stockData.previous }); }; } }; }]); |
$compile
service injected into our test.$compile
service.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // File: chapter12/stockDirectiveRenderSpec.js describe('Stock Widget Directive Rendering', function() { beforeEach(module('stockMarketApp')); var compile, mockBackend, rootScope; // Step 1 beforeEach(inject(function($compile, $httpBackend, $rootScope) { compile = $compile; mockBackend = $httpBackend; rootScope = $rootScope; })); it('should render HTML based on scope correctly', function() { // Step 2 var scope = rootScope.$new(); scope.myStock = { name: 'Best Stock', price: 100, previous: 200 }; scope.title = 'the best'; // Step 3 mockBackend.expectGET('stock.html').respond( '<div ng-bind="stockTitle"></div>' + '<div ng-bind="stockData.price"></div>'); // Step 4 var element = compile('<div stock-widget' + ' stock-data="myStock"' + ' stock-title="This is {\{title}}"></div>')(scope); // Step 5 scope.$digest(); mockBackend.flush(); // Step 6 expect(element.html()).toEqual( '<div ng-bind="stockTitle" class="ng-binding">' + 'This is the best' + '</div>' + '<div ng-bind="stockData.price" class="ng-binding">' + '100' + '</div>'); // Step 6 var compiledElementScope = element.isolateScope(); expect(compiledElementScope.stockData) .toEqual(scope.myStock); expect(compiledElementScope.getChange( compiledElementScope.stockData)).toEqual(-50); // Step 7 expect(scopeClickCalled).toEqual(''); compiledElementScope.onSelect(); expect(scopeClickCalled).toEqual('100;200;Best Stock'); }); }); |
When an AngularJS application is loaded in our browser window, the following events are executed in order:
AngularJS adds watchers for all its bindings and ng-model. And whenever one of the aforementioned events happens, AngularJS checks its watchers and bindings to see if anything has changed.
The digest cycle in AngularJS is responsible for keeping the UI up to date in an AngularJS application. The AngularJS UI update cycle happens as follows:
AngularJS directives have a concept of transclusions to allow us to create reusable directives where each implementation might need to render a certain section of the UI differently.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | <script> // File: chapter13/directive-transclusion/directive.js angular.module('stockMarketApp') .directive('stockWidget', [function() { return { templateUrl: 'stock.html', restrict: 'A', transclude: true, scope: { stockData: '=' }, link: function($scope, $element, $attrs) { $scope.getChange = function(stock) { return Math.ceil(((stock.price - stock.previous) / stock.previous) * 100); }; } }; }]); </script> <!-- File: chapter13/directive-transclusion/stock.html --> <div class="stock-dash"> <span ng-transclude></span> Price: <span class="stock-price" ng-bind="stockData.price | currency"> </span> Percentage Change: <span class="stock-change" ng-bind="getChange(stockData) + '%'"> </span> </div> |
The ng-transclude
content explicitly refers to something that
is available in the scope of ng-repeat,
but not inside the directive’s scope.
Thus, the transcluded content and the directive content form a sibling relationship but do not share the same scope.
We try to create a trivial replacement for the ng-repeat
that will pick up
some variables from our outer scope, and add some variables for each instance.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 | <script> // File: chapter13/directive-advanced-transclusion/app.js angular.module('stockMarketApp', []) .controller('MainCtrl', [function() { var self = this; self.stocks = [ {name: 'First Stock', price: 100, previous: 220}, {name: 'Second Stock', price: 140, previous: 120}, {name: 'Third Stock', price: 110, previous: 110}, {name: 'Fourth Stock', price: 400, previous: 420} ]; }]); // File: chapter13/directive-advanced-transclusion/directive.js angular.module('stockMarketApp').directive('simpleStockRepeat', [function() { return { restrict: 'A', // Capture and replace the entire element // instead of just its content transclude: 'element', // A $transclude is passed in as the fifth // argument to the link function link: function($scope, $element, $attrs, ctrl, $transclude) { var myArray = $scope.$eval($attrs.simpleStockRepeat); var container = angular.element( '<div class="container"></div>'); for (var i = 0; i < myArray.length; i++) { // Create an element instance with a new child // scope using the clone linking function var instance = $transclude($scope.$new(), function(clonedElement, newScope) { // Expose custom variables for the instance newScope.currentIndex = i; newScope.stock = myArray[i]; }); // Add it to our container container.append(instance); } // With transclude: 'element', the element gets replaced // with a comment. Add our generated content // after the comment $element.after(container); } }; }]); </script> <div ng-controller="MainCtrl as mainCtrl"> <h3>List of Stocks</h3> <div simple-stock-repeat="mainCtrl.stocks"> We found {\{stock.name}} at {\{currentIndex}} </div> </div> |
Because transclude element
copies the entire element, it also removes the element
from the HTML
Directive controllers are used in AngularJS for inter-directive communication, while link functions are fully contained and specific to the directive instance.
By inter-directive communication, we mean when one directive on an element wants to communicate with another directive on its parent or on the same element.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 | <script> angular.module('stockMarketApp', []) .controller('MainCtrl', [function() { var self = this; self.startedTime = new Date().getTime(); self.stocks = [ {name: 'First Stock', price: 100, previous: 220}, {name: 'Second Stock', price: 140, previous: 120}, {name: 'Third Stock', price: 110, previous: 110}, {name: 'Fourth Stock', price: 400, previous: 420} ]; }]); // File: chapter13/directive-controllers/tabs.js angular.module('stockMarketApp') .directive('tabs', [function() { return { restrict: 'E', transclude: true, scope: true, template: '<div class="tab-headers">' + ' <div ng-repeat="tab in tabs"' + 'ng-click="selectTab($index)"' + 'ng-class="{selected: isSelectedTab($index)}">' + '<span ng-bind="tab.title"></span>' + ' </div>' + '</div>' + '<div ng-transclude></div> ', controller: function($scope) { var currentIndex = 0; $scope.tabs = []; this.registerTab = function(title, scope) { if ($scope.tabs.length === 0) { scope.selected = true; } else { scope.selected = false; } $scope.tabs.push({title: title, scope: scope}); }; $scope.selectTab = function(index) { currentIndex = index; for (var i = 0; i < $scope.tabs.length; i++) { $scope.tabs[i].scope.selected = currentIndex === i; } }; $scope.isSelectedTab = function(index) { return currentIndex === index; }; } }; }]); // File: chapter13/directive-controllers/tab.js angular.module('stockMarketApp') .directive('tab', [function() { return { restrict: 'E', transclude: true, template: '<div ng-show="selected" ng-transclude></div>', require: '^tabs', scope: true, link: function($scope, $element, $attr, tabCtrl) { tabCtrl.registerTab($attr.title, $scope); } }; }]); </script> <div ng-controller="MainCtrl as mainCtrl"> <tabs> <tab title="First Tab"> This is the first tab. The app started at {\{mainCtrl.startedTime | date}} </tab> <tab title="Second Tab"> This is the second tab <div ng-repeat="stock in mainCtrl.stocks"> Stock Name: {\{stock.name}} </div> </tab> </tabs> </div> |
A directive controller is a function that gets the scope and element injected in.
The controller can define functions that are specific to the directive instance
by defining them on $scope
as we have been doing so far, and define the API
or accessible functions and variables by defining them on this or the
controller’s instance.
The require keyword in the directive definition object either takes a string or an array of strings, each of which is the name of the directive that must be used in conjunction with the current directive.
require: 'tabs'
,tells AngularJS to look for a directive called tabs, which exposes a controller on the
same element the directive is on. Implies that AngularJS should locate the directive tabs on the same element, and throw
an error if it’s not found:require: ['tabs', 'ngModel']
, tells AngularJS that both the tabs and ng-model directives must be present on the ele‐
ment our directive is used on. When used as an array, the link function gets an array
of controllers as the fourth argument, instead of just one controller.require: '?tabs'
tells AngularJS to treat the directive as an optional dependency.require: '^tabs'
tells AngularJS that the tabs directive must be present on one of the parent elements.require: '?^tabs'
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | <div ng-controller="MainCtrl as mainCtrl"> <div> The current value of the slider is </div> <no-ui-slider class="slider" ng-model="mainCtrl.selectedValue" range-min="500" range-max="5000"> </no-ui-slider> <div> <input type="number" ng-model="mainCtrl.textValue" min="500" max="5000" placeholder="Set a value"> <button ng-click="mainCtrl.setSelectedValue()"> Set slider value </button> </div> </div> <scirpt> // File: chapter13/directive-slider/app.js angular.module('sliderApp', []) .controller('MainCtrl', [function() { var self = this; self.selectedValue = 2000; self.textValue = 4000; self.setSelectedValue = function() { self.selectedValue = self.textValue; }; }]); // File: chapter13/directive-slider/noui-slider.js angular.module('sliderApp') .directive('noUiSlider', [function() { return { restrict: 'E', require: 'ngModel', link: function($scope, $element, $attr, ngModelCtrl) { $element.noUiSlider({ // We might not have the initial value in ngModelCtrl yet start: 0, range: { // $attrs by default gives us string values // nouiSlider expects numbers, so convert min: Number($attr.rangeMin), max: Number($attr.rangeMax) } }); // When data changes inside AngularJS // Notify the third party directive of the change ngModelCtrl.$render = function() { $element.val(ngModelCtrl.$viewValue); }; // When data changes outside of AngularJS $element.on('set', function(args) { // Also tell AngularJS that it needs to update the UI $scope.$apply(function() { // Set the data within AngularJS ngModelCtrl.$setViewValue($element.val()); }); }); } }; }]); </scirpt> |
AngularJS calls the $render
method whenever the model value
changes inside AngularJS (for example, when it is initialized to a value in our controller).
A third-party UI component is outside the AngularJS life cycle,
so we need to manually call $scope.$apply()
to ensure that AngularJS updates the UI.
The $scope.$apply()
call takes an optional function as
an argument and ensures that the AngularJS digest cycle that’s
responsible for updating the UI with the latest values is triggered.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <div ng-controller="MainCtrl as mainCtrl"> <h3>Zip Code Input</h3> <h5>Zips are allowed in one of the following formats</h5> <ul> <li>12345</li> <li>12345 1234</li> <li>12345-1234</li> </ul> <form novalidate=""> Enter valid zip code: <input type="text" ng-model="mainCtrl.zip" valid-zip> </form> </div> <script> // File: chapter13/directive-custom-validator/app.js angular.module('stockMarketApp', []) .controller('MainCtrl', [function() { this.zip = '1234'; }]); // File: chapter13/directive-custom-validator/directive.js angular.module('stockMarketApp') .directive('validZip', [function() { var zipCodeRegex = /^\d{5}(?:[-\s]\d{4})?$/g; return { restrict: 'A', require: 'ngModel', link: function($scope, $element, $attrs, ngModelCtrl) { // Handle DOM update --> Model update // The parser function has to return the correct value (if the data is valid) or undefined (in case the data isn’t). ngModelCtrl.$parsers.unshift(function(value) { var valid = zipCodeRegex.test(value); ngModelCtrl.$setValidity('validZip', valid); return valid ? value : undefined; }); // Handle Model Update --> DOM // We again check for validity here and return the value. ngModelCtrl.$formatters.unshift(function(value) { ngModelCtrl.$setValidity('validZip', zipCodeRegex.test(value)); return value; }); } }; }]); </script> |
In the directive life cycle, we mentioned that a directive goes through two distinct phases: a compile step and a link step.
The compile step in a directive is the correct place to do any sort of HTML template manipulation and DOM transformation. We never use the link and compile functions together, because when we use the compile key, we have to return a linking function from within it instead.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 | <form novalidate="" name="mainForm"> <form-element type="text" name="uname" bind-to="mainCtrl.username" label="Username" required ng-minlength="5"> <validation key="required"> Please enter a username </validation> <validation key="minlength"> Username must be at least 5 characters </validation> </form-element> Username is <form-element type="password" name="pwd" bind-to="mainCtrl.password" label="Password" required ng-pattern="/^[a-zA-Z0-9]+$/"> <validation key="required"> Please enter a password </validation> <validation key="pattern"> Password must only be alphanumeric characters </validation> </form-element> Password is <button>Suubmit</button> </form> <script> // File: chapter13/directive-compile/app.js angular.module('dynamicFormApp', []) .controller('MainCtrl', [function() { var self = this; self.username = ''; self.password = ''; }]); // File: chapter13/directive-compile/directive.js angular.module('dynamicFormApp') .directive('formElement', [function() { return { restrict: 'E', require: '^form', scope: true, compile: function($element, $attrs) { var expectedInputAttrs = { 'required': 'required', 'ng-minlength': 'ngMinlength', 'ng-pattern': 'ngPattern' // More here to be implemented }; // Start extracting content from the HTML var validationKeys = $element.find('validation'); var presentValidationKeys = {}; var inputName = $attrs.name; angular.forEach(validationKeys, function(validationKey) { validationKey = angular.element(validationKey); presentValidationKeys[validationKey.attr('key')] = validationKey.text(); }); // Start generating final element HTML var elementHtml = '<div>' + '<label>' + $attrs.label + '</label>'; elementHtml += '<input type="' + $attrs.type + '" name="' + inputName + '" ng-model="' + $attrs.bindTo + '"'; $element.removeAttr('type'); $element.removeAttr('name'); for (var i in expectedInputAttrs) { if ($attrs[expectedInputAttrs[i]] !== undefined) { elementHtml += ' ' + i + '="' + $attrs[expectedInputAttrs[i]] + '"'; } $element.removeAttr(i); } elementHtml += '>'; elementHtml += '<span ng-repeat="(key, text) in validators" ' + ' ng-show="hasError(key)"' + ' ng-bind="text"></span>'; elementHtml += '</div>'; $element.html(elementHtml); return function($scope, $element, $attrs, formCtrl) { $scope.validators = angular.copy(presentValidationKeys); $scope.hasError = function(key) { return !!formCtrl[inputName]['$error'][key]; }; }; } }; }]); </script> |
Finally, we return a postLink function (we cannot have a link keyword along with compile; we need to return the link function from within compile instead), which adds the validators array and a hasError function to show each of the validation messages under the correct conditions.
As mentioned before, compile is only used in the rarest of cases, where you need to do major DOM transformations at runtime.
When a post-link
function executes, all children directives
have been compiled and linked at this point.
But in case we needed a hook to execute something before the children are linked, we
can add what is called pre-link
function. At this point, the children directives aren’t
linked, and DOM transformations are not safe and can have weird effects.
1 2 3 4 5 6 7 8 9 | { link: function($scope, $element, $attrs) {} } { link: { pre: function($scope, $element, $attrs) {}, post: function($scope, $element, $attrs) {} } } |
The last two options we look at when creating directives are priority and terminal.
priority
is used to decide the order in which directives are evaluated when there are
multiple directives used on the same element. For example, when we use the ngModel
directive along with ngPattern
or ngMinlength
, we need to ensure that ngPattern
or
ngMinlength
executes only after ngModel
has had a chance to execute.
By default, any directive we create has a priority of 0.
The terminal
keyword in a directive is used to ensure that no other directives are
compiled or linked on an element after the current priority directives are finished.
AngularJS cannot clean up event listeners we add on elements outside of the scope and HTML of the directive. When we add these listeners or watchers, it becomes our responsibility to clean up when the directive gets destroyed.
1 2 3 4 5 6 7 | $scope.$on('$destroy', function() { // Do clean up here }); $element.$on('$destroy', function() { // Do clean-up here }); |
These basically get triggered by AngularJS whenever the variable under watch changes, and we get access to both the new and the old value in such a case.
$watch
, whenever the value changes, then the function passed to it as the
second argument is triggered with the old and new value. Which takes:$watch
, The same as the standard watch, but takes a Boolean true as the third argument.
This forces AngularJS to recursively check each object and key inside the object or
variable and use angular.equals to check for equality for all objects.$watchCollection
, The function is triggered any time an
item is added, removed, or moved in the array. It does not watch for changes to
individual properties in an item in the array.Whenever you’re working with third-party components, remember that there are two distinct life cycles at play. The first is the AngularJS life cycle that is responsible for the keeping the UI updated and the second is a third-party component’s life cycle.
And this is done by triggering $scope.$apply()
,
which starts a digest cycle on the $rootScope
.
Sometimes, another event in AngularJS will automatically trigger and take care of this,
but in any case if you are updating any scope variables in response to an external event,
make sure you manually trigger the $scope.$apply()
or $scope.$digest()
.
AngularJS filters are used to process data and format values to present to the user.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | <div ng-controller="FilterCtrl as ctrl"> <div> Amount as a number: {\{ctrl.amount | number}} </div> <div> Total Cost as a currency: {\{ctrl.totalCost | currency}} </div> <div> Total Cost in INR: {\{ctrl.totalCost | currency:'INR '}} </div> <div> Shouting the name: {\{ctrl.name | uppercase}} </div> <div> Whispering the name: {\{ctrl.name | lowercase}} </div> <div> Start Time: {\{ctrl.startTime | date:'medium'}} </div> </div> <script type="text/javascript"> angular.module('filtersApp', []) .controller('FilterCtrl', [function() { this.amount = 1024; this.totalCost = 4906; this.name = 'Shyam Seshadri'; this.startTime = new Date().getTime(); }]); </script> |
The filter will take the value of the expression (a string, number, or array) and convert it into some other form.
{\{ctrl.name | lowercase | limitTo: 5}}
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | <ul ng-controller="FilterCtrl as ctrl"> <li> Amount - {\{ctrl.amount}} </li> <li> Amount - Default Currency: {\{ctrl.amount | currency}} </li> <li> <!-- Using the English pound sign --> Amount - INR Currency: {\{ctrl.amount | currency:'£ '}} </li> <li> Amount - Number: {\{ctrl.amount | number}} </li> <li> Amount - No. with 4 decimals: {\{ctrl.amount | number:4}} </li> <li> Name with no filters: {\{ctrl.name}} </li> <li> Name - lowercase filter: {\{ctrl.name | lowercase}} </li> <li> Name - uppercase filter: {\{ctrl.name | uppercase}} </li> <li> <!-- printed as a string {"test": "value", "num": 123} --> The JSON Filter: {\{ctrl.obj | json}} </li> <li> Timestamp: {\{ctrl.startTime}} </li> <li> Default Date filter: {\{ctrl.startTime | date}} </li> <li> Medium Date filter: {\{ctrl.startTime | date:'medium'}} </li> <li> Custom Date filter: {\{ctrl.startTime | date:'M/dd, yyyy'}} </li> </ul> <script type="text/javascript"> angular.module('filtersApp', []) .controller('FilterCtrl', [function() { this.amount = 1024; this.name = 'Shyam Seshadri'; this.obj = {test: 'value', num: 123}; this.startTime = new Date().getTime(); }]); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 | <div ng-controller="FilterCtrl as ctrl"> <button ng-click="ctrl.currentFilter = 'string'"> Filter with String </button> <button ng-click="ctrl.currentFilter = 'object'"> Filter with Object </button> <button ng-click="ctrl.currentFilter = 'function'"> Filter Text </button> <input type="text" ng-model="ctrl.filterOptions['string']"> Show Done Only <input type="checkbox" ng-model="ctrl.filterOptions['object'].done"> <ul> <li ng-repeat="note in ctrl.notes | filter:ctrl.filterOptions[ctrl.currentFilter] | orderBy:ctrl.sortOrder | limitTo:5"> {\{note.label}} - - </li> </ul> </div> <script type="text/javascript"> angular.module('filtersApp', []) .controller('FilterCtrl', [function() { this.notes = [ {label: 'FC Todo', type: 'chore', done: false}, {label: 'FT Todo', type: 'task', done: false}, {label: 'FF Todo', type: 'fun', done: true}, {label: 'SC Todo', type: 'chore', done: false}, {label: 'ST Todo', type: 'task', done: true}, {label: 'SF Todo', type: 'fun', done: true}, {label: 'TC Todo', type: 'chore', done: false}, {label: 'TT Todo', type: 'task', done: false}, {label: 'TF Todo', type: 'fun', done: false} ]; this.sortOrder = ['+type', '-label']; this.filterOptions = { "string": '', "object": {done: false, label: 'C'}, "function": function(note) { return note.type === 'task' && note.done === false; } }; this.currentFilter = 'string'; }]); </script> |
string
, AngularJS will look for the string in the keys
of each object of the array, and if it is found, the element is included.object
, AngularJS takes each key of the object and makes sure that its value
is present in the corresponding key of each object of the array. For example,
an object expression like {size: "M"}
would check each item of
the array and ensure that the objects have a key called size
and that they contain the letter “M”function
, return true or falseAngularJS allows us to use the filters wherever we want or need through the power of Dependency Injection.
Any filter, whether built-in or our own, can be injected into any service or controller by affixing the word “Filter” at the end of the name of the filter, and asking it to be injected.
1 2 3 4 5 6 | // if we need the currency filter in our controller angular.module('myModule', []) .controller('MyCtrl', ['currencyFilter', function(currencyFilter) { self.filteredArray = filterFilter(self.notes, 'ch'); }]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | <div ng-controller="FilterCtrl as ctrl"> <div> Start Time (Timestamp): {\{ctrl.startTime}} </div> <div> Start Time (DateTime): {\{ctrl.startTime | date:'medium'}} </div> <div> Start Time (Our filter): {\{ctrl.startTime | timeAgo}} </div> <div> someTimeAgo : {\{ctrl.someTimeAgo | date:'short'}} </div> <div> someTimeAgo (Our filter): {\{ctrl.someTimeAgo | timeAgo}} </div> </div> <script type="text/javascript"> angular.module('filtersApp', []) .controller('FilterCtrl', [function() { this.startTime = new Date().getTime(); this.someTimeAgo = new Date().getTime() - (1000 * 60 * 60 * 4); }]) .filter('timeAgo', [function() { var ONE_MINUTE = 1000 * 60; var ONE_HOUR = ONE_MINUTE * 60; var ONE_DAY = ONE_HOUR * 24; var ONE_MONTH = ONE_DAY * 30; return function(ts) { var currentTime = new Date().getTime(); var diff = currentTime - ts; if (diff < ONE_MINUTE) { return 'seconds ago'; } else if (diff < ONE_HOUR) { return 'minutes ago'; } else if (diff < ONE_DAY) { return 'hours ago'; } else if (diff < ONE_MONTH) { return 'days ago'; } else { return 'months ago'; } }; }]); //return function(ts, arg1, arg2, arg3) { //{\{ctrl.startTime | timeAgo:arg1:arg2:arg3}} </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | // File: chapter9/timeAgoFilter.js angular.module('filtersApp', []) .filter('timeAgo', [function() { var ONE_MINUTE = 1000 * 60; var ONE_HOUR = ONE_MINUTE * 60; var ONE_DAY = ONE_HOUR * 24; var ONE_MONTH = ONE_DAY * 30; return function(ts, optShowSecondsMessage) { if (optShowSecondsMessage !== false) { optShowSecondsMessage = true; } var currentTime = new Date().getTime(); var diff = currentTime - ts; if (diff < ONE_MINUTE && optShowSecondsMessage) { return 'seconds ago'; } else if (diff < ONE_HOUR) { return 'minutes ago'; } else if (diff < ONE_DAY) { return 'hours ago'; } else if (diff < ONE_MONTH) { return 'days ago'; } else { return 'months ago'; } }; }]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | // File: chapter9/timeAgoFilterSpec.js describe('timeAgo Filter', function() { beforeEach(module('filtersApp')); var filter; beforeEach(inject(function(timeAgoFilter) { filter = timeAgoFilter; })); it('should respond based on timestamp', function() { // The presence of new Date().getTime() makes it slightly // hard to unit test deterministicly. // Ideally, we would inject a dateProvider into the timeAgo // filter, but we are trying to keep it simple here. // So we will assume that our tests are fast enough to // execute in mere milliseconds. var currentTime = new Date().getTime(); currentTime -= 10000; expect(filter(currentTime)).toEqual('seconds ago'); var fewMinutesAgo = currentTime - 1000 * 60; expect(filter(fewMinutesAgo)).toEqual('minutes ago'); var fewHoursAgo = currentTime - 1000 * 60 * 68; expect(filter(fewHoursAgo)).toEqual('hours ago'); var fewDaysAgo = currentTime - 1000 * 60 * 60 * 26; expect(filter(fewDaysAgo)).toEqual('days ago'); var fewMonthsAgo = currentTime - 1000 * 60 * 60 * 24 * 32; expect(filter(fewMonthsAgo)).toEqual('months ago'); }); }); |
AngularJS provides us with an optional module called ngRoute, which can be used to do routing in an AngularJS application.
We mark in our HTML where we want the routing to take
effect with the ng-view
directive.
It takes care of the browser history, so you can actually use back and forward buttons in your browser to navigate within the application.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | <script type="text/javascript" src="/path/to/angular-route.min.js"></script> <ul> <li><a href="#/">Default Route</a></li> <li><a href="#/second">Second Route</a></li> <li><a href="#/asdasdasd">Nonexistent Route</a></li> </ul> <div ng-view></div> <script type="text/javascript"> angular.module('routingApp', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/', { template: '<h5>This is the default route</h5>' }) .when('/second', { template: '<h5>This is the second route</h5>' }) .otherwise({redirectTo: '/'}); }]); </script> |
The AngularJS route definition allows us to
define more complex templates.
The $routeProvider.when
function takes a URL or
URL regular expression as the first argument, and the
route configuration object as the second.
1 2 3 4 5 6 7 | $routeProvider.when(url, { template: string, templateUrl: string, controller: string, function or array, controllerAs: string, resolve: object<key, function> }); |
/list
, /recipe/:recipeId
tempateUrl
1 2 3 | $routeProvider.when('/test', { templateUrl: 'views/test.html', }); |
controller, if we have not directly defined the controller in the HTML using the ng-controller directive, e we pass the con‐ troller function directly to the controller key.
1 2 3 4 5 6 | $routeProvider.when('/test', { template: '<h1>Test Route</h1>', controller: ['$window', function($window) { $window.alert('Test route has been loaded!'); }] }); |
controllerAs
1 2 3 4 5 6 7 8 9 | $routeProvider.when('/test', { template: '<h1>Test Route</h1>', controller: 'MyCtrl as ctrl' }); $routeProvider.when('/test', { template: '<h1>Test Route</h1>', controller: 'MyCtrl', controllerAs: 'ctrl' }); |
redirectTo
1 2 3 4 5 6 | $routeProvider.when('/new', { template: '<h1>New Route</h1>' }); $routeProvider.when('/old', { redirectTo: '/new' }); |
resolve, At a conceptual level, resolves are a way of executing and finishing asynchronous tasks before a particular route is loaded. This is a great way to check if the user is logged in and has authorization and permissions, and even preload some data before a controller and route are loaded into the view.
When we define a resolve, we can define a set of asynchronous tasks to execute before the route is loaded. A resolve is a set of keys and functions. Each function can return a value or a promise.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | angular.module('resolveApp', ['ngRoute']) .value('Constant', {MAGIC_NUMBER: 42}) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/', { template: '<h1>Main Page, no resolves</h1>' }) .when('/protected', { template: '<h2>Protected Page</h2>', resolve: { immediate: ['Constant', function(Constant) { return Constant.MAGIC_NUMBER * 4; }], async: ['$http', function($http) { return $http.get('/api/hasAccess'); }] }, controller: ['$log', 'immediate', 'async', function($log, immediate, async) { $log.log('Immediate is ', immediate); $log.log('Server returned for async', async); }] }); }]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | angular.module('resolveApp', ['ngRoute']) .config(['$routeProvider', function($routeProvider) { $routeProvider.when('/', { template: '<h1>Main Page</h1>' }).when('/detail/:detId', { template: '<h2>Loaded ' + ' and query String is </h2>', controller: ['$routeParams', function($routeParams) { this.detailId = $routeParams.detId; this.qStr = $routeParams.q; }], controllerAs: 'myCtrl' }); }]); // /detail/123?q=MySearchParam |
For every AngularJS application that uses ngRoute, there can be one and only
one ng-view
directive for that application.
TODO
A URL like http://www.myawesomeapp.com/#/first/
page would look
like http://www.myawesomeapp.com/first/page
with HTML5 mode enabled.
To support search engine crawling, it is expected
that the SPA will use hashbang URLs instead of
pure hash URLs (#!
instead of #
).
1 2 3 4 5 6 7 8 9 10 | angular.module('myHtml5App', ['ngRoute']) .config(['$locationProvider', '$routeProvider', function($locationProvider, $routeProvider) { $locationProvider.html5Mode(true); //Optional $locationProvider.hashPrefix('!'); // Route configuration here as normal // Route for /first/page // Route for /second/page }]); |
This is to tell the browser where, in relation to the URL, the static resources are served from, so that if the application requests an image or CSS file with a relative path, it doesn’t take it from the current URL necessarily.
All relative paths would be resolved relative to /app and not to some other URL.
1 2 3 4 5 | <html> <head> <base href="/app" /> </head> </html> |
But what if we had more complex requirements and wanted to change different parts
of our UI differently depending on the URL? use ng-show
or ng-hide
or ng-switch
ui-router
is state-oriented, and by default does
not modify URLs. We need to specify the URL for
each state individually.
We should consider using ui-router if our project needs or has the following requirements:
响应式, 可以适应不同尺寸的屏幕, 自动响应变化自己的外观的网站.使用html5和css3
HTML5 由全球五大浏览器厂商共同指定
<!doctype html>
, 包含了以往所有版本的文档dtd功能
1 2 3 4 5 6 7 8 9 | <!doctype html> <html> <head> <meta charset='utf-8'/> <meta name=”renderer” content=”webkit|ie-comp|ie-stand”> </head> <body> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | /* 禁用iPhone中Safari的字号自动调整 */ html { -webkit-text-size-adjust: 100%; /*让谷歌浏览器可以设定字体大小*/ -ms-text-size-adjust: 100%; } /* 设置HTML5元素为块 */ article,aside, details, figcaption,figure, footer, header, hgroup,menu, nav, section { display:block; } /* 设置图片视频等自适应调整 */ img { max-widthL: 100%; height:auto; width:auto\9; /* ie8 */ } .video embed, .video object, .video iframe { width: 100%; height: auto; } |
ie9+对html5支持, 检测浏览器支持html5. 任何浏览器对不能识别的标签, 都能显示里面的内容
1 | <canvas> 您的浏览器版本过低, 请更新浏览器 </canvas> |
用来布局
<header> </header>
<nav></nav>
<section></section>
, 分区标签, 语义上大于 div<aside></aside>
<article></article>
<footer></footer>
hgroup
定义比标题标记的组figure
定义一组媒体内容及其标题figcaption
定义figure元素的标题h5新增的标签本质是div制作的, 最大的意义在于嵌套使用. 比如 header
, hgroup>h2
早在html4, 或者 xhtml1.0 是没有处理视频的能力的, 只能通过某些插件来实现.
1 2 3 4 | <video src="" controls="controls"></video> <video src="" autoplay ="autoplay"></video> <!-- 设置视频循环播放 --> <video src="" loop="loop"></video> |
支持的格式, ogg, mpeg4, webm(w3c推荐格式), 但是不是所有浏览器都支持.
可以放两个视频来全部支持如 ogg
组合mp4
, webm
组合mp4
1 2 3 4 5 | <video width="" height="" controls="controls"> <source src="" type="video/ogg"></source> <source src="" type="video/mp4"></source> Your browser dont support this tag </video> |
1 2 3 4 5 6 | video.play(); video.pause(); // 快进 video.currentTime += 10; // 加速 video.playbackRate = 2; |
1 | <audio src="" controls="controls" autoplay loop> </audio> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <!-- 温度计标签 --> <meter low="15" value="23" hight="" min="" max=""></meter> <!-- 进度条 --> <progress max='100' value='0'> </progress> <input type="text" list="car"/> <datalist id='car'> <option> one </option> <option> two </option> <option> three</option> </datalist> <details> <summary>概要</summary> 详情内容 </details> <!-- 新增表单元素 --> <input type="email"/> <input type="color"/> <input type="range" min="0" max="100"/> <input type="search"/> <script> $().change(function(){}) </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 | // 新增了选择器 nth-of-type 等同于 eq(), 从 1 开始 ul li:nth-of-type(1) { border-radius: 10px; border-radius: 5 0%; /**上右下左**/ border-radius: 10px 10px 10px 10px; } div { /*水平偏移 纵向偏移 羽化程度*/ box-shadow: 0 0 10px #000; /*内阴影*/ box-shadow: 0 0 10px #000 inset; /*透明*/ background: rgba(0, 0, 0, 0.5) /*文字投影*/ text-shadow: 0 0 10px #000; } /*转换为内联级元素*/ section { display: box; display: -webkit-box; } article {-webkit-box-flex: 3;} /* 3/8 */ article {-webkit-box-flex: 5;} /* 5/8 */ |
:nth-child
, 表示过滤当前容器中的第几个:nth-of-type
表示过滤当前容器中同类型的第几个, 相当于 eq()
:first-child
, :last-child
:not()
排除指定的元素:empty
空元素[key=value]
, 过滤拥有指定属性值的元素, 例如 input[type=button]
:before
:after
1 2 | div:before { content: '你好'; display: block;} div:after {content: '';} |
-ms-
-moz-
-webkit-
-0-
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | div{ /* 顺时针旋转, 旋转的是整个坐标系 */ transform: rotate(45deg); transform: scale(0.5, 2); /* 移动横向, 纵向 */ transform: translate(100px, 0) scale(2, 2); transform-origin: 0, 10px; /*设置旋转原点*/ transform-origin: 50%, top; transform-origin: left, center; transform-origin: right, bottom; /*沿着y轴转*/ transform: rotateY(45deg); /*沿着X轴转*/ transform: rotateX(45deg); /*沿着Z轴转*/ transform: rotate(45deg); } |
1 2 3 4 5 6 7 | /*方式一*/ div {transition: width 1s, height 2s;} /*默认样式*/ div:hover {width: 400px; height: 300px;} /*方式二 linear */ div{transition: all 2s ease 2s;} /*transition-timing-function transition-delay*/ |
linear
匀速度ease
逐渐慢下来ease-in
加速ease-out
减速ease-in-out
先加后减1 2 3 4 5 6 7 8 9 10 11 12 13 14 | div { /*角度*/ background: linear-gradient(180deg, red, green 30%, blue); background-size: cover; /*拉申*/ background-size: 100% 100%; /* 背景盒子永远一样大*/ background-size: contain; /* 等比例拉申*/ background-image:url(), url(); /*设置两张背景图片*/ } /*精灵图的缩放*/ div { /*使用背景缩放*/ background-size: 80px 283px; } |
排除 border和padding 对宽高的影响
1 | box-sizing: border-box; |
animation, 让指定的元素实现自动运行的动画
1 2 3 4 5 6 7 8 9 10 | div { /*linear ease-in*/ /*正常播放, 轮流反向播放*/ -webkit-animation: sport 2s ease-out 4s infinite|n normal|alternate running|paused; } div:hover {animation-play-state: paused;} @-webkit-keyframes sport { %0{left:0} %100{left:500} } |
通过跳转域名的方式来制作手机, 使用device.js
判断手记设备
1 2 | window.location = ''; window.location.href = ''; |
icomoon.io
iconfont.cn2
1 2 3 4 | /* 声明字体 */ @font-face {} /* 引用 */ div{ font-family: ; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 | <style> body{ /*设置透视效果*/ perspective: 1000px; transform: translate3d(100%, 100%, 100%); } /*transform-style:preserve-3d|flat*/ ul{ transform-style: preserve-3d; width: 200px;height:200px; border: 1px dashed; position: relative; transition: all 4s; } ul:hover { transform:rotateX(360deg) rotateY(360deg); } ul li{ width: 200px; height: 200px; border: 1px; position: absolute; } /*可以通过设置 scale 来设置成长方体*/ li:nth-of-type(1){ background: red; transform: rotateY(90deg) translateZ(100px);} li:nth-of-type(2){ background: green; transform: rotateY(270deg) translateZ(100px);} li:nth-of-type(3){ background: gold; transform: rotateX(0deg) translateZ(100px);} li:nth-of-type(4){ background: blue; transform: rotateX(90deg) translateZ(100px);} li:nth-of-type(5){ background: purple; transform:rotateX(180deg) translateZ(100px);} li:nth-of-type(6){ background: pink; transform:rotateX(270deg) translateZ(100px);} </style> <ul> <li></li> <li></li> <li></li> <li></li> <li></li> <li></li> </ul> |
响应式布局实现基本上是通过百分比来完成的, 必须了解设备的长 宽.
1 2 3 | min-height: 300px; width: 70%; min-width: 20px; box-sizing: border-box; |
1 2 3 4 5 6 7 8 9 10 | /* 内嵌 */ @media (max-width: 620px) {/* mobile css */} @media (min-width:621px) and (max-width: 980px) {/* pad css */} @media (min-width:981px) {/* desktop css */} /* 链接式 */ <link rel="stylesheet" type="text/css" href="css/phone.css" media="all"/> <link rel="stylesheet" type="text/css" href="css/phone.css" media=" screen and (max-width:620px)"/>1 <link rel="stylesheet" type="text/css" href="css/pad.css" media="(min-width:621px) and (max-width:980px)"/> <link rel="stylesheet" type="text/css" href="css/pc.css" media="(min-width:981px)"/> |
pad和手机 正文文字一般都是 14 加粗.
1 2 3 4 | body>*{min-width: 400px} <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no"> |
将制作的网页封装成手机app, 可以安装到手机里面
1 2 |
root em 就是以根 html
1 | height: 2rem; |
em
默认以父级大小
需要导入CSS样式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 | <div data-role="page"> <div data-role="header"> <!-- 添加图标, 在image中找 arrow-l-black --> <a href="#" data-icon="arrow-l">后退</a> <h1>我的手机网站</h1> <!-- 设置图片的位置 top (notext没有文字))--> <a href="#" data-icon="arrow-r" data-iconpos="right">后退</a> </div> <div data-role="content"> <!-- 内部连接 #ID --> <a href="#page1" data-role='button'>订阅</a> <!-- 外部链接 --> <a href="out.html" rel='external' >订阅</a> <!-- 转产效果 flip slide fade Slidedown Slideup --> <a href="out.html" data-role='button' data-transition='pop'>订阅</a> </div> <!-- .ui-control-group{100%;}; a{width:25%; border-sizing: boder-box; } --> <div data-role="footer" data-role="controlgroup" data-type="horizontal" data-position='fixed'> <a href="#" data-role="button"></a> <a href="#" data-role="button"></a> <a href="#" data-role="button"></a> <a href="#" data-role="button"></a> </div> </div> <div data-role="page" id=’page1‘> <div data-role="header"> <h1>我的手机网站</h1> </div> <div data-role="content"> 这里是第二部分内容 <select name="''" id="''"> <option> 选择 </option> </select> </div> <div data-role="footer" data-position='fixed'> <h4>版权信息</h4> </div> </div> <!-- 弹出对话框 --> <a href="#page2" data-rel="dialog"></a> <div data-role="page" id="page2"> <div data-role="header"> <h2>您确定要退出吗?</h2> </div> <div data-role="content"> 确定 返回 </div> </div> <div data-role="page" id="page2"> <div data-role="header"> <h2>您确定要退出吗?</h2> </div> <div data-role="content"> <a href="#" data-role="button">重启</a> <a href="#">静音</a> <a href="#"></a> </div> </div> <!-- 列表 --> <div dat-role="content"> <ul data-role="listview" data-inset="true" data-filter="true" data-filter-placeholder="请输入名字"> <li> <a> <img src=""/> <h3>刘德华</h3> <p>简介</p> </a> </li> <li><a>张学友</a></li> <li><a></a></li> <li><a></a></li> </ul> </div> <div data-role="content"> <!-- ui-grid-a 两个子盒子排列 ui-grid-b 三个子盒子排列 以此类推--> <div class="ui-grid-a"> <div class="ui-block-a"> </div> <div class="ui-block-b"> </div> </div> </div> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | // File: chapter7/karma.conf.js // Karma configuration module.exports = function(config) { config.set({ basePath: '', frameworks: ['jasmine'], files: [ 'angular.min.js', 'angular-mocks.js', '*.js' ], exclude: [], port: 8080, logLevel: config.LOG_INFO, autoWatch: true, browsers: ['Chrome'], singleRun: false }); }; // File: chapter7/simpleCtrl2.js angular.module('simpleCtrl2App', []) .controller('SimpleCtrl2', ['$location', '$window', function($location, $window) { var self = this; self.navigate1 = function() { $location.path('/some/where'); }; self.navigate2 = function() { $location.path('/some/where/else'); }; }]); // File: chapter7/simpleCtrl2Spec.js describe('SimpleCtrl2', function() { beforeEach(module('simpleCtrl2App')); var ctrl, $loc; beforeEach(inject(function($controller, $location) { ctrl = $controller('SimpleCtrl2'); $loc = $location; })); it('should navigate away from the current page', function() { expect($loc.path()).toEqual(''); $loc.path('/here'); ctrl.navigate1(); expect($loc.path()).toEqual('/some/where'); }); it('should navigate away from the current page', function() { expect($loc.path()).toEqual(''); $loc.path('/there'); ctrl.navigate2(); expect($loc.path()).toEqual('/some/where/else'); }); }); |
Now for the purpose of our unit test, we want to mock out ItemService.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // File: chapter7/notesApp1.js angular.module('notesApp1', []) .factory('ItemService', [function() { var items = [ {id: 1, label: 'Item 0'}, {id: 2, label: 'Item 1'} ]; return { list: function() { return items; }, add: function(item) { items.push(item); } }; }]) .controller('ItemCtrl', ['ItemService', function(ItemService) { var self = this; self.items = ItemService.list(); }]); |
This provider shares its namespace with the modules loaded before. So now we create our mockService and tell the provider that when any controller or service asks for ItemSer vice, give it our value.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | // File: chapter7/notesApp1Spec.js describe('ItemCtrl with inline mock', function() { beforeEach(module('notesApp1')); var ctrl, mockService; beforeEach(module(function($provide) { mockService = { list: function() { return [{id: 1, label: 'Mock'}]; } }; $provide.value('ItemService',mockService); })); beforeEach(inject(function($controller) { ctrl = $controller('ItemCtrl'); })); it('should load mocked out items', function() { expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]); }); }); |
To change the preceding to be a more reusable, general-purpose mock of the ItemSer vice, we could do the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | // File: chapter7/notesApp1-mocks.js angular.module('notesApp1Mocks', []) .factory('ItemService', [function() { return { list: function() { return [{id: 1, label: 'Mock'}]; } }; // File: chapter7/notesApp1SpecWithMock.js describe('ItemCtrl With global mock', function() { var ctrl; beforeEach(module('notesApp1')); beforeEach(module('notesApp1Mocks')); beforeEach(inject(function($controller) { ctrl = $controller('ItemCtrl'); })); it('should load mocked out items', function() { expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]); }); }); |
Spies allow us to hook into certain functions, and check whether they were called, how many times they were called, what arguments they were called with, and so on.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | describee('ItemCtrl with spies', function() {
beforeEach(module('notesApp1'));
var ctrl, itemService;
beforeEach(inject(function($controller, ItemService) {
// tell it to continue calling the actual service underneath by
// calling andCallThrough on the spy.
spyOn(ItemService, 'list').andCallThrough();
itemService = ItemService;
ctrl = $controller('ItemCtrl');
}));
it('should load mocked out items', function() {
expect(itemService.list).toHaveBeenCalled();
expect(itemService.list.callCount).toEqual(1);
expect(ctrl.items).toEqual([
{id: 1, label: 'Item 0'},
{id: 2, label: 'Item 1'}
]);
});
});
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // File: chapter7/notesApp1SpecWithSpyReturn.js describe('ItemCtrl with SpyReturn', function() { beforeEach(module('notesApp1')); var ctrl, itemService; beforeEach(inject(function($controller, ItemService) { spyOn(ItemService, 'list') .andReturn([{id: 1, label: 'Mock'}]); itemService = ItemService; ctrl = $controller('ItemCtrl'); })); it('should load mocked out items', function() { expect(itemService.list).toHaveBeenCalled(); expect(itemService.list.callCount).toEqual(1); expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]); }); }); |
With AngularJS, as long as we include the angular-mocks.js file as part of the Karma configuration, AngularJS takes care of ensuring that when we use the $http service, it doesn’t actually make server calls.
1 2 3 4 5 6 7 8 9 10 11 12 | // File: chapter7/serverApp.js angular.module('serverApp', []) .controller('MainCtrl', ['$http', function($http) { var self = this; self.items = []; self.errorMessage = ''; $http.get('/api/note').then(function(response) { self.items = response.data; }, function(errResponse) { self.errorMessage = errResponse.data.msg; }); }]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 | // File: chapter7/serverAppSpec.js describe('MainCtrl Server Calls', function() { beforeEach(module('serverApp')); var ctrl, mockBackend; beforeEach(inject(function($controller, $httpBackend) { mockBackend = $httpBackend; mockBackend.expectGET('/api/note') .respond([{id: 1, label: 'Mock'}]); ctrl = $controller('MainCtrl'); // At this point, a server request will have been made })); it('should load items from server', function() { // Initially, before the server responds, // the items should be empty expect(ctrl.items).toEqual([]); // Simulate a server response // $httpBackend.flush(3) flush three request mockBackend.flush(); expect(ctrl.items).toEqual([{id: 1, label: 'Mock'}]); }); afterEach(function() { // Ensure that all expects set on the $httpBackend // were actually called mockBackend.verifyNoOutstandingExpectation(); // Ensure that all requests to the server // have actually responded (using flush()) mockBackend.verifyNoOutstandingRequest(); }); }); |
AngularJS provides the ng-model
directive for us to deal with
inputs and two-way data-binding
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <body ng-controller="MainCtrl as ctrl"> <input type="text" ng-model="ctrl.username"/> You typed <button ng-click="ctrl.change()">Change Values</button> </body> <script type="text/javascript"> angular.module('notesApp', []) .controller('MainCtrl', [function() { this.username = 'nothing'; self.change = function() { self.username = 'changed'; self.password = 'password'; }; }]); </script> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | <form ng-submit="ctrl.submit()"> <input type="text" ng-model="ctrl.user.username"> <input type="password" ng-model="ctrl.user.password"> <input type="submit" value="Submit"> </form> <form ng-submit="ctrl.submit()" name="myForm"> <input type="text" ng-model="ctrl.user.username" required ng-minlength="4"> <input type="password" ng-model="ctrl.user.password" required> <input type="submit" value="Submit" ng-disabled="myForm.$invalid"> </form> |
Form states in AngularJS
$invalid
, AngularJS sets this state when any of the validations (required, ng-minlength, and others) mark any of
the fields within the form as invalid.$valid
, The inverse of the previous state, which states that all the validations in the form are currently evaluating to
correct.$pristine
,All forms in AngularJS start with this state. This allows you to figure out if a user has started typing in and
modifying any of the form elements. Possible usage: disabling the reset button if a form is pristine.$dirty
, The inverse of $pristine, which states that the user made some changes (he can revert it, but the $dirty
bit is set).$error
, This field on the form houses all the individual fields and the errors on each form element. Built-in AngularJS validators
required
,ng-required
, Unlike required, which marks a field as always required, the ng-required directive allows us to
conditionally mark an input field as required based on a Boolean condition in the controller.ng-minlength
and ng-maxlength
ng-pattern
type="email"
type="number"
,Can also have additional attributes for min and max values of the
number itself.type="date"
, This expects the date to be in yyyy-mm-dd formattype="url"
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | <form ng-submit="ctrl.submit()" name="myForm"> <input type="text" name="uname" ng-model="ctrl.user.username" required ng-minlength="4"> <span ng-show="myForm.uname.$error.required"> This is a required field </span> <span ng-show="myForm.uname.$error.minlength"> Minimum length required is 4 </span> <span ng-show="myForm.uname.$invalid"> This field is invalid </span> <input type="password" name="pwd" ng-model="ctrl.user.password" required> <span ng-show="myForm.pwd.$error.required"> This is a required field </span> <input type="submit" value="Submit" ng-disabled="myForm.$invalid"> </form> |
AngularJS adds and removes the CSS classes to and from the forms and input elements.
$invalid
, ng-invalid$valid
, ng-valid$pristine
, ng-pristine$dirty
, ng-dirtyrequired
, ng-valid-required or ng-invalid-requiredmin
, ng-valid-min or ng-invalid-minmax
, ng-valid-max or ng-invalid-maxminlength
, ng-valid-minlength or ng-invalid-minlengthmaxlength
, ng-valid-maxlength or ng-invalid-maxlengthpattern
, ng-valid-pattern or ng-invalid-patternurl
, ng-valid-url or ng-invalid-urlemail
, ng-valid-email or ng-invalid-emaildate
, ng-valid-date or ng-invalid-datenumber
, ng-valid-number or ng-invalid-number1 2 3 4 5 6 7 8 9 | .username.ng-valid { background-color: green; } .username.ng-dirty.ng-invalid-required { background-color: red; } .username.ng-dirty.ng-invalid-minlength { background-color: lightpink; } |
We sometimes run into cases where we need subsections of our form to be valid as a group, and to check and ascertain its validity.
AngularJS provides an ng-form directive, which acts similar to form but allows nesting, so that we can accomplish the requirement of grouping related form fields
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | <form novalidate name="myForm"> <input type="text" class="username" name="uname" ng-model="ctrl.user.username" required="" placeholder="Username" ng-minlength="4" /> <input type="password" class="password" name="pwd" ng-model="ctrl.user.password" placeholder="Password" required="" /> <ng-form name="profile"> <input type="text" name="firstName" ng-model="ctrl.user.profile.firstName" placeholder="First Name" required> <input type="text" name="middleName" placeholder="Middle Name" ng-model="ctrl.user.profile.middleName"> <input type="text" name="lastName" placeholder="Last Name" ng-model="ctrl.user.profile.lastName" required> <input type="date" name="dob" placeholder="Date Of Birth" ng-model="ctrl.user.profile.dob"> </ng-form> <span ng-show="myForm.profile.$invalid"> Please fill out the profile information </span> <input type="submit" value="Submit" ng-disabled="myForm.$invalid"/> </form> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | <!-- textarea --> <textarea ng-model="ctrl.user.address" required></textarea> <!-- checkbox --> <input type="checkbox" ng-model="ctrl.user.agree"> <!-- What if we wanted to assign the string YES or NO to our model --> <input type="checkbox" ng-model="ctrl.user.agree" ng-true-value="YES" ng-false-value="NO"> <!-- oneway binding --> <input type="checkbox" ng-checked="sport.selected === 'YES'"></input> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <div ng-init="otherGender = 'other'"> <input type="radio" name="gender" ng-model="user.gender" value="male">Male <input type="radio" name="gender" ng-model="user.gender" value="female">Female <input type="radio" name="gender" ng-model="user.gender" ng-value="otherGender"> </div> |
We assign it as part of the initialization block (ng-init
).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | <div ng-init="location = 'India'"> <select ng-model="location"> <option value="USA">USA</option> <option value="India">India</option> <option value="Other">None of the above</option> </select> </div> <select ng-model="ctrl.selectedCountryId" ng-options="c.id as c.label for c in ctrl.countries"> </select> <select ng-model="ctrl.selectedCountry" ng-options="c.label for c in ctrl.countries"> </select> <!-- this.selectedCountryId = 2; --> <!-- this.selectedCountry = this.countries[1]; --> |
AngularJS services are functions or objects that can hold behavior or state across our application. Each AngularJS service is instantiated only once, so each part of our application gets access to the same instance of the AngularJS service.
Controllers are stateful, but ephemeral. That is, they can be destroyed and re-created multiple times throughout the course of navigating across a Single Page Application.
When we say “services” in AngularJS, we include factories, services, and providers.
Any service known to AngularJS (internal or our own) can be simply injected into any other service, directive, or controller by stating it as a dependency.
1 2 | myModule.controller("MainCtrl", ["$log", function($log) {}]); myModule.controller("MainCtrl", function($log) {}); |
$window
, the $window
service in AngularJS is nothing but a wrapper
around the global window object.$location
, the $location
service in AngularJS allows us to
interact with the URL in the browser bar, and get
and manipulate its value. absUrl
, $location.absUrl())
url
, A getter and setter that gets or sets the URL.path
,$location.path()
, $location.path('/new')
search
$location.search()
, returns the search parameter as an object$location.search('test')
, removes the search parameter from Url$location.search('test', 'abc')
$http
, it is the core AngularJS service used to make XHR requests to the server from the application.1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 | angular.module('notesApp', []) .controller('MainCtrl', [function() { var self = this; self.tab = 'first'; self.open = function(tab) { self.tab = tab; }; }]) .controller('SubCtrl', ['ItemService', function(ItemService) { var self = this; self.list = function() { return ItemService.list(); }; self.add = function() { ItemService.add({ id: self.list().length + 1, label: 'Item ' + self.list().length }); }; }]) .factory('ItemService', [function() { var items = [ {id: 1, label: 'Item 0'}, {id: 2, label: 'Item 1'} ]; return { list: function() { return items; }, add: function(item) { items.push(item); } }; }]); |
You should use module.factory()
to define your services if:
When we use a service
, AngularJS assumes that the function
definition passed in as part of the array of
dependencies is actually a JavaScript type/class.
So instead of just invoking the function
and storing its return value, AngularJS will call new on the function to create an instance
of the type/class.
1 2 | // class ItemService angular.module('notesApp', []) .service('ItemService', [ItemService]) |
When we need to set up some configuration for our service
before our application loads, you can use provider
function.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | angular.module('notesApp', []) .provider('ItemService', function() { var haveDefaultItems = true; this.disableDefaultItems = function() { haveDefaultItems = false; }; // This function gets our dependencies, not the // provider above this.$get = [function() { var optItems = []; if (haveDefaultItems) { optItems = [ {id: 1, label: 'Item 0'}, {id: 2, label: 'Item 1'} ]; } return new ItemService(optItems); }]; }) .config(['ItemServiceProvider', function(ItemServiceProvider) { // To see how the provider can change // configuration, change the value of // shouldHaveDefaults to true and try // running the example var shouldHaveDefaults = false; // Get configuration from server // Set shouldHaveDefaults somehow //Assume it magically changes for now if (!shouldHaveDefaults) { ItemServiceProvider.disableDefaultItems(); } }]) |
Note that the provider does not use the same notation as factory and service. It doesn’t take an array as the second argument because providers cannot have dependencies on other services.
The config
function executes before the AngularJS app executes. So we can be
assured that this executes before our controllers, services, and other functions.
The AngularJS XHR API follows what is commonly known as the Promise interface.
1 2 3 4 5 6 7 8 9 10 | angular.module('notesApp', []) .controller('MainCtrl', ['$http', function($http) { var self = this; self.items = []; $http.get('/api/note').then(function(response) { self.items = response.data; }, function(errResponse) { console.error('Error while fetching notes'); }); }]); |
Both these handlers get passed in a response object, which has the following keys:
headers
The headers for the callstatus
The status code for the responseconfig
The configuration with which the call was madedata
The body of the response from the server$http
provides the following convenience methods to
make certain types of requests: GET
HEAD
POST
DELETE
PUT
JSONP
$http.get(url, config)
can be replaced with:
$http(config)
where the url and the method (GET, in this case)
become part of the configuration object itself.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | { method: string, url: string, params: object, data: string or object, headers: object, xsrfHeaderName: string, xsrfCookieName: string, transformRequest: function transform(data, headersGetter) or an array of functions, transformResponse: function transform(data, headersGetter) or an array of functions, cache: boolean or Cache object, timeout: number, withCredentials: boolean } |
[{key1: 'value1', key2: 'value2'}]
would be converted to:
?key1=value1&key2=value2
. If we use an object instead of a string or a
number for the value, the object will be converted to a JSON string.{'Content-Type': 'text/csv'}
would set the Content-Type header to be text/csv
.xsrfCookieName: The name of the cookie that has the xsrf token to be used for the XSRF handshake.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // A simple transformRequest that takes the JSON // post data and converts it into jQuery like a post data // string is as follows: transformRequest: function(data, headers) { var requestStr; for (var key in data) { if (requestStr) { requestStr += '&' + key + '=' + data[key]; } else { requestStr = key + '=' + data[key]; } } return requestStr; } |
cache: A Boolean or a cache object to use for an application-level caching mechanism. This would be over and above the browser-level caching. If set to true, AngularJS will automatically cache server responses and return them for subsequent requests to the same URL.
We can use the config section of our module, and use the
$httpProvider
to configure these defaults.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 | angular.module('notesApp', []) .controller('LoginCtrl', ['$http', function($http) { var self = this; self.user = {}; self.message = 'Please login'; self.login = function() { $http.post('/api/login', self.user).then( function(resp) { self.message = resp.data.msg; }); }; }]) .config(['$httpProvider', function($httpProvider) { // Every POST data becomes jQuery style $httpProvider.defaults.transformRequest.push( function(data) { var requestStr; if (data) { data = JSON.parse(data); for (var key in data) { if (requestStr) { requestStr += '&' + key + '=' + data[key]; } else { requestStr = key + '=' + data[key]; } } } return requestStr; }); // Set the content type to be FORM type for all post requests // This does not add it for GET requests. $httpProvider.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'; }]); |
The $httpProvider.defaults.headers
object allows us
to set default headers for common, get, post, and put requests.
Each one ($httpProvider.defaults.headers.post
, for example) is an
object, where the key is the header name and the
value is the value of the header.
The following is the list of keys and values that can have defaults set using $httpPro vider (using $httpProvider.defaults):
headers.common
headers.get
headers.put
headers.post
transformRequest
transformResponse
xsrfHeaderName
xsrfCookieName
It usually required planning to create a layer through which all requests would be channeled so that we could add global hooks.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | angular.module('notesApp', []) .factory('MyLoggingInterceptor', ['$q', function($q) { return { request: function(config) { console.log('Request made with ', config); return config; // If an error, not allowed, or my custom condition, // return $q.reject('Not allowed'); }, requestError: function(rejection) { console.log('Request error due to ', rejection); // Continue to ensure that the next promise chain // sees an error return $q.reject(rejection); // Or handled successfully? // return someValue }, response: function(response) { console.log('Response from server', response); // Return a promise return response || $q.when(response); }, responseError: function(rejection) { console.log('Error in response ', rejection); // Continue to ensure that the next promise chain // sees an error // Can check auth status code here if need to // if (rejection.status === 403) { // Show a login dialog // return a value to tell controllers it has // been handled // } // Or return a rejection to continue the // promise failure chain return $q.reject(rejection); } }; }]) .config(['$httpProvider', function($httpProvider) { $httpProvider.interceptors.push('MyLoggingInterceptor'); }]); |
The interceptors will be called in the order we add them to the provider, so we can also control the order in which they are called.
The Promise API was designed to solve this nesting problem and the problem of error handling.
1 2 3 4 5 6 7 8 9 | $http.get('/api/server-config').then(function(configResponse) { return $http.get('/api/' + configResponse.data.USER_END_POINT); }).then(function(userResponse) { return $http.get('/api/' + userResponse.data.id + '/items'); }).then(function(itemResponse) { // Display items here }, function(error) { // Common error handling }); |
If any error happens in any of the functions in the promise chain, AngularJS will find the next closest error handler and trigger it.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | var self = this; self.items = []; self.newTodo = {}; var fetchTodos = function() { return $http.get('/api/note').then( function(response) { self.items = response.data; }, function(errResponse) { console.error('Error while fetching notes'); }); }; fetchTodos(); self.add = function() { $http.post('/api/note', self.newTodo) .then(fetchTodos) .then(function(response) { self.newTodo = {}; }); }; |
If we want to trigger the success handler for the next promise in the chain, we can just return a value from the success or the error handler.
If, on the other hand, we want to trigger the error handler for the next promise in the
chain, we can leverage the $q service in AngularJS.
Just ask for $q as a dependency in our controller and service,
and return $q.reject(data)
from the handler.
1 2 3 4 | xhrCall() .then(S1, E1) //P1 .then(S2, E2) //P2 .then(S3, E3) //P3 |
$q.defer()
Creates a deferred object when we need to create a promise for our own asynchro‐
nous task. Use deferredObject.resolve
and deferredObject.reject
to
trigger the Promise$q.reject()
The return value of this should be returned to ensure that the
promise continues to the next error handler instead of the success handler in the
promise chain.AngularJS’s optional module, ngResource. ngResource
allows
us to take an API endpoint and create an
AngularJS service around it.
/api/project/
returned an array of projects/api/project/17
returned the project with ID 17/api/project/
with a project object as JSON created a new project/api/project/19
with a project object as JSON updated the project
with ID 19/api/project/
deleted all the projects/api/project/23
deleted the project with ID 231 2 3 4 | angular.module('resourceApp', ['ngResource']) .factory('ProjectService', ['$resource', function($resource) { return $resource('/api/project/:id'); }]); |
ProjectService.query()
to get a list of projectsProjectService.save({id: 15}, projectObj)
to update a project with ID 15ProjectService.get({id: 19})
to get an individual project with ID 19Wrap $http
in services
1 2 3 4 5 6 7 8 | angular.module('notesApp', []) .factory('NoteService', ['$http', function($http) { return { query: function() { return $http.get('/api/notes'); } }; }]); |
Use interceptors
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | angular.module('notesApp', []) .factory('AuthInterceptor', ['AuthInfoService', '$q', function(AuthInfoService, $q) { return { request: function(config) { if (AuthInfoService.hasAuthHeader()) { config.headers['Authorization'] = AuthInfoService.getAuthHeader(); } return config; }, responseError: function(responseRejection) { if (responseError.status === 403) { // Authorization issue, access forbidden AuthInfoService.redirectToLogin(); } return $q.reject(responseRejection); } }; }]) .config(['$httpProvider', function($httpProvider) { $httpProvider.interceptors.push('AuthInterceptor'); }]); |
1 2 3 4 5 6 7 8 9 10 11 | <!DOCTYPE html> <html> <body ng-app> <input type="text" ng-model="name" placeholder="Enter your name"> <h1>Hello <span ng-bind="name"></span></h1> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.2.19/angular.js"> </script> </body> </html> |
angular.forEach
Iterator over objects and arrays, to help you write code in a functional manner.angular.fromJson
and angular.toJson
Convenience methods to convert from a string to a JSON object and back from a
JSON object to a string.angular.copy
Performs a deep copy of a given object and returns the newly created copy.angular.equals
Determines if two objects, regular expressions, arrays, or values are equal. Does
deep comparison in the case of objects or arrays.angular.isObject
, angular.isArray
, and angular.isFunction
Convenience methods to quickly check if a given variable is an object, array, or
function.angular.isString
, angular.isNumber
, and angular.isDate
Convenience methods to check if a given variable is a string, number, or date object.Modules are AngularJS’s way of packaging relevant code under a single name.
The module can also depend on other modules as dependencies, which are defined when the module is instantiated.
Module to load as the main entry point for the application by passing the module name
to the ng-app
directive.
1 2 3 4 5 | // define a module angular.module('notesApp', ['notesApp.ui', 'thirdCompany.fusioncharts']); // get a module angular.module('notesApp', []); |
This example defines a module (note the empty array as the second argument), and then
lets AngularJS bootstrap the module through the ng-app
directive.
1 2 3 4 5 6 7 8 9 10 | <!-- File: chapter2/module-example.html --> <html ng-app="notesApp"> <head><title>Hello AngularJS</title></head> <body> Hello 2nd time AngularJS <script type="text/javascript"> angular.module('notesApp', []); </script> </body> </html> |
An AngularJS controller is almost always directly linked to a view or HTML. It acts as the gateway between our model, which is the data that drives our application, and the view, which is what the user sees and interacts with.
The array holds all the dependencies for the controller as string variables, and the last argument in the array is the actual controller function. In this case, because we have no dependencies, the function is the only argument in the array.
We also introduce a new directive, ng-controller
. This is used to tell AngularJS to go
instantiate an instance of the controller with the given name, and attach it to the DOM
element.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 | <body ng-controller="MainCtrl as ctrl"> AngularJS. <button ng-click="ctrl.changeMessage()"> Change Message </button> <div ng-repeat="note in ctrl.notes"> <span class="label"> </span> <span class="status" ng-bind="note.done"></span> </div> <script type="text/javascript"> angular.module('notesApp', []) .controller('MainCtrl', [function() { var self = this; self.message= 'Hello '; self.changeMessage = function() { self.message = 'Goodbye'; }; self.notes = [ {id: 1, label: 'First Note', done: false}, {id: 2, label: 'Second Note', done: false}, {id: 3, label: 'Done Note', done: true}, {id: 4, label: 'Last Note', done: false} ]; }]); </script> </body> |
Use class="ng-cloak"
to hide \{\{\}\}
in angularjs when bootstrap.
1 2 3 | [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak],.ng-cloak, .x-ng-cloak { display: none !important; } |
Each time it hits an ng-controller
or an ng-repeat
directive, it creates what we
call a scope in AngularJS. A scope is the context for that element. The scope dictates
what each DOM element has access to in terms of functions, variables, and the like.
Also note that while the ng-repeat
instances each get their own scope, they still have
access to the parent scope.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | <style> .done { background-color: green;} .pending {background-color: red;} </style> <body ng-controller="MainCtrl as ctrl"> <div ng-repeat="note in ctrl.notes" ng-class="ctrl.getNoteClass(note.done)"> <span class="label"> </span> <span class="assignee" ng-show="note.assignee" ng-bind="note.assignee"> </span> </div> </body> <script type="text/javascript"> angular.module('notesApp', []).controller('MainCtrl', [ function() { var self = this; self.notes = [ {label: 'First Note', done: false, assignee: 'Shyam'}, {label: 'Second Note', done: false}, {label: 'Done Note', done: true}, {label: 'Last Note', done: false, assignee: 'Brad'} ]; self.getNoteClass = function(status) { return { done: status, pending: !status }; }; }]); </script> |
AngularJS treats true, nonempty strings, nonzero numbers, and nonnull JS objects as truthy.
The ng-class
directive can take
strings or objects as values. If it is a string,
it simply applies the CSS classes directly.
If it is an object, AngularJS takes a look at each key of the object,
and depending on whether the value for that key is true or false,
applies or removes the CSS class.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | <div ng-repeat="(author, note) in ctrl.notes"> <span class="label"> </span> <span class="author" ng-bind="author"></span> </div> <script type="text/javascript"> angular.module('notesApp', []) .controller('MainCtrl', [function() { var self = this; self.notes = { shyam: { id: 1, label: 'First Note', done: false }, Misko: { id: 3, label: 'Finished Third Note', done: true }, brad: { id: 2, label: 'Second Note', done: false } }; }]); </script> |
Helper variables in ng-repeat
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | <div ng-repeat="note in ctrl.notes"> <div>First Element: {\{$first}}</div> <div>Middle Element: {\{$middle}}</div> <div>Last Element: {\{$last}}</div> <div>Index of Element: {\{$index}}</div> <div>At Even Position: {\{$even}}</div> <div>At Odd Position: {\{$odd}}</div> <span class="label"> {\{note.label}}</span> <span class="status" ng-bind="note.done"></span> </div> <script type="text/javascript"> var self = this; self.notes = [ {id: 1, label: 'First Note', done: false}, {id: 2, label: 'Second Note', done: false}, {id: 3, label: 'Done Note', done: true}, {id: 4, label: 'Last Note', done: false} ]; </script> |
To optimize performance, ng-repeat
caches or reuses DOM elements if
the objects are exactly the same, according to the hash of the object
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | <!-- DOM Elements are reused every time someone clicks --> <button ng-click="ctrl.changeNotes()">Change Notes</button> <div ng-repeat="note in ctrl.notes2 track by note.id"> \{\{note.$$hashKey}} <span class="label"> </span> <span class="author" ng-bind="note.done"></span> </div> <!-- ng-repeat Across Multiple HTML Elements --> <table> <tr ng-repeat-start="note in ctrl.notes"> <td></td> </tr> <tr ng-repeat-end> <td>Done: </td> </tr> </table> |
1 2 3 4 5 | sudo npm install karma-cli -g npm install karma npm install karma-jasmine karma-chrome-launcher karma init |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 | // File: chapter3/karma.conf.js // Karma configuration module.exports = function(config) { config.set({ // base path that will be used to resolve files and exclude basePath: '', // testing framework to use (jasmine/mocha/qunit/...) frameworks: ['jasmine'], // list of files / patterns to load in the browser files: [ 'angular.min.js', 'angular-mocks.js', 'controller.js', 'simpleSpec.js', 'controllerSpec.js' ], // list of files / patterns to exclude exclude: [], // web server port port: 8080, // level of logging // possible values: LOG_DISABLE || LOG_ERROR || // LOG_WARN || LOG_INFO || LOG_DEBUG logLevel: config.LOG_INFO, // enable / disable watching file and executing tests // whenever any file changes autoWatch: true, // Start these browsers, currently available: // - Chrome // - ChromeCanary // - Firefox // - Opera // - Safari (only Mac) // - PhantomJS // - IE (only Windows) browsers: ['Chrome'], // Continuous Integration mode // if true, it captures browsers, runs tests, and exits singleRun: false }); }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | describe('My Function', function() { var t; // Similar to setup beforeEach(function() { t = true; }); afterEach(function() { t = null; }); it('should perform action 1', function() { expect(t).toBeTruthy(); }); it('should perform action 2', function() { var expectedValue = true; expect(t).toEqual(expectedValue); }); }); |
toEqual
, does a deep equality check between the two objects,
like array.toBe
, expects both items passed to the expect
and the matcher to be the exact same object reference.toBeTruthy
and toBeFalsy
toBeDefined
, toBeUndefined
and toBeNull
toContain
, array passed to the expect contains the element passed to the matchertoMatch
, Used for regular expression checks when the first argument to the expect is a string
that needs to match a specific regular expression pattern.1 2 3 4 5 6 7 8 9 10 11 12 13 14 | angular.module('notesApp', []) .controller('ListCtrl', [function() { var self = this; self.items = [ {id: 1, label: 'First', done: true}, {id: 2, label: 'Second', done: false} ]; self.getDoneClass = function(item) { return { finished: item.done, unfinished: !item.done }; }; }]); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | // File: chapter3/controllerSpec.js describe('Controller: ListCtrl', function() { // Instantiate a new version of my module before each test beforeEach(module('notesApp')); var ctrl; // Before each unit test, instantiate a new instance // of the controller beforeEach(inject(function($controller) { ctrl = $controller('ListCtrl'); })); it('should have items available on load', function() { expect(ctrl.items).toEqual([ {id: 1, label: 'First', done: true}, {id: 2, label: 'Second', done: false} ]); }); it('should have highlight items based on state', function() { var item = {id: 1, label: 'First', done: true}; var actualClass = ctrl.getDoneClass(item); expect(actualClass.finished).toBeTruthy(); expect(actualClass.unfinished).toBeFalsy(); item.done = false; actualClass = ctrl.getDoneClass(item); expect(actualClass.finished).toBeFalsy(); expect(actualClass.unfinished).toBeTruthy(); }); }); // karma start my.conf.js // The examples and tests in this book were run using version 0.12.16 // of Karma, and version 1.2.19 of AngularJS (both the angular.js and // angular-mocks.js files) |
Css 操作
1 2 3 4 5 6 | // 单属性修改 $('div:eq(0)').css('background', 'red'); // 获取 $('div').css('background'); // 多属性修改 $('div').css({'background':'red', 'width':'100px'}); |
选择器
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | // 子代选择器 $('div>p'); // 后继选择器 $('div+p'); // 兄弟选择器 $('div~p'); // 过滤选择器 $('ul:eq(3) li:not(:first)') $('ul li:lt(12):gt(3)'); // 这样不行 // 筛选选择器 $().parent() $().children('p') $().siblings() // 滑动切换动画 $().slideDown(1000); // 向下滑动切换 $().slideUp(500); $().slideToggle(600); // 先清空排队动画 , 再执行动画 !!!!!!!!!!! $().stop().slideUp(500); // 排他 $(this).css('background', 'red').siblings().css('background', 'blue') |
jq动画机制, 里面的所有动画都遵循排队机制, 所有没有执行完的动画都会 排队等待执行.
Jquery中的选择器是层层过滤的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | // 获得索引 $(this).index(); $('li').eq(index); // 修改标签属性 $(this).attr('key','value'); // 类属性操作 $(this).toggleClass("current"); $(this).addClass("current"); $(this).removeClass("current"); // 透明度动画 $(this).fadeOut(); // 淡出 $(this).fadeIn(); // 淡入 $(this).fadeTo(动画时间, 0.5); // 透明到 |
节点控制就是对文档当中的标签控制
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | var tag = $('<li>ok</li>'); $(this).append(tag); // 从内部, 前面插入 $(this).prepend(tag); // 同级之后 $(this).after(tag); $(this).before(tag); // 直接删除 $(this).remove(); // 清空内部节点 $(this).empty(); //获取 or 修改参数 $(this).val(); // 替换指定的节点 $(this).replaceWith('<h3></h3>'); // 对现有节点的修改, 都是选择器的形式 $(this).insertAfter(); // 把*插入*之后 $(this).insertBefore(); // 把*插入*之前 // 链式动画 $(this).animate({left: 300, top: 300}, 500).animate({top: 200}, 300); // 加工函数 $(this).each(function(index, element){ $(element).css('background-position','0 ' + num + 'px'); }); // 浏览器的宽度 高度 $(window).width(); $(window).scrollTop(); $(this).scrollLeft(); $(this).animate({scrollTop:2000 }, 500); $('html, body').stop().animate({scrollTop: 2000}, 500) |
网页上所有弹窗效果都可以用创建节点实现
1 2 3 | $(window).mousemove(function(e){
event.pageX; evnet.pageY;
})
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | var div = document.getElementById(); div.style.display = "block"; div.className = "box"; // 基于换肤 link.href = "css2.css"; div.onmouseover = function(){ this.style.display = "block"; } div.innerHTML = "<font></font>"; var inputArr = document.getElementByTagName("input"); inputArr[0].checked = true; // update 会在一秒后执行 setInterval(update, 1000); update(); // 让div移动起来 div.style.left = oDiv.offsetLeft + 5 + 'px'; // 不包括外边距的实际高度 odiv.offsetWidth; // ui 所有的li的长度 vaqr uiLength = li.offsetWidth*aLi.length; // 复制一份 ui.innerHTML += ui.innerHTML; // 获取计算后的样式 // IE div.currentStyle.width; // Firefox getComputedStyle(div, flase).width; |
JS类型
字符串转换
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | // 从字符串中提取数字 parseInt("12px34"); // 12 parseInt("xxx"); // NaN parseFloat("12.1"); 33 + NaN; // NaN isNaN(NaN); NaN == NaN; // false "12" - "5"; // 7 function(){ arguments.length(); arguments.callee.caller; } |
数组
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 | var arr = [1, 2]; arr.length = 0; // 清空数组 // 尾部操作 arr.push(3); arr.pop(); // 头部 arr.shift(); arr.unshift(3); arr.concat(arr2); var str = arr.join('_'); str.split('_'); arr.splice(起始, length); // 从中间删除 arr.splice(起始, 0, 'a', 'b');// 从中间插入 arr.splice(起始, 2, 'a', 'b'); // 替换 |
文档碎片, 只渲染一次, 提高速度(插入), 理论上
1 2 3 | var oFrag = document.createDocumentFragment(); oFrag.appendChild(li); ul.appendChild(oFrag); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | // 火狐 下空行当做一个子节点 var nodes = div.childNodes; // 3 文本节点 // 1 元素节点 div.nodeType; if(childNodes[i].nodeType == 1) {} // childNodes 的兼容版, 但是包括空字符文本节点 div.children; div.parrentNode; // 用来定位的父元素 div.offsetParent; // 获取第一个子节点 oFirst = oUl.firstElementChild || oUl.firstCihld oList = oUl.lastElementChild || oUl.lastChild; oNext = oUl.nextSibling || oUl.nextElementSibling; oPrevious = oUl.previousSibling || oUl.previousElementSibling; // 获取文本属性 oTxt.value = "123"; oTxt.setAttribute("value", "123"); var id = oTxt.getAttribute("value"); // 通过 class 来选择元素 var aEle = div.getElementsByTagName('*'); ale[0].className; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 | var returnedNode = someNode.appendChild(newNode); log(returnedNode == newNode); // true returnedNode = someNode.insertBefore(newNode, null); log(returnedNode == someNode.lastChild); // true returnedNode = someNode.insertBefore(newNode, someNode.firstChild); log(returnedNode == someNode.firstChild); // true returnedNode = someNode.insertBefore(newNode, someNode.lastChild); log(newNode == someNode.childNodes[someNode.childNodes.length - 2]); // 替换 var returnedNode = someNode.replaceChild(newNode, someNode.firstchild); var formerFirstChild = someNode.removeChild(someNode.firstChild); var html = document.documentElement; html == docuemnt.childNodes[0]; html == docuemnt.firstChild; var body = document.body; var doctype = document.doctype; // 取得完整的URL var url = document.URL; // 取得域名 var domain = document.domain; // 获得来源域名的URL var referer = document.referer; // 当前页面是 p2p.wrox.com document.domain = "wrox.com"; // ok document.domain = "nczonline.net"; // failed // 查找元素 document.getElementById("myDiv"); // 返回 HTMLCollection var imgs = document.getElementByTagName("img"); imgs.length; imgs[0].src; imgs.item(0).src; imgs.namedItem("myImage"); // 获取文档中的所有元素 var allElements = document.getElementByTagName("*") document.anchors; document.forms; document.images; document.links; // 文档写入 document.write("<strong>" + (new Date()).toString() + "</strong>"); element.tagName.toLowerCase() == "div"; // 判断是不是div var div = document.getElementBy("myDiv"); div.tagName == div.nodeName; // true div.id; div.className; div.title; div.lang; div.dir; div.getAttribute("title"); div.removeAttribute("title"); div.setAttribute("title", "xx"); element.attributes.getNameItem("id").nodeValue; element.attributes.removeNameItem("id"); var div = document.createElement("div"); div.di = ""; div.className = "box"; document.body.appendChild(div); document.createTextNode("Hello world."); // 合并多个文本节点 element.normalize(); val attr = document.createAttribute("align"); attr.value = "left"; element.setAttributeNode(attr); ul.removeChild(btn.parentNode); |
根据html5属性, 自定义特性应该加上 data-
前缀
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 | document.onclick = function(ev){ // 可视区距离 if(ev) { // firefox ev.clientX; ev.clientY; } else { // IE event.clientX; event.clientY; } // or var oEent = ev || event; // 阻止冒泡 oEvent.cancelBubble = true; } // 滚动条距离 // firefox document.documentElement.scrollTop; // chrome document.body.scrollTop; // onpress = onkeydown + onkeyup; document.onkeydown = function(ev){ // shiftKey altKey if(en.ctrlKey){} } // 右键菜单 document.oncontextmenu = function(){ if(ev.preventDeault){ // 火狐 ev.preventDefault(); } // 阻止默认行为 return false; } // 阻止提交 form.onsubmit = function(){return false;} // 阻止填入 txt.onkeydown = function(){return false;} |
1 2 3 4 5 6 7 8 9 10 | document.cookie = "user=blue;expires=" + (new Date()); // 不会覆盖 document.cookie = "pass=123"; // 获得 document.cookie.split("; "); // 删除cookie var expires = "前一天"; document.cookie = "user=blue;expires=" + expires; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | // target must be a integer function startMove(target){ clearInterval(obj.timer); // 缓存运动 var speed = (targetOffset - div.offsetLeft)/8; speed = speed<0?Math.floor(speed):Math.ceil(speed); obj.timer = setInterval(function(){} // 匀速运动 // 缓存运动直接等于 if(Math.abs(oDiv.offsetLeft-target)<speed) { clearInterval(timer); oDiv.style.left = iTarget + "px"; } else { move(); } , 30); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | function getStyle(obj, attr){ if(obj.currentStyle){ return obj.currentStyle[attr];} else { return getComputedStyle(obj, flase);} } function startMov(obj, attr, iTarget){ clearInterval(obj.timer); obj.timer = setInterval(function(){ var iCur; if(attr == 'opacity'){ iCur = parseInt(parseFloat(getStyle(obj, attr))*100) } else { iCur = parseInt(getStyle(obj, attr)); } var iSpeed = (iTarget - iCur)/8; iSpeed = iSpeed>0?Math,ceil(iSpeed): Math.floor(iSpeed); if(iCur == iTarget){ clearInterval(obj.timer);} else { if(attr == 'opacity'){ obj.style.filter = 'alpha(opacity:' + (iCur + iSpeed)+ ')' obj.style.filter.opacity = ( iCur + iSpeed)/100; } else { obj.style[attr] = iCur + iSpeed + "px"; } } }, 30); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 | function startMove(obj, json, fn) { clearInterval(obj.timer); obj.timer=setInterval(function (){ var bStop=true; //这一次运动就结束了——所有的值都到达了 for(var attr in json) { //1.取当前的值 var iCur=0; if(attr=='opacity') { iCur=parseInt(parseFloat(getStyle(obj, attr))*100); } else { iCur=parseInt(getStyle(obj, attr)); } //2.算速度 var iSpeed=(json[attr]-iCur)/8; iSpeed=iSpeed>0?Math.ceil(iSpeed):Math.floor(iSpeed); //3.检测停止 if(iCur!=json[attr]) { bStop=false; } if(attr=='opacity') { obj.style.filter='alpha(opacity:'+(iCur+iSpeed)+')'; obj.style.opacity=(iCur+iSpeed)/100; } else { obj.style[attr]=iCur+iSpeed+'px'; } } if(bStop) { clearInterval(obj.timer); if(fn) { fn(); } } }, 30) } |
speed++
speed--
1 2 3 4 5 6 7 8 9 10 11 12 13 | function startMove(){ obj.timer = setInterval(function(){ iSpeed += (iTarget-height)/5; iSpeed *= 0.7; if(Math.abs(speed)<1 && Math.abs(iTarget-height)<1){ clearInterval(obj.timer); obj.style.height = iTarget + 'px'; } else { height += iSpeed; obj.style.height = height + 'px'; } }, 30); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 | var timer=null; var iSpeedX=0; var iSpeedY=0; function startMove() { clearInterval(timer); timer=setInterval(function (){ var oDiv=document.getElementById('div1'); iSpeedY+=3; var l=oDiv.offsetLeft+iSpeedX; var t=oDiv.offsetTop+iSpeedY; if(t>=document.documentElement.clientHeight-oDiv.offsetHeight) { iSpeedY*=-0.8; iSpeedX*=0.8; t=document.documentElement.clientHeight-oDiv.offsetHeight; } else if(t<=0) { iSpeedY*=-1; iSpeedX*=0.8; t=0; } if(l>=document.documentElement.clientWidth-oDiv.offsetWidth) { iSpeedX*=-0.8; l=document.documentElement.clientWidth-oDiv.offsetWidth; } else if(l<=0) { iSpeedX*=-0.8; l=0; } if(Math.abs(iSpeedX)<1) { iSpeedX=0; } if(Math.abs(iSpeedY)<1) { iSpeedY=0; } if(iSpeedX==0 && iSpeedY==0 && t==document.documentElement.clientHeight-oDiv.offsetHeight) { clearInterval(timer); alert('停止'); } else { oDiv.style.left=l+'px'; oDiv.style.top=t+'px'; } document.title=iSpeedX; }, 30); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | // IE if(obj.attachEvent){ obj.attachEvent('onclick', function(){ this; // this is window }); } else { // FF oBtn.addEventListener('click', func, false); // this is oBtn } obj.detachEvent('onclick', fun); obj.removeEventListener('onclick', fun); // IE 捕获所有事件 obj.setCapture(); // IE 释放所有事件 obj.releaseCapture(); // ie chrome 鼠标滚轮 obj.onmousewheel = func; // FF DOM 事件 obj.addEventistener("DOMMouseScroll", func); |
1 2 3 4 5 6 7 8 9 10 11 12 | str.search('d'); // 返回要查找的字符串 第一次出现的位置 str.substring(1, 4); // 不包含最后一个位子 var re = /\d+/g; // m multiline str.match(re); re.text(str); // 去首尾空格 var str = /^\s*|\s*$/; // 匹配中文 var str = /\u4e00-\u9fa5/; new RegExp('\\b' + className + '\\b', 'i'); |
预判加载, 自动加载下一张图片
延迟加载, 先加载html框架, 在加载图片
1 2 | var img = new Imgage(); img.src = "http://..."; |
document.domain = 'a.com'
mouseover, mouseout 连续触发问题, 可以用 mouseenter, mouseleave 替换
]]>1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 | <!DOCTYPE html> <html> <head> <title>A simple blog </title> <meta name="viewport" content="width=device-width, initial- scale=1.0"/> <link href="css/bootstrap.min.css" rel="stylesheet"/> <link href="css/custom.css" rel="stylesheet"/> </head> <body> <nav class="navbar navbar-default" role="navigation"> <div class="container"> <div class="navbar-header"> <button type="button" class="navbar-toggle" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1"> <span class="sr-only">Toggle navigation</span> <span class="icon-bar"></span> <span class="icon-bar"></span> <span class="icon-bar"></span> </button> <a class="navbar-brand" href="#">Blog</a> </div> <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1"> <ul class="nav navbar-nav"> <li class="active"><a href="#">Home</a></li> <li><a href="#">Archive</a></li> <li><a href="#">About</a></li> <li><a href="#">Contact</a></li> </ul> </div> </div> </nav> <div class="container"> <div class="content"> <div class="jumbotron"> <div class="container"> <h1>A simple blog</h1> </div> </div> <article> <header> <h2>Extending Bootstrap</h2> <p><time pubdate="pubdate">1/12/2012 3:36 PM</time> · <a href="#">Blogger</a></p> </header> <p>Recently I stumbled on a book on extending Twitter Bootstrap and it really...</p> <p class="read-more"><a href="#">Read more »</a></p> <footer> <ul class="list-inline"> <li><a href="#" class="label label-primary">Bootstrap</a></li> <li><a href="#" class="label label-primary">CSS</a></li> <li><a href="#" class="label label-primary">LESS</a></li> <li><a href="#" class="label label-primary">JavaScript</a></li> <li><a href="#" class="label label-primary">Grunt</a></li> </ul> </footer> </article> </div> </div> <script src="https://code.jquery.com/jquery.js"></script> <script src="//netdna.bootstrapcdn.com/bootstrap/3.1.1/js/bootstrap.min.js"></script> </body> </html> |
1 2 3 4 5 6 7 | <!-- `@import "bootstrap/bootstrap";` --> <!-- recess less/main.less --compile > css/main.css --> <!-- Remove it --> <link href="css/bootstrap.min.css" rel="stylesheet"/> <link href="css/custom.css" rel="stylesheet"/> <!-- Add it --> <link href="main.css" rel="stylesheet"/> |
custom-variables.less
1 2 3 4 5 6 7 | @brand-color: #bada55; @navbar-default-bg: darken(@brand-primary, 10%); @navbar-default-brand-hover-color: #fff; @navbar-default-link-color: #fff; @navbar-default-link-active-color: lighten(@brand-primary, 25%); @navbar-default-link-active-bg: darken(@brand-primary, 20%); @jumbotron-bg: lighten(@brand-primary, 30%); |
main.less
1 2 | @import "custom-theme"; @import "custom-variables"; |
custom-theme.less
1 2 3 4 5 6 7 8 9 | .content {.make-md-column(9);} article {margin-bottom: 40px;} .sidebar {.make-md-column(3);} .sidebar-avatar { display: block; margin-bottom: 20px; max-width: 100%; } .sidebar-bio {color: @gray;} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 | <div class="row"> <div class="content"> <div class="jumbotron"> <div class="container"> <h1>A simple blog</h1> </div> </div> <article> <header> <h2>Extending Bootstrap</h2> <p><time pubdate="pubdate">1/12/2012 3:36 PM</time> · <a href="#">Blogger</a></p> </header> <p>Recently I stumbled on a book on extending Twitter Bootstrap and it really ...</p> <p class="read-more"><a href="#">Read more »</a></p> <footer> <ul class="list-inline"> <li><a href="#" class="label label- primary">Bootstrap</a></li> <li><a href="#" class="label label- primary">CSS</a></li> <li><a href="#" class="label label- primary">LESS</a></li> <li><a href="#" class="label label- primary">JavaScript</a></li> <li><a href="#" class="label label- primary">Grunt</a></li> </ul> </footer> </article> </div> <aside class="sidebar"> <img class="sidebar-avatar" src="http:// lorempixel.com/400/400/cats" alt="Avatar"/> <p class="sidebar-bio">Christoffer is a web developer that Lorem ipsum dolor sit amet, consectetur adipisicing elit. Asperiores, maxime, neque? Assumenda at commodi et eum illum, incidunt ipsa laborum molestias, necessitatibus numquam quod ratione sint vero. Amet, facilis iusto. </p> </aside> </div> |
1 2 3 4 5 6 | sudo npm install –g grunt-cli npm init npm install grunt npm install grunt-contrib-less npm install grunt-contrib-watch |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 | module.exports = function (grunt) { // Grunt configuration grunt.initConfig({ less: { app: { files: {"less/main.less": "css/main.css"} } }, watch: { styles: { files: ["less/**/*.less"], tasks: ["less:app"], options: {spawn: false} } } }); // Load plugins grunt.loadNpmTasks("grunt-contrib-less"); grunt.loadNpmTasks("grunt-contrib-watch"); }; |
grunt less:app
grunt watch:styles
livereload: true
, 浏览器自动加载更改
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @grid-columns: 24; .sidebar { .make-md-column(6); } .content { .make-md-column(18); } @grid-gutter-width: 50px; @screen-xs-min: 500px; @screen-sm-min: 790px; @screen-md-min: 1020px; @screen-lg-min: 1240px; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | @grid-columns: 12; @grid-gutter-width: 30px; @grid-float-breakpoint: 768px; .wrapper { .make-row(); } .content-main { .make-lg-column(8); } .content-secondary { .make-lg-column(3); .make-lg-column-offset(1); } |
1 2 3 4 5 6 | <h1>h1. Bootstrap heading <small>Secondary text</small></h1> <h2>h2. Bootstrap heading <small>Secondary text</small></h2> <h3>h3. Bootstrap heading <small>Secondary text</small></h3> <h4>h4. Bootstrap heading <small>Secondary text</small></h4> <h5>h5. Bootstrap heading <small>Secondary text</small></h5> <h6>h6. Bootstrap heading <small>Secondary text</small></h6> |
通过添加 .lead
类可以让段落突出显示。
@font-size-base
和 @line-height-base
决定排版尺寸
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | <p class="text-left">Left aligned text.</p> <p class="text-center">Center aligned text.</p> <p class="text-right">Right aligned text.</p> <p class="text-justify">Justified text.</p> <p class="text-nowrap">No wrap text.</p> <p class="text-lowercase">Lowercased text.</p> <p class="text-uppercase">Uppercased text.</p> <p class="text-capitalize">Capitalized text.</p> <!-- 略缩语句 --> <abbr title="attribute">attr</abbr> <!-- 无样式列表 --> <ul class="list-unstyled"> <li>...</li> </ul> <!-- 内联样式 --> <ul class="list-inline"> <li>...</li> </ul> <!-- 横排排列 --> <dl class="dl-horizontal"> <dt>...</dt> <dd>...</dd> </dl> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | @import "reset/utilities"; @include global-reset; /* a :hover :active :visited :focus */ a { @include link-colors(#333, #00f, #f00, #555, #f00); } a { @include link-colors( #333, $hover: #00f, $active: #f00, $visited: #555, $focus: #f00); } // a {text-decoration: none;} a:hover{underline} a {@include hover-link} // 隐藏超链接 p.secret a, p.secret a:hover, p.secret a:focus { color: inherit; cursor: inherit; text-decoration: inherit } p.secret a { @include unstyled-link } // 列表标签 ul.features { @include pretty-bullets('pretty-bullet.png', $padding: 10px, $line-height: 22px) } ul.no-bullet { @include no-bullets} li.no-bullet { @include no-bullet } // 标题: default $padding is 4px. ul.nav { @include horizontal-list } // For browsers that support :first-child and :last-child, we can omit the padding // on the outside-facing edge of those elements. // 列表一行展示, 用 ! 分割 ul.words { @include delimited-list("! ") } // 超出自动省略 td.dot-dot-dot { @include ellipsis; } td { @include nowrap } // 图片替换文本 h1.coffee { @include replace-text("coffee-header.png") } |
1 2 3 4 5 6 | @import "compass/layout"; . @include sticky-footer(40px, "#content", "#footer", "#sticky-footer"); // 弹出窗口 绝对定位 a.login { @include stretch(5px, 5px, 5px, 5px) } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | @import "compass/css3"; $experimental-support-for-opera: false; $experimental-support-for-microsoft: false; $experimental-support-for-khtml: false; .notice { @include border-radius(5px); } .h2 { @include box-shadow(#ccc 5px 5px 2px); text-shadow: #ddd -1px 1px 0; background: #999; padding: 1em; } .motion { @include text-shadow( rgba(#000,.5) -200px 0 0, rgba(#000,.4) -400px 0 0, rgba(#000,.3) -600px 0 0, rgba(#000,.2) -800px 0 0 ); font-size: 2em; font-style: italic; text-align: right; } @import "compass"; @include font-face("ChunkFiveRegular", font-files( "Chunkfive-webfont.woff", woff, "Chunkfive-webfont.ttf", ttf, "Chunkfive-webfont.svg", svg), "Chunkfive-webfont.eot", normal, normal); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | //compass install compass/pie @import "compass/css3/pie"; .pie-element { // relative is the default, so passing relative // is redundant, but we do it here for clarity. @include pie-element(relative); } .rounded { @include pie; @include border-radius(20px); } .gradient { @include pie; @include background(linear-gradient(#aaa, #333)); } |
https://github.com/Keyamoon/IcoMoon-limited
<map>
part is a placeholder and should be replaced with
the name of the folder containing your sprite images.
The all-sprites mixin will write all the necessary CSS for the entire sprite map, whereas the second mixin will output CSS for a single named sprite.
1 2 | @include all-<map>-sprites; @include <map>-sprite($name); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | @import "compass/utilities/sprites"; // generate sprite from images/icons/ @import "icons/*.png"; @include all-icons-sprites; .add-button { @extend .icons-box-add; } // 导入单个sprite, 不使用 @include all-icons-sprites .add-button { @include icons-sprite(box-add); } $<map>-<property>: setting; $<map>-<sprite>-<property>: setting; // sprite map 中的间隔 $icons-spacing: 4px; $icons-arrow-spacing:12px; // 会在map中重复 $icons-arrow-repeat: repeat-x; // 左边距 4px, arrow浮动到最右边 $icons-position: 4px; $icons-arrow-position: 100%; // 布局方式, 默认是 vertical $<map>-layout: vertical/horizontal/diagonal/smart; // configuring-automatic-sprites/layout. $icons-layout: smart; // 清理 $<map>-clean-up: true/false; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | @import "icons/*.png"; .next { @include icons-sprite(arrow); width: icons-sprite-width(arrow); height: icons-sprite-height(arrow); } .add-button { @include icons-sprite(box-add); } // 是否自动度量元素的高度和宽度, 会给容器自动添加 width, height $<map>-sprite-dimensions: true/false; $disable-magic-sprite-selectors: true/false; |
Magic sprite selectors are enabled by default, meaning Compass will automatically
output CSS :hover
, :active
, and :target
pseudo selectors for sprites
with names ending in _hover
, _active
, or _target
.
You add arrow.png
and arrow_hover.png
to your sprite folder.
帮助函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 | $icons: sprite-map("icons/*.png", $arrow-spacing: 5px); $icons: sprite-map("icons/*.png", $arrow-spacing: 5px); sprite($map, $sprite, [$offset-x], [$offset-y]) $icons: sprite-map("icons/*.png"); .next { background: sprite($icons, arrow) no-repeat; } .add-button { background: sprite($icons, box-add) no-repeat; } $icons: sprite-map("icons/*.png"); .sprite-base { background: $icons no-repeat; } .next { @extend .sprite-base; background-position: sprite-position($icons, arrow); } .add-button { @extend .sprite-base; @include sprite-background-position($icons, box-add); } // 自动添加 width height $icons: sprite-map("icons/*.png"); .sprite-base { background: $icons no-repeat; } .next { @extend .sprite-base; @include sprite-background-position($icons, arrow); @include sprite-dimensions($icons, arrow); } |
1 2 3 4 5 6 7 8 9 10 11 12 | # 配置文件中 # Increment the deploy_version before every # release to force cache busting. asset_cache_buster do |http_path, real_path| "v=1" end asset_cache_buster :none // 设置相对路径 relative_assets = true |
We encourage you to investigate using a rapid prototyping framework like Serve (http://get-serve.com/) or Middleman (http://middlemanapp.com/), which include support for Sass and Compass out of the box.
compass compile --force -e production
1 2 3 4 5 6 7 8 9 10 | if environment == :production output_style = :compact end // 部署时, 改变路径 http_path = '/my-app' relative_assets = false images_dir = 'images' #locally it's the images folder http_images_dir = 'imgs' #on the webserver it's different |
添加版权信息
1 2 3 4 5 6 | $copyright-year: unquote("2012"); $company-name: unquote("Example, Inc."); /*! Copyright © #{$copyright-year}, #{$company-name} All Rights Reserved. */ |
1 2 | compass compile my_sass_dir/application.scss sass --compass my_sass_dir/application.scss my_css_dir/application.css |
1 2 3 4 5 6 7 8 9 10 11 | # STAGING=true compass compile --force -e production if ENV['STAGING'] relative_urls = true output_style = :compact elsif environment == :production relative_urls = false output_style = :compact else #development relative_urls = true output_style = :expanded end |
1 2 3 4 5 6 7 8 9 10 11 | // compass compile --force -c staging_config.rb -e production eval(File.read("#{File.dirname(__FILE__)}/config.rb")) relative_urls = true output_style = :compact on_stylesheet_save do |filename| # run the gzip tool on the file # generates a file of the same name # plus a .gz at the end. `gzip -f #{file}` end |
PNG is a complex format that can handle a range of image types. Be sure to remove the alpha layer unless you need transparency. We highly recommend that you install the free tool Pngcrush and run it on all your PNG images. http://pmt.sourceforge.net/pngcrush/
Beyond the benefits of parallelization, it’s also important to set up your assets hosts to use a cookieless domain—a domain that doesn’t share cookies with your site. This will result in fewer bytes being sent to your web server with each image request.
1 2 3 4 | asset_host do |asset| host_number = (asset.hash % 4) + 1 "http://img-#{host_number}.example.com" end |
1 2 | background-image: inline-url("logo.gif"); *background-image: image-url("logo.gif"); |
1 2 | gem install css_parser
compass stats
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | $grid-cells: 20; $cell-width: 25px; #main { $main-width: $grid-cells * $cell-width; $main-padding: 10px; width: $main-width; padding: $main-padding; .siderbar {width: ($main-width - $main-padding*2)/4} } $pixels-per-em: 16px/1em; 5em * $pixels-per-em // 80px 1px/2px => 1px/2px; // dont work // that's work $var: 1px; $var/2px => 0.5px (1px/2px) => 0.5px 1 + (1px/2px) => 1.5px abs($number); ceil($number); comparable(13in, 4cm); floor($number); percentage(0.4); // 40%; round($number); unit($number); unitless($number); // 颜色函数 alpha($color); opacity($color); lightness($color); red($color); greyscale($color); invert($color); miix($color-1, $color-2, [$weight]); scale($color, $lightness: 30%); // List Funciton nth(foo bar baz, 2); // bar join($list1, $list2, [$separator]); length(1 2 3); type-of($value); // number string color bool list if($condition, $if-true, $if-false) @function grid-width($cells) { @return ($cell-width + $cell-padding) * $cells; } @mixin thing($class, $prop) { .thing.#{$class} { prop-{$prop}: val; } } @mixin bang-hack($property, $value, $ie6-value) { #{$property}: $value !important; #{$property}: $ie6-value; } content: "This element is #{$color}"; width: calc(10% + #{$padding}); filter: progid:DXImageTransform.Microsoft.Alpha( Opacity=#{$opacity * 100} ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | @for $i from 1 through 5 { .rating-#{$i} { background-image; url(/images/rating-#{$i}.png); } } // count backwards from 10 to 0 @for $i from 0 through 10 { $i: 10 - $i; } // count to 20 by twos @for $i from 0 through 10 { $i: $i * 2; } @each $section in home, about, archive, project { nav .#{section} { background-image: url(/images/nav/#{$section}.png); } } @if $alpha < 0.2 { background-color: black; } @else if $alpha < 0.5 { background-color: gray; } @else { background-color: white; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 | @import "compass/css3"; .rounded { @include border-radius(5px); @include box-shadow(#ccc 5px 5px 2px); } .rounded-one { @include border-corner-radius(top, left, 5px); } .pattern { @include background( linear-gradient( 360deg, #bfbfbf 0%, #bfbfbf 12.5%, #bfbf00 12.5%, #bfbf00 25%, #00bfbf 25%, #00bfbf 37.5%, #bfbf00 37.5%, #00bf00 37.5%, #00bf00 50%, #bf00bf 50%, #bf00bf 62.5%, #bf0000 62.5%, #bf0000 75%, #0000bf 75%, #0000bf 87.5%, #000 87.5%, #000 100%)); height: 300px; margin: 100px auto; width: 400px; } @mixin nb-gradient($bg) { // scale main color to pick $top:scale-color($bg, $lightness: 40%); $middle-1: scale-color($bg, $lightness: 10%); $middle-2: scale-color($bg, $lightness: -5%); $bottom: scale-color($bg, $lightness: -20%); @include background-image(linear-gradient( $top, $middle-1 50%, $middle-2 50%, $bottom)); } div:nth-child(2) { background: green; /* @include border-radius(10px); */ @include rotateX(45deg); @include translate(0, 30px); @include transform-origin(left, right); @include box-sizing(border-box); @include transition(all 1s); &:hover { @include border-corner-radius(top, left, 10px); @include scale(1, 2); } } @include animation(sport 1s ease-out 0 infinite alternate); @include keyframes(sport){ 0% { opacity: 0; } 100% { opacity: 1; } } |
1 2 3 | gem install compass-960-plugin compass create -r ninesixty twelve_col --using 960 require 'ninesixty' |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | <section class="wrapper container_24"> <header class="main grid_24"> Header </header> <section class="content grid_20"> Content </section> <aside id="sidebar grid_4"> The last column </aside> <footer class="main grid_24"> Footer </footer> </section> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | $ninesixty-columns: 24; .wrapper { @include grid-container; header.main, footer.main { @include grid(24); } #sidebar { @include grid(4); } .content { @include grid(20); } } // another usage, to enable container_24 or grid_* .container_24 { @include grid-system(24); } |
(baseline unit/ font-size) = line height
(24px / 36px) = .6666667 em
1 2 3 4 5 6 | h1 {font-size: 48px; line-height: 1.5em} h2 {font-size: 36px; line-height: .666667em} h3 {font-size: 24px; line-height: 1em} h4 {font-size: 20px; line-height: 1.2em} h5 {font-size: 18px; line-height: 1.33333em} p {margin: 1.5em 0} |
compass 实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | @import "compass/typography"; $base-font-size: 16px; $base-line-height: 24px; @include establish-baseline; body { font-family: 'Helvetica Neue', sans-serif; @include debug-vertical-alignment("../images/debug.png"); } h1 {@include adjust-font-size-to(48px)} h2 {@include adjust-font-size-to(36px)} h3 {@include adjust-font-size-to(24px)} h4 {@include adjust-font-size-to(20px)} h5 {@include adjust-font-size-to(18px)} p {margin: 1.5em 0;} |
$ninesixty-columns
(default: 12) controls the default number of grid columns$ninesixty-grid-width
(default: 960px) controls the default overall grid width$ninesixty-gutter-width
(default: 20px) controls the default gutter width$ninesixty-class-separator
(default: '_') sets the word separator for classnames generated by +grid-system1 2 | p {@include leader; @include trailer;} h2.important {@include leader(2); @include trailer(2)} |
The leader mixin adds one baseline unit of margin before the element, whereas the trailer adds one baseline unit of margin after the element.
Compass also provides padding-leader
and
padding-trailer
variants of these mixins
compass create bootstrap -r bootstrap-sass --using bootstrap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | /* 下划线 和 中线可以一起用 */ $link-color: blue; a { color: $link_color } /* 嵌套 */ # content { article { h1 { color: #333} p{margin-bottom: 1.4em } } aside { background-color: #eee } } /* 父选择器 */ article a { color: blue; &:hover { color: red } } #content aside { color: red; body.ie & { color: green} } nav, aside { a { color: blue } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | /*子选择*/ article > section { border: 1px solid #ccc } /* 后继选择, header后面跟着的p */ header + p { font-size: 1.1em } /* 兄弟选择器 article 后面所有的 article */ article ~ article { border-top: 1px dashed #ccc } article { ~ article { border-top: 1px dashed #ccc } > section { background: #eee } dl > { dt { color: #333 } dd { color: #555 } } nav + & { margin-top: 0 } } /* 简化 border-style border-width */ nav { border: { style: solid; width: 1px; color: #ccc; } } nav { border: 1px solid #ccc { left: 1px; right: 0px; } } |
Sass has an @import
rule as well, but Sass does its
importing when it’s compiling to CSS.
1 | @import "colors"; /* import include the colors.scss */ |
The convention for Sass partials is to begin the filenames with _. This tells Sass that it shouldn’t generate an individual CSS file for the partial, and should only use it for imports.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | /* `themes/_night-sky.scss` */ @import "themes/night-sky"; /** It means, if this variable is already declared, leave it alone, but otherwise use this value. **/ $fancybox-width: 400px !default; .fancybox { width: $fancybox-width; } /* If a user sets $fancybox-width before @importing your Sass partial, then your declara- tion of 400px is ignored because of the !default flag. If the user hasn’t set the value of $fancybox-width it’ll default to 400px.*/ |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | /* _blue-theme.scss */ aside { background: blue; color: white; } .blue-theme {@import "blue-theme"} /** 翻译成 .blue-theme { aside { background: blue; color: #fff; } } **/ |
This means you can’t directly import a plain CSS file without having Sass think you want a plain CSS @import as well.
1 2 3 4 5 6 7 8 9 | body { color: #333; // This won't appear in the CSS padding: 0; /* This will appear in the CSS */ } body { color /* This won't appear in the CSS */: #333; padding: 1em; /* Nor will this */ 0; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | @mixin rounded-corners { -moz-border-radius: 5px; -webkit-border-radius: 5px; border-radius: 5px; } .notice { background-color: green; border: 2px solid #00aa00; @include rounded-corners; } @mixin no-bullets { list-style: none; li{ list-style-image: none; list-style-type: none; margin-left; } } ul.plain { color: #444; @include no-bullets; } @mixin link-colors($normal, $hover, $visited){ color: $normal; &:hover {color: $hover;} &:visited {color: $visited;} } a { @include link-colors(blue, red, green); } a{ @include link-colors( $normal: blue, $visited: green; $hover: red ) } /* have default value */ @mixin link-colors ( $normal, $hover: $normal, $visited: $normal, ) { color: $normal; &:hover { color: $hover;} &:visited {color: $visited;} } |
1 2 3 4 5 6 7 8 | .error { border: 1px red; background-color: #fdd; } .seriousError { @extend .error; border-width: 3px; } |
So if .seriousError
@extended
.important.error
,
it would inherit styles for .important.error
and h1.important.error
,
but not for .important
or .error
.
In this case, you’d probably want .seriousError
to @extend
.important
and .error
separately.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | var wroxWin = window.open("http://www.wrox.com", "wroxWindow", "height=400, width=400, top=10, left=10, resizable=yes") wroxWin.resizeTo(500, 500); wroxWin.moveTo(100, 100); wroxWin.close(); log(wroxWin.closed); alert(wroxWin.opener == window); // Timeout var timeoutId = setTimeout(function(){ alert(); }, 1000); clearTimeout(timeoutId); // interval var intervalId = setInterval(doFunc, 500); clearInterval(intervalId); var b = comfirm("any"); // prompt var result = prompt("What is your name?", "") if(result !== null){} |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | log(window.location == document.location); // true location.hash; // #contens location.host; // www.wrox.com:80 location.hostname; // www.worx.com location.href; // http://www.wrox.com/ab/ location.pathname; // /WileyCDA location.port; // 80 location.protocol; // http location.search; // ?q=javascript // 每次修改属性, 都会重新加载 location.assign("http://www.baidu.com") window.location = "httt://www.baidu.com" location.href = "http://www.baidu.com" location.replace(""); // 浏览器不会记录历史 location.reload(); // 可能从缓存中加载 location.reload(true); // 从服务器中加载 |
检测插件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | function hasPlugin(name) { name = name.toLowerCase(); for( var i=0, i< navigator.plugin.length; i++) { if(navigator.plugin[i].name.toLowerCase().indexOf(name) > -1) { return true } } return false; } function hasIEPlugin(name) { try { new activeXObject(name); return true; } catch (ex) { return false; } } function hasFlash() { var result = hasPlugin("Flash"); if (!result) { result = hasIEPlugin("ShockwaveFlash.ShockwaveFlash"); } return result; } |
screen 对象基本上只用来表示客户端的能力, 其中包括浏览器窗口外部的显示器的信息, 如像素宽度和高度等.
1 | window.resizeTo(screen.availWidth, screen.availHeight); |
1 2 3 4 5 6 7 8 9 10 | history.go(-1); // 后退一页 history.go(1); history.go(2); // 前进二页 history.back(); history.forward(); if(history.length == 0 ){} // 用户打开的第一个页面 history.go("wrox.com"); // 跳转到最近的 wrox.com 的页面 |
1 2 3 4 5 6 7 8 9 10 11 12 | var win = window.open('about:blank'); // 清空页面并写入 win.document.write(); win.close(); // 关闭窗口 // 可视区居中, 可以用 fixed 但是 ie6 不支持 window.onresize = window.onload = window.onscroll = function (){ var oDiv = document.getElementById('div'); var scrollTop = document.documentElement.scrollTop || document.body.scrollTop; var t = (document.documentElement.clientHeight - oDiv.offsetHeight)/2; oDiv.style.top = scrollTop + t + 'px'; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 | window.onload=function () { var oBtn=document.getElementById('btn1'); var bSys=true; var timer=null; //如何检测用户拖动了滚动条 window.onscroll=function () { if(!bSys) { clearInterval(timer); } bSys=false; }; oBtn.onclick=function () { timer=setInterval(function (){ var scrollTop=document.documentElement.scrollTop||document.body.scrollTop; var iSpeed=Math.floor(-scrollTop/8); if(scrollTop==0) { clearInterval(timer); } bSys=true; document.documentElement.scrollTop=document.body.scrollTop=scrollTop+iSpeed; }, 30); }; }; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml"> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> <title>无标题文档</title> <style type="text/css"> </style> <link type="text/css" rel="stylesheet" href=""/> <link rel="shortcut icon" href=""/> </head> <body> <form> <input id="male" type="radio"/> <!-- 控制焦点范围 --> <label for="male">男</label> 会进一步类级别,. <!-- 只在html5中有用 --> <label> <input/> </label> </form> </body> </html> |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | h1 { display:block; /* display:none; display: inline-block; visible: false; 占位 */ background:red!Important; /*最高优先级*/ border: 1px #000 solid; border: 1px #000 dashed; /*坐标(0,0) 对住中位线*/ background; url() no-repeat center top; background; url() no-repeat right 0; background; url() no-repeat 0 bottom; <!-- repeat-x repeat-y repeat--> } |
行高, 从文字中心基线出发, 向上到向下延伸一定的距离
测量方法, 从一行文字的最大有效像素到下一行文字的最大有效像素
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | p { line-height: 2.2em; /* line-height: 盒子高度; 垂直居中 */ font-weight: bold; // bold is 700 font-weight: normal; font-style: itatic; letter-spacing: 10px; // 字符间距 word-spacing: 10px; // 单词间距 text-indent: 2em; // 首行缩进 text-decoration: none; // 无下划线 text-decoration: underline; text-decoration: line-through; text-decoration: overline; //顶部 } a:link {} a:visited{} a:hover{} a:active{} |
1 2 3 4 5 6 7 8 | .box { padding: 30px; marging: 20px; border: 1px solid white; padding: 10px 20px 40px 80px; // 上右下左 padding: 10px 40px 80px; // 上 左右(40) 下 padding: 10px 40px; // 上下 左右 } |
hr
标签有兼容性问题, 不建议使用(圆角or直角)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 | /* 清空标签默认样式 */ body, h1, p, input, div, span, a, img, ul, li, ol, dl, dt, dd, h2, h3, h4, h5, h6{ padding: 0px; margin: 0px; list-style: none; /** 兼容性问题 **/ border: 0px; } /** 置body的全局样式(文字三属性) **/ body { color: #393939; font-size: 12px; font-family: "Verdana", "Microsoft YaHei", "Simsun"; font-family: "Microsoft YaHei", "SimHei"; /* 大小 行高 字体*/ font: 100px/1.5 "宋体", "黑体"; } a { color: #393939; text-decoration: none; } a:hover { text-decoration: underline; } |
块级标签, 水平居中
1 2 3 4 | .main { width: 330;// 必须要有宽度 margin: 100px, auto; } |
装饰性的图片用背景
可以用 line-heihgt 文本上下 margin
嵌套排列的两个盒子也有塌陷问题, 给子盒子添加 margin-top
,
会将父盒子一起往下挪,解决办法:
border
属性, 能够完整的划分出盒子的边缘overflow: hidden
行内标签, 浏览器当做文字处理;
如果想要改变行内标签的垂直方向的位置, 通过margin
padding
是不能生效的.
只能通过 line-height
改变垂直方向的位置
浮动是第一种脱离标准流的方式, 半脱离, 只要是浮动的的标签浏览器当做不存在
float: left | right
所有浮动之后的标签显示模式变成了行内块
clear:left
清除左侧浮动的影响;
clear:right
清除右侧浮动的影响; clear:both
控制父容器内容溢出, 显示问题
overflow: hidden
; overflow:auto;
自适应, 垂直滚动条
打断字母, 强制字母换行西阿汉按word-break: break-all
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | <style type="text/css"> .main { margin: 0 auto; width: 1000px; overflow:hidden; // 强制检测浮动流 } .left { float: left; width: 300px; } .right{ float: right; width: 650px; } </style> <div class="main"> <div class="left"> </div> <div class="right"> </div> </div> |
position: relative;
偏移原先位置, 配合 top
, left
, right
, bottom
使用
position: absolute;
完全脱离, 参照物是浏览器; 如果绝对定位的盒子有最近定位的父容器,
那么就以这个父容器为参照物.(子绝对, 父相对)
改变z轴堆叠顺序, z-index: 998
ie6双倍边距问题 如果外边距的方向和浮动方向相同, 那么ie6浏览器肯定会出现双倍边距问题;
如果外边距方向和浮动方向不同, 可能会出现双倍边距问题. 解决办法_display: inline;
, 加下划线, 只针对ie6启用.
ie浏览器, 图片链接的边框线问题. img{border: 0;}
ie6, img底部留白, 显示将回车当做空格
img{display:block;}
div{overflow:hidden;}
内部所有标签都左浮动, 解决非矩形盒子背景为题
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 | .left { float: left; width: 5px; height: 40px; background: url(img/left.jpg); } .center { float: left; height: 40px; line-height: 40px; background: url(img/center.jpg); } .right { float: left; width: 5px; height: 40px; background: url(img/right.jpg); } |
1 2 3 4 5 6 | <li> <span class="left"></span> <div class="center"> </div> <span class="right"></span> </li> |
图片整合技术, css sprite, css 雪碧
针对的图片形式是背景图像
半透明效果, 包括上面的文字也会透明, 所以要hack一下
1 2 | filter: alpha(opacity=60);/*只有ie内核*/ opacity: 0.6; |
网站基本页面类型
1 2 3 4 5 6 | banner_wrapper { width: 100%; overflow: hidden; position: relative; } banner { left: -50%; margin-left: -XXpx; position: absolute; } |
制作小三角
1 2 3 4 5 6 7 | .box { width: 0px; height: 0px; border-left: 10px solid #000; border-right: 10px solid #fff; border-bottom: 10px solid #fff; border-top: 10px solid #fff; overflow: hidden; /**hack ie6 **/ } |
*html.header{width: 100px;}
*+html.header{width: 100px;}
_color: red
*color: red
color:red\9
color:red\0
color:red\9\0
1 2 3 4 5 6 7 8 | <!--[if IE]> 只能被ie识别 ;<![endif]--> <!--[if IE 6]> 只能被ie6识别 ;<![endif]--> <!--[if gte IE 6]> <![endif]--> <!--[if gt IE 6]> ;<![endif]--> <!-- lte lt --> <!-- 判断不是ie --> <!--[if ! ie]><!-->要判断的内容<!--<![endif]--> |
行内块间距问题
加上注释
1 2 | <span></span><!-- --><span></span> |
margin-left: -8px
word-spacing: -8px
font-size: 0
ie6注释引起的bug(多余字符)
position:relative
ie6 li出现空白间隙, 因为li里面浮动太复杂, 导致有间隙;
解决方法 vertical-lign: middle;
绝对定位, 父盒子奇数长宽, 出现间距; 解决办法, 尽量奇数
text-indent: 9999em
控制隐藏文字, 优化 logo
表单标签和表单标签对齐, 或表单标签和普通标签对齐,
那么用浮动 float
, 浮动可以实现完全没有间距的左对齐和顶对齐
ie6 float 浮动会自动展开, 除非给他加确定的宽度
ie6元素高度给设置成19px以下, 设置不了, overflow: hidden
white-space: pre
不合并空格; nowrap
强制在同一行显示所有文本
text-overflow: ellipsis
, 如果文本溢出, 显示省略号
1 | <div title="tooltip"></div> |
hack 居中对齐
1 2 3 4 5 6 7 8 9 10 11 12 | * div{ float: left;} .left, .right { width: 300px; height: 400px; } .content{ height: 300px; width: 100%; } .left { margin-left: -100%;} .right { margin-left: -300px;} .cont_in { margin: 0 300px; } |
1 2 3 4 5 6 | <div class="content"> <div class="cont_in"> </div> </div> <div class="left"> </div> <div class="right"> </div> |
满屏显示技巧
1 2 3 | html, body, ul, li {height: 100%} /* 显示拉条 */ overflow-y: scroll; |